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