580 lines
16 KiB
JavaScript
580 lines
16 KiB
JavaScript
/*
|
|
* This script is used for menu and popup tests. Call startPopupTests to start
|
|
* the tests, passing an array of tests as an argument. Each test is an object
|
|
* with the following properties:
|
|
* testname - name of the test
|
|
* test - function to call to perform the test
|
|
* events - a list of events that are expected to be fired in sequence
|
|
* as a result of calling the 'test' function. This list should be
|
|
* an array of strings of the form "eventtype targetid" where
|
|
* 'eventtype' is the event type and 'targetid' is the id of
|
|
* target of the event. This function will be passed two
|
|
* arguments, the testname and the step argument.
|
|
* Alternatively, events may be a function which returns the array
|
|
* of events. This can be used when the events vary per platform.
|
|
* result - function to call after all the events have fired to check
|
|
* for additional results. May be null. This function will be
|
|
* passed two arguments, the testname and the step argument.
|
|
* steps - optional array of values. The test will be repeated for
|
|
* each step, passing each successive value within the array to
|
|
* the test and result functions
|
|
* autohide - if set, should be set to the id of a popup to hide after
|
|
* the test is complete. This is a convenience for some tests.
|
|
* condition - an optional function which, if it returns false, causes the
|
|
* test to be skipped.
|
|
* end - used for debugging. Set to true to stop the tests after running
|
|
* this one.
|
|
*/
|
|
|
|
const menuactiveAttribute = "_moz-menuactive";
|
|
|
|
var gPopupTests = null;
|
|
var gTestIndex = -1;
|
|
var gTestStepIndex = 0;
|
|
var gTestEventIndex = 0;
|
|
var gActualEvents = [];
|
|
var gAutoHide = false;
|
|
var gExpectedEventDetails = null;
|
|
var gExpectedTriggerNode = null;
|
|
var gWindowUtils;
|
|
var gPopupWidth = -1,
|
|
gPopupHeight = -1;
|
|
|
|
function startPopupTests(tests) {
|
|
document.addEventListener("popupshowing", eventOccurred);
|
|
document.addEventListener("popupshown", eventOccurred);
|
|
document.addEventListener("popuphiding", eventOccurred);
|
|
document.addEventListener("popuphidden", eventOccurred);
|
|
document.addEventListener("command", eventOccurred);
|
|
document.addEventListener("DOMMenuItemActive", eventOccurred);
|
|
document.addEventListener("DOMMenuItemInactive", eventOccurred);
|
|
document.addEventListener("DOMMenuInactive", eventOccurred);
|
|
document.addEventListener("DOMMenuBarActive", eventOccurred);
|
|
document.addEventListener("DOMMenuBarInactive", eventOccurred);
|
|
|
|
// This is useful to explicitly finish a test that shouldn't trigger events.
|
|
document.addEventListener("TestDone", eventOccurred);
|
|
|
|
gPopupTests = tests;
|
|
gWindowUtils = SpecialPowers.getDOMWindowUtils(window);
|
|
|
|
goNext();
|
|
}
|
|
|
|
if (!window.opener && window.arguments) {
|
|
window.opener = window.arguments[0];
|
|
}
|
|
|
|
function finish() {
|
|
if (window.opener) {
|
|
window.close();
|
|
window.opener.SimpleTest.finish();
|
|
return;
|
|
}
|
|
SimpleTest.finish();
|
|
}
|
|
|
|
function ok(condition, message) {
|
|
if (window.opener) {
|
|
window.opener.SimpleTest.ok(condition, message);
|
|
} else {
|
|
SimpleTest.ok(condition, message);
|
|
}
|
|
}
|
|
|
|
function info(message) {
|
|
if (window.opener) {
|
|
window.opener.SimpleTest.info(message);
|
|
} else {
|
|
SimpleTest.info(message);
|
|
}
|
|
}
|
|
|
|
function is(left, right, message) {
|
|
if (window.opener) {
|
|
window.opener.SimpleTest.is(left, right, message);
|
|
} else {
|
|
SimpleTest.is(left, right, message);
|
|
}
|
|
}
|
|
|
|
function disableNonTestMouse(aDisable) {
|
|
gWindowUtils.disableNonTestMouseEvents(aDisable);
|
|
}
|
|
|
|
function eventOccurred(event) {
|
|
if (gPopupTests.length <= gTestIndex) {
|
|
ok(false, "Extra " + event.type + " event fired");
|
|
return;
|
|
}
|
|
|
|
var test = gPopupTests[gTestIndex];
|
|
if ("autohide" in test && gAutoHide) {
|
|
if (event.type == "DOMMenuInactive") {
|
|
gAutoHide = false;
|
|
setTimeout(goNextStep, 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var events = test.events;
|
|
if (typeof events == "function") {
|
|
events = events();
|
|
}
|
|
if (events) {
|
|
if (events.length <= gTestEventIndex) {
|
|
ok(
|
|
false,
|
|
"Extra " +
|
|
event.type +
|
|
" event fired for " +
|
|
event.target.id +
|
|
" " +
|
|
gPopupTests[gTestIndex].testname
|
|
);
|
|
return;
|
|
}
|
|
|
|
gActualEvents.push(`${event.type} ${event.target.id}`);
|
|
|
|
var eventitem = events[gTestEventIndex].split(" ");
|
|
var matches;
|
|
if (eventitem[1] == "#tooltip") {
|
|
is(
|
|
event.originalTarget.localName,
|
|
"tooltip",
|
|
test.testname + " event.originalTarget.localName is 'tooltip'"
|
|
);
|
|
is(
|
|
event.originalTarget.getAttribute("default"),
|
|
"true",
|
|
test.testname + " event.originalTarget default attribute is 'true'"
|
|
);
|
|
matches =
|
|
event.originalTarget.localName == "tooltip" &&
|
|
event.originalTarget.getAttribute("default") == "true";
|
|
} else {
|
|
is(
|
|
event.type,
|
|
eventitem[0],
|
|
test.testname + " event type " + event.type + " fired"
|
|
);
|
|
is(
|
|
event.target.id,
|
|
eventitem[1],
|
|
test.testname + " event target ID " + event.target.id
|
|
);
|
|
matches = eventitem[0] == event.type && eventitem[1] == event.target.id;
|
|
}
|
|
|
|
var modifiersMask = eventitem[2];
|
|
if (modifiersMask) {
|
|
var m = "";
|
|
m += event.altKey ? "1" : "0";
|
|
m += event.ctrlKey ? "1" : "0";
|
|
m += event.shiftKey ? "1" : "0";
|
|
m += event.metaKey ? "1" : "0";
|
|
is(m, modifiersMask, test.testname + " modifiers mask matches");
|
|
}
|
|
|
|
var expectedState;
|
|
switch (event.type) {
|
|
case "popupshowing":
|
|
expectedState = "showing";
|
|
break;
|
|
case "popupshown":
|
|
expectedState = "open";
|
|
break;
|
|
case "popuphiding":
|
|
expectedState = "hiding";
|
|
break;
|
|
case "popuphidden":
|
|
expectedState = "closed";
|
|
break;
|
|
}
|
|
|
|
if (gExpectedTriggerNode && event.type == "popupshowing") {
|
|
if (gExpectedTriggerNode == "notset") {
|
|
// check against null instead
|
|
gExpectedTriggerNode = null;
|
|
}
|
|
|
|
is(
|
|
event.originalTarget.triggerNode,
|
|
gExpectedTriggerNode,
|
|
test.testname + " popupshowing triggerNode"
|
|
);
|
|
}
|
|
|
|
if (expectedState) {
|
|
is(
|
|
event.originalTarget.state,
|
|
expectedState,
|
|
test.testname + " " + event.type + " state"
|
|
);
|
|
}
|
|
|
|
if (matches) {
|
|
gTestEventIndex++;
|
|
if (events.length <= gTestEventIndex) {
|
|
setTimeout(checkResult, 0);
|
|
}
|
|
} else {
|
|
info(`Actual events so far: ${JSON.stringify(gActualEvents)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function checkResult() {
|
|
var step = null;
|
|
var test = gPopupTests[gTestIndex];
|
|
if ("steps" in test) {
|
|
step = test.steps[gTestStepIndex];
|
|
}
|
|
|
|
if ("result" in test) {
|
|
await test.result(test.testname, step);
|
|
}
|
|
|
|
if ("autohide" in test) {
|
|
gAutoHide = true;
|
|
document.getElementById(test.autohide).hidePopup();
|
|
return;
|
|
}
|
|
|
|
goNextStep();
|
|
}
|
|
|
|
function goNextStep() {
|
|
info(`events: ${JSON.stringify(gActualEvents)}`);
|
|
gTestEventIndex = 0;
|
|
gActualEvents = [];
|
|
|
|
var step = null;
|
|
var test = gPopupTests[gTestIndex];
|
|
if ("steps" in test) {
|
|
gTestStepIndex++;
|
|
step = test.steps[gTestStepIndex];
|
|
if (gTestStepIndex < test.steps.length) {
|
|
test.test(test.testname, step);
|
|
return;
|
|
}
|
|
}
|
|
|
|
goNext();
|
|
}
|
|
|
|
function goNext() {
|
|
// We want to continue after the next animation frame so that
|
|
// we're in a stable state and don't get spurious mouse events at unexpected targets.
|
|
window.requestAnimationFrame(function () {
|
|
setTimeout(goNextStepSync, 0);
|
|
});
|
|
}
|
|
|
|
function goNextStepSync() {
|
|
if (
|
|
gTestIndex >= 0 &&
|
|
"end" in gPopupTests[gTestIndex] &&
|
|
gPopupTests[gTestIndex].end
|
|
) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
gTestIndex++;
|
|
gTestStepIndex = 0;
|
|
if (gTestIndex < gPopupTests.length) {
|
|
var test = gPopupTests[gTestIndex];
|
|
// Set the location hash so it's easy to see which test is running
|
|
document.location.hash = test.testname;
|
|
info("Starting " + test.testname);
|
|
|
|
// skip the test if the condition returns false
|
|
if ("condition" in test && !test.condition()) {
|
|
goNext();
|
|
return;
|
|
}
|
|
|
|
// start with the first step if there are any
|
|
var step = null;
|
|
if ("steps" in test) {
|
|
step = test.steps[gTestStepIndex];
|
|
}
|
|
|
|
test.test(test.testname, step);
|
|
|
|
// no events to check for so just check the result
|
|
if (!("events" in test)) {
|
|
checkResult();
|
|
} else if (typeof test.events == "function" && !test.events().length) {
|
|
checkResult();
|
|
}
|
|
} else {
|
|
finish();
|
|
}
|
|
}
|
|
|
|
function openMenu(menu) {
|
|
if ("open" in menu) {
|
|
menu.open = true;
|
|
} else if (menu.hasMenu()) {
|
|
menu.openMenu(true);
|
|
} else {
|
|
synthesizeMouse(menu, 4, 4, {});
|
|
}
|
|
}
|
|
|
|
function closeMenu(menu, popup) {
|
|
if ("open" in menu) {
|
|
menu.open = false;
|
|
} else if (menu.hasMenu()) {
|
|
menu.openMenu(false);
|
|
} else {
|
|
popup.hidePopup();
|
|
}
|
|
}
|
|
|
|
function checkActive(popup, id, testname) {
|
|
var activeok = true;
|
|
var children = popup.childNodes;
|
|
for (var c = 0; c < children.length; c++) {
|
|
var child = children[c];
|
|
if (
|
|
(id == child.id && child.getAttribute(menuactiveAttribute) != "true") ||
|
|
(id != child.id && child.hasAttribute(menuactiveAttribute) != "")
|
|
) {
|
|
activeok = false;
|
|
break;
|
|
}
|
|
}
|
|
ok(activeok, testname + " item " + (id ? id : "none") + " active");
|
|
}
|
|
|
|
function checkOpen(menuid, testname) {
|
|
var menu = document.getElementById(menuid);
|
|
if ("open" in menu) {
|
|
ok(menu.open, testname + " " + menuid + " menu is open");
|
|
} else if (menu.hasMenu()) {
|
|
ok(
|
|
menu.getAttribute("open") == "true",
|
|
testname + " " + menuid + " menu is open"
|
|
);
|
|
}
|
|
}
|
|
|
|
function checkClosed(menuid, testname) {
|
|
var menu = document.getElementById(menuid);
|
|
if ("open" in menu) {
|
|
ok(!menu.open, testname + " " + menuid + " menu is open");
|
|
} else if (menu.hasMenu()) {
|
|
ok(!menu.hasAttribute("open"), testname + " " + menuid + " menu is closed");
|
|
}
|
|
}
|
|
|
|
function convertPosition(anchor, align) {
|
|
if (anchor == "topleft" && align == "topleft") {
|
|
return "overlap";
|
|
}
|
|
if (anchor == "topleft" && align == "topright") {
|
|
return "start_before";
|
|
}
|
|
if (anchor == "topleft" && align == "bottomleft") {
|
|
return "before_start";
|
|
}
|
|
if (anchor == "topright" && align == "topleft") {
|
|
return "end_before";
|
|
}
|
|
if (anchor == "topright" && align == "bottomright") {
|
|
return "before_end";
|
|
}
|
|
if (anchor == "bottomleft" && align == "bottomright") {
|
|
return "start_after";
|
|
}
|
|
if (anchor == "bottomleft" && align == "topleft") {
|
|
return "after_start";
|
|
}
|
|
if (anchor == "bottomright" && align == "bottomleft") {
|
|
return "end_after";
|
|
}
|
|
if (anchor == "bottomright" && align == "topright") {
|
|
return "after_end";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/*
|
|
* When checking position of the bottom or right edge of the popup's rect,
|
|
* use this instead of strict equality check of rounded values,
|
|
* because we snap the top/left edges to pixel boundaries,
|
|
* which can shift the bottom/right up to 0.5px from its "ideal" location,
|
|
* and could cause it to round differently. (See bug 622507.)
|
|
*/
|
|
function isWithinAPixel(a, b, message) {
|
|
ok(Math.abs(a - b) < 1, `${message}: ${a}, ${b}`);
|
|
}
|
|
|
|
function compareEdge(anchor, popup, edge, offsetX, offsetY, testname) {
|
|
testname += " " + edge;
|
|
|
|
checkOpen(anchor.id, testname);
|
|
|
|
var anchorrect = anchor.getBoundingClientRect();
|
|
var popuprect = popup.getBoundingClientRect();
|
|
|
|
if (gPopupWidth == -1) {
|
|
ok(
|
|
Math.round(popuprect.right) - Math.round(popuprect.left) &&
|
|
Math.round(popuprect.bottom) - Math.round(popuprect.top),
|
|
testname + " size"
|
|
);
|
|
} else {
|
|
is(Math.round(popuprect.width), gPopupWidth, testname + " width");
|
|
is(Math.round(popuprect.height), gPopupHeight, testname + " height");
|
|
}
|
|
|
|
var spaceIdx = edge.indexOf(" ");
|
|
if (spaceIdx > 0) {
|
|
let cornerX, cornerY;
|
|
let [position, align] = edge.split(" ");
|
|
switch (position) {
|
|
case "topleft":
|
|
cornerX = anchorrect.left;
|
|
cornerY = anchorrect.top;
|
|
break;
|
|
case "topcenter":
|
|
cornerX = anchorrect.left + anchorrect.width / 2;
|
|
cornerY = anchorrect.top;
|
|
break;
|
|
case "topright":
|
|
cornerX = anchorrect.right;
|
|
cornerY = anchorrect.top;
|
|
break;
|
|
case "leftcenter":
|
|
cornerX = anchorrect.left;
|
|
cornerY = anchorrect.top + anchorrect.height / 2;
|
|
break;
|
|
case "rightcenter":
|
|
cornerX = anchorrect.right;
|
|
cornerY = anchorrect.top + anchorrect.height / 2;
|
|
break;
|
|
case "bottomleft":
|
|
cornerX = anchorrect.left;
|
|
cornerY = anchorrect.bottom;
|
|
break;
|
|
case "bottomcenter":
|
|
cornerX = anchorrect.left + anchorrect.width / 2;
|
|
cornerY = anchorrect.bottom;
|
|
break;
|
|
case "bottomright":
|
|
cornerX = anchorrect.right;
|
|
cornerY = anchorrect.bottom;
|
|
break;
|
|
}
|
|
|
|
switch (align) {
|
|
case "topleft":
|
|
cornerX += offsetX;
|
|
cornerY += offsetY;
|
|
break;
|
|
case "topcenter":
|
|
cornerX += -popuprect.width / 2 + offsetX;
|
|
cornerY += offsetY;
|
|
break;
|
|
case "topright":
|
|
cornerX += -popuprect.width + offsetX;
|
|
cornerY += offsetY;
|
|
break;
|
|
case "leftcenter":
|
|
cornerX += offsetX;
|
|
cornerY += -popuprect.height / 2 + offsetY;
|
|
break;
|
|
case "rightcenter":
|
|
cornerX += -popuprect.width + offsetX;
|
|
cornerY += -popuprect.height / 2 + offsetY;
|
|
break;
|
|
case "bottomleft":
|
|
cornerX += offsetX;
|
|
cornerY += -popuprect.height + offsetY;
|
|
break;
|
|
case "bottomcenter":
|
|
cornerX += -popuprect.width / 2 + offsetX;
|
|
cornerY += -popuprect.height + offsetY;
|
|
break;
|
|
case "bottomright":
|
|
cornerX += -popuprect.width + offsetX;
|
|
cornerY += -popuprect.height + offsetY;
|
|
break;
|
|
}
|
|
|
|
isWithinAPixel(popuprect.left, cornerX, testname + " x position");
|
|
isWithinAPixel(popuprect.top, cornerY, testname + " y position");
|
|
return;
|
|
}
|
|
|
|
if (edge == "overlap") {
|
|
isWithinAPixel(
|
|
anchorrect.left + offsetY,
|
|
popuprect.left,
|
|
testname + " position1"
|
|
);
|
|
isWithinAPixel(
|
|
anchorrect.top + offsetY,
|
|
popuprect.top,
|
|
testname + " position2"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (edge.indexOf("before") == 0) {
|
|
isWithinAPixel(
|
|
anchorrect.top + offsetY,
|
|
popuprect.bottom,
|
|
testname + " position1"
|
|
);
|
|
} else if (edge.indexOf("after") == 0) {
|
|
isWithinAPixel(
|
|
anchorrect.bottom + offsetY,
|
|
popuprect.top,
|
|
testname + " position1"
|
|
);
|
|
} else if (edge.indexOf("start") == 0) {
|
|
isWithinAPixel(
|
|
anchorrect.left + offsetX,
|
|
popuprect.right,
|
|
testname + " position1"
|
|
);
|
|
} else if (edge.indexOf("end") == 0) {
|
|
isWithinAPixel(
|
|
anchorrect.right + offsetX,
|
|
popuprect.left,
|
|
testname + " position1"
|
|
);
|
|
}
|
|
|
|
if (0 < edge.indexOf("before")) {
|
|
isWithinAPixel(
|
|
anchorrect.top + offsetY,
|
|
popuprect.top,
|
|
testname + " position2"
|
|
);
|
|
} else if (0 < edge.indexOf("after")) {
|
|
isWithinAPixel(
|
|
anchorrect.bottom + offsetY,
|
|
popuprect.bottom,
|
|
testname + " position2"
|
|
);
|
|
} else if (0 < edge.indexOf("start")) {
|
|
isWithinAPixel(
|
|
anchorrect.left + offsetX,
|
|
popuprect.left,
|
|
testname + " position2"
|
|
);
|
|
} else if (0 < edge.indexOf("end")) {
|
|
isWithinAPixel(
|
|
anchorrect.right + offsetX,
|
|
popuprect.right,
|
|
testname + " position2"
|
|
);
|
|
}
|
|
}
|