diff options
Diffstat (limited to 'toolkit/content/tests/widgets')
81 files changed, 9505 insertions, 0 deletions
diff --git a/toolkit/content/tests/widgets/audio.ogg b/toolkit/content/tests/widgets/audio.ogg Binary files differnew file mode 100644 index 0000000000..a553c23e73 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.ogg diff --git a/toolkit/content/tests/widgets/audio.wav b/toolkit/content/tests/widgets/audio.wav Binary files differnew file mode 100644 index 0000000000..c6fd5cb869 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.wav diff --git a/toolkit/content/tests/widgets/chrome.toml b/toolkit/content/tests/widgets/chrome.toml new file mode 100644 index 0000000000..af2c778947 --- /dev/null +++ b/toolkit/content/tests/widgets/chrome.toml @@ -0,0 +1,68 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "tree_shared.js", + "popup_shared.js", + "window_label_checkbox.xhtml", + "window_menubar.xhtml", + "seek_with_sound.ogg", +] +prefs = ["app.support.baseURL='https://support.mozilla.org/'"] + +["test_contextmenu_menugroup.xhtml"] +skip-if = ["os == 'linux'"] # Bug 1115088 + +["test_contextmenu_nested.xhtml"] +skip-if = ["os == 'linux'"] # Bug 1116215 + +["test_editor_currentURI.xhtml"] + +["test_label_checkbox.xhtml"] + +["test_menubar.xhtml"] +skip-if = ["os == 'mac'"] + +["test_moz_button_group.html"] + +["test_moz_card.html"] + +["test_moz_five_star.html"] + +["test_moz_label.html"] + +["test_moz_message_bar.html"] + +["test_moz_support_link.html"] + +["test_moz_toggle.html"] + +["test_panel_item_accesskey.html"] + +["test_panel_list_accessibility.html"] + +["test_panel_list_anchoring.html"] + +["test_panel_list_in_xul_panel.html"] + +["test_panel_list_min_width_from_anchor.html"] + +["test_popupanchor.xhtml"] +skip-if = [ + "os == 'linux' && os_version == '18.04'", # Bug 1335894 perma-fail on linux 16.04 + "verify && os == 'win'", +] + +["test_popupreflows.xhtml"] + +["test_tree_column_reorder.xhtml"] + +["test_videocontrols_focus.html"] +support-files = [ + "head.js", + "video.ogg", +] +skip-if = [ + "os == 'android'", + "os == 'linux' && debug", # Bug 1765783 +] +["test_videocontrols_onclickplay.html"] diff --git a/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..56917b69ac --- /dev/null +++ b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html @@ -0,0 +1,2 @@ +<video src="seek_with_sound.ogg" controls autoplay=true></video> +<script>window.testExpando = true;</script> diff --git a/toolkit/content/tests/widgets/head.js b/toolkit/content/tests/widgets/head.js new file mode 100644 index 0000000000..d7473fa92d --- /dev/null +++ b/toolkit/content/tests/widgets/head.js @@ -0,0 +1,67 @@ +"use strict"; + +const InspectorUtils = SpecialPowers.InspectorUtils; + +var tests = []; + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function () { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function () { + clearInterval(interval); + nextTest(); + }; +} + +function getElementWithinVideo(video, aValue) { + const shadowRoot = SpecialPowers.wrap(video).openOrClosedShadowRoot; + return shadowRoot.getElementById(aValue); +} + +/** + * Runs querySelectorAll on an element's shadow root. + * @param {Element} element + * @param {string} selector + */ +function shadowRootQuerySelectorAll(element, selector) { + const shadowRoot = SpecialPowers.wrap(element).openOrClosedShadowRoot; + return shadowRoot?.querySelectorAll(selector); +} + +function executeTests() { + return tests + .map(fn => () => new Promise(fn)) + .reduce((promise, task) => promise.then(task), Promise.resolve()); +} + +function once(target, name, cb) { + let p = new Promise(function (resolve, reject) { + target.addEventListener( + name, + function () { + resolve(); + }, + { once: true } + ); + }); + if (cb) { + p.then(cb); + } + return p; +} diff --git a/toolkit/content/tests/widgets/image-zh.png b/toolkit/content/tests/widgets/image-zh.png Binary files differnew file mode 100644 index 0000000000..944a12d39e --- /dev/null +++ b/toolkit/content/tests/widgets/image-zh.png diff --git a/toolkit/content/tests/widgets/image.png b/toolkit/content/tests/widgets/image.png Binary files differnew file mode 100644 index 0000000000..3faa11b221 --- /dev/null +++ b/toolkit/content/tests/widgets/image.png diff --git a/toolkit/content/tests/widgets/mochitest.toml b/toolkit/content/tests/widgets/mochitest.toml new file mode 100644 index 0000000000..7e20352256 --- /dev/null +++ b/toolkit/content/tests/widgets/mochitest.toml @@ -0,0 +1,110 @@ +[DEFAULT] +support-files = [ + "audio.wav", + "audio.ogg", + "file_videocontrols_jsdisabled.html", + "image.png", + "image-zh.png", + "seek_with_sound.ogg", + "video.ogg", + "head.js", + "tree_shared.js", + "test-webvtt-1.vtt", + "test-webvtt-2.vtt", + "videocontrols_direction-1-ref.html", + "videocontrols_direction-1a.html", + "videocontrols_direction-1b.html", + "videocontrols_direction-1c.html", + "videocontrols_direction-1d.html", + "videocontrols_direction-1e.html", + "videocontrols_direction-2-ref.html", + "videocontrols_direction-2a.html", + "videocontrols_direction-2b.html", + "videocontrols_direction-2c.html", + "videocontrols_direction-2d.html", + "videocontrols_direction-2e.html", + "videocontrols_direction_test.js", + "videomask.css", +] + +["test_audiocontrols_dimensions.html"] + +["test_audiocontrols_fullscreen.html"] + +["test_bug898940.html"] + +["test_bug1654500.html"] + +["test_image_recognition.html"] +run-if = ["os == 'mac'"] # Mac only feature. + +["test_image_recognition_unsupported.html"] +skip-if = ["os == 'mac'"] + +["test_image_recognition_zh.html"] +run-if = ["os == 'mac' && os_version != '10.15'"] # Mac only feature, requires > 10.15 to support multilingual results. + +["test_mousecapture_area.html"] + +["test_nac_mutations.html"] + +["test_panel_list_shadow_node_anchor.html"] +support-files = [ + "../../widgets/panel-list/panel-item.css", + "../../widgets/panel-list/panel-list.js", + "../../widgets/panel-list/panel-list.css", +] + +["test_ua_widget_elementFromPoint.html"] + +["test_ua_widget_sandbox.html"] + +["test_ua_widget_unbind.html"] + +["test_videocontrols.html"] +tags = "fullscreen" +skip-if = [ + "os == 'android'", #TIMED_OUT #Bug 1484210 + "os == 'linux'", #TIMED_OUT #Bug 1511256 +] + +["test_videocontrols_audio.html"] + +["test_videocontrols_audio_direction.html"] +skip-if = ["xorigin"] # Rendering of reftest videocontrols_direction-2a.html should not be different to the reference, fails/passes inconsistently + +["test_videocontrols_clickToPlay_ariaLabel.html"] + +["test_videocontrols_closed_caption_menu.html"] + +["test_videocontrols_error.html"] + +["test_videocontrols_iframe_fullscreen.html"] + +["test_videocontrols_jsdisabled.html"] +skip-if = ["os == 'android'"] # bug 1272646 + +["test_videocontrols_keyhandler.html"] +skip-if = [ + "os == 'android'", #Bug 1366957 + "os == 'linux'", #Bug 1366957 +] + +["test_videocontrols_scrubber_position.html"] + +["test_videocontrols_scrubber_position_nopreload.html"] + +["test_videocontrols_size.html"] + +["test_videocontrols_standalone.html"] +skip-if = ["os == 'android'"] # bug 1075573 + +["test_videocontrols_video_direction.html"] +skip-if = [ + "os == 'win'", + "xorigin", # Rendering of reftest videocontrols_direction-2a.html should not be different to the reference, fails/passes inconsistently +] + +["test_videocontrols_video_noaudio.html"] + +["test_videocontrols_vtt.html"] diff --git a/toolkit/content/tests/widgets/popup_shared.js b/toolkit/content/tests/widgets/popup_shared.js new file mode 100644 index 0000000000..0f093193c9 --- /dev/null +++ b/toolkit/content/tests/widgets/popup_shared.js @@ -0,0 +1,602 @@ +/* + * 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 isWithinHalfPixel(a, b, message) { + ok(Math.abs(a - b) <= 0.5, `${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; + } + + is( + Math.round(popuprect.left), + Math.round(cornerX), + testname + " x position" + ); + is( + Math.round(popuprect.top), + Math.round(cornerY), + testname + " y position" + ); + return; + } + + if (edge == "after_pointer") { + is( + Math.round(popuprect.left), + Math.round(anchorrect.left) + offsetX, + testname + " x position" + ); + is( + Math.round(popuprect.top), + Math.round(anchorrect.top) + offsetY + 21, + testname + " y position" + ); + return; + } + + if (edge == "overlap") { + is( + Math.round(anchorrect.left) + offsetY, + Math.round(popuprect.left), + testname + " position1" + ); + is( + Math.round(anchorrect.top) + offsetY, + Math.round(popuprect.top), + testname + " position2" + ); + return; + } + + if (edge.indexOf("before") == 0) { + isWithinHalfPixel( + anchorrect.top + offsetY, + popuprect.bottom, + testname + " position1" + ); + } else if (edge.indexOf("after") == 0) { + is( + Math.round(anchorrect.bottom) + offsetY, + Math.round(popuprect.top), + testname + " position1" + ); + } else if (edge.indexOf("start") == 0) { + isWithinHalfPixel( + anchorrect.left + offsetX, + popuprect.right, + testname + " position1" + ); + } else if (edge.indexOf("end") == 0) { + is( + Math.round(anchorrect.right) + offsetX, + Math.round(popuprect.left), + testname + " position1" + ); + } + + if (0 < edge.indexOf("before")) { + is( + Math.round(anchorrect.top) + offsetY, + Math.round(popuprect.top), + testname + " position2" + ); + } else if (0 < edge.indexOf("after")) { + isWithinHalfPixel( + anchorrect.bottom + offsetY, + popuprect.bottom, + testname + " position2" + ); + } else if (0 < edge.indexOf("start")) { + is( + Math.round(anchorrect.left) + offsetX, + Math.round(popuprect.left), + testname + " position2" + ); + } else if (0 < edge.indexOf("end")) { + isWithinHalfPixel( + anchorrect.right + offsetX, + popuprect.right, + testname + " position2" + ); + } +} diff --git a/toolkit/content/tests/widgets/seek_with_sound.ogg b/toolkit/content/tests/widgets/seek_with_sound.ogg Binary files differnew file mode 100644 index 0000000000..c86d9946bd --- /dev/null +++ b/toolkit/content/tests/widgets/seek_with_sound.ogg diff --git a/toolkit/content/tests/widgets/test-webvtt-1.vtt b/toolkit/content/tests/widgets/test-webvtt-1.vtt new file mode 100644 index 0000000000..20c9fc94cc --- /dev/null +++ b/toolkit/content/tests/widgets/test-webvtt-1.vtt @@ -0,0 +1,10 @@ +WEBVTT + +1 +00:00:00.000 --> 00:00:02.000 +This is a one line text track + +2 +00:00:05.000 --> 00:00:07.000 +- <b>This is a new text track but bolded</b> +- <i>This line should be italicized<i> diff --git a/toolkit/content/tests/widgets/test-webvtt-2.vtt b/toolkit/content/tests/widgets/test-webvtt-2.vtt new file mode 100644 index 0000000000..48dd63a9ee --- /dev/null +++ b/toolkit/content/tests/widgets/test-webvtt-2.vtt @@ -0,0 +1,10 @@ +WEBVTT + +1 +00:00:00.000 --> 00:00:02.000 +Voici un text track en une ligne + +2 +00:00:05.000 --> 00:00:07.000 +- <b>Voici un nouveau text track en gras</b> +- <i>Cette ligne devrait être en italiques<i> diff --git a/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html new file mode 100644 index 0000000000..2d29fe9be4 --- /dev/null +++ b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Audio controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls preload="auto"></audio> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + const audio = document.getElementById("audio"); + const controlBar = getElementWithinVideo(audio, "controlBar"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + audio.addEventListener("loadedmetadata", resolve, {once: true}); + audio.src = "audio.wav"; + }); + }); + + add_task(async function check_audio_height() { + is(audio.clientHeight, 40, "checking height of audio element"); + }); + + add_task(async function check_controlbar_width() { + const originalControlBarWidth = controlBar.clientWidth; + + isnot(originalControlBarWidth, 400, "the default audio width is not 400px"); + + audio.style.width = "400px"; + audio.offsetWidth; // force reflow + + isnot(controlBar.clientWidth, originalControlBarWidth, "new width should differ from the origianl width"); + is(controlBar.clientWidth, 400, "controlbar's width should grow with audio width"); + }); + + add_task(function check_audio_height_construction_sync() { + let el = new Audio(); + el.src = "audio.wav"; + el.controls = true; + document.body.appendChild(el); + is(el.clientHeight, 40, "Height of audio element with controls"); + document.body.removeChild(el); + }); + + add_task(function check_audio_height_add_control_sync() { + let el = new Audio(); + el.src = "audio.wav"; + document.body.appendChild(el); + is(el.clientHeight, 0, "Height of audio element without controls"); + el.controls = true; + is(el.clientHeight, 40, "Height of audio element with controls"); + document.body.removeChild(el); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html b/toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html new file mode 100644 index 0000000000..6963c3da1e --- /dev/null +++ b/toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Audio controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls preload="auto"></audio> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + const audio = document.getElementById("audio"); + const controlBar = getElementWithinVideo(audio, "controlBar"); + + add_setup(async function setup() { + await new Promise(resolve => { + audio.addEventListener("loadedmetadata", resolve, {once: true}); + audio.src = "audio.wav"; + }); + }); + + add_task(async function test_double_click_does_not_fullscreen() { + SimpleTest.requestCompleteLog(); + SimpleTest.requestFlakyTimeout("Waiting to ensure that fullscreen event does not fire"); + const { x, y } = audio.getBoundingClientRect(); + const endedPromise = new Promise(resolve => { + audio.addEventListener("ended", () => { + info('Audio ended event fired!'); + resolve(); + }, { once: true }); + setTimeout( () => { + info('Audio ran out of time before ended event fired!'); + resolve(); + }, audio.duration * 1000); + }); + let noFullscreenEvent = true; + document.addEventListener("mozfullscreenchange", () => { + noFullscreenEvent = false; + }, { once: true }); + info("Simulate double click on media player."); + synthesizeMouse(audio, x, y, { clickCount: 2 }); + info("Waiting for video to end..."); + await endedPromise; + // By this point, if the double click was going to trigger fullscreen then + // it should have happened by now. + ok( + noFullscreenEvent, + "Double clicking should not trigger fullscreen event" + ); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_bug1654500.html b/toolkit/content/tests/widgets/test_bug1654500.html new file mode 100644 index 0000000000..9bb05ba9c8 --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug1654500.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Clear disabled/readonly datetime inputs</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<p>Disabled inputs should be able to be cleared just as they can be set with real values</p> +<input type="date" id="date1" value="2020-08-11" disabled> +<input type="date" id="date2" value="2020-08-11" readonly> +<input type="time" id="time1" value="07:01" disabled> +<input type="time" id="time2" value="07:01" readonly> +<script> +/* global date1, date2, time1, time2 */ +function querySelectorAllShadow(parent, selector) { + const shadowRoot = SpecialPowers.wrap(parent).openOrClosedShadowRoot; + return shadowRoot.querySelectorAll(selector); +} +date1.value = date2.value = ""; +time1.value = time2.value = ""; +for (const date of [date1, date2]) { + const fields = [...querySelectorAllShadow(date, ".datetime-edit-field")]; + is(fields.length, 3, "Three numeric fields are expected"); + for (const field of fields) { + is(field.getAttribute("value"), "", "All fields should be cleared"); + } +} +for (const time of [time1, time2]) { + const fields = [...querySelectorAllShadow(time, ".datetime-edit-field")]; + ok(fields.length >= 2, "At least two numeric fields are expected"); + for (const field of fields) { + is(field.getAttribute("value"), "", "All fields should be cleared"); + } +} +</script> diff --git a/toolkit/content/tests/widgets/test_bug898940.html b/toolkit/content/tests/widgets/test_bug898940.html new file mode 100644 index 0000000000..f2349c9a4c --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug898940.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an audio element that's already playing when controls are attached displays the controls</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls src="audio.ogg"></audio> +</div> + +<pre id="test"> +<script class="testbody"> + var audio = document.getElementById("audio"); + audio.play(); + audio.ontimeupdate = function doTest() { + ok(audio.getBoundingClientRect().height > 0, + "checking audio element height is greater than zero"); + audio.ontimeupdate = null; + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml new file mode 100644 index 0000000000..88511001b7 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Context menugroup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="context"> + <menugroup> + <menuitem id="a"/> + <menuitem id="b"/> + </menugroup> + <menuitem id="c" label="c"/> + <menugroup/> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="context" style="width: 20px; height: 20px"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gMenuPopup = $("context"); +ok(gMenuPopup, "Got the reference to the context menu"); + +var popupTests = [ +{ + testname: "one-down-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popupshowing context", "popupshown context", "DOMMenuItemActive a" ], + test() { + synthesizeMouse($("popuparea"), 4, 4, {}); + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "a", testname); + } +}, +{ + testname: "two-down-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive a", "DOMMenuItemActive b" ], + test: () => synthesizeKey("KEY_ArrowDown"), + result(testname) { + checkActive(gMenuPopup, "b", testname); + } +}, +{ + testname: "three-down-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive b", "DOMMenuItemActive c" ], + test: () => synthesizeKey("KEY_ArrowDown"), + result(testname) { + checkActive(gMenuPopup, "c", testname); + } +}, +{ + testname: "three-down-keys-one-up-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive c", "DOMMenuItemActive b" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result (testname) { + checkActive(gMenuPopup, "b", testname); + } +}, +{ + testname: "three-down-keys-two-up-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive b", "DOMMenuItemActive a" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result(testname) { + checkActive(gMenuPopup, "a", testname); + } +}, +{ + testname: "three-down-keys-three-up-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive a", "DOMMenuItemActive c" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result(testname) { + checkActive(gMenuPopup, "c", testname); + } +}, +]; + +SimpleTest.waitForFocus(function runTest() { + startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml b/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml new file mode 100644 index 0000000000..883e8e4d98 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Nested Context Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="outercontext"> + <menuitem label="Context One"/> + <menu id="outercontextmenu" label="Sub"> + <menupopup id="innercontext"> + <menuitem id="innercontextmenu" label="Sub Context One"/> + </menupopup> + </menu> +</menupopup> + +<menupopup id="outermain"> + <menuitem label="One"/> + <menu id="outermenu" label="Sub"> + <menupopup id="innermain"> + <menuitem id="innermenu" label="Sub One" context="outercontext"/> + </menupopup> + </menu> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="outermain" style="width: 20px; height: 20px"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var popupTests = [ +{ + testname: "open outer popup", + events: [ "popupshowing outermain", "popupshown outermain" ], + test: () => synthesizeMouse($("popuparea"), 4, 4, {}), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname); + } +}, +{ + testname: "open inner popup", + events: [ "DOMMenuItemActive outermenu", "popupshowing innermain", "popupshown innermain" ], + test () { + synthesizeMouse($("outermenu"), 4, 4, { type: "mousemove" }); + synthesizeMouse($("outermenu"), 2, 2, { type: "mousemove" }); + }, + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + } +}, +{ + testname: "open outer context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popupshowing outercontext", "popupshown outercontext" ], + test: () => synthesizeMouse($("innermenu"), 4, 4, { type: "contextmenu", button: 2 }), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + } +}, +{ + testname: "open inner context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemActive outercontextmenu", "popupshowing innercontext", "popupshown innercontext" ], + test () { + synthesizeMouse($("outercontextmenu"), 4, 4, { type: "mousemove" }); + setTimeout(function() { + synthesizeMouse($("outercontextmenu"), 2, 2, { type: "mousemove" }); + }, 1000); + }, + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + is($("innercontext").triggerNode, $("innermenu"), testname + " inner context"); + } +}, +{ + testname: "close context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popuphiding innercontext", "popuphidden innercontext", + "popuphiding outercontext", "popuphidden outercontext", + "DOMMenuInactive innercontext", + "DOMMenuItemInactive outercontextmenu", + "DOMMenuInactive outercontext" ], + test: () => $("outercontext").hidePopup(), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + } +}, +{ + testname: "hide menus", + events: [ "popuphiding innermain", "popuphidden innermain", + "popuphiding outermain", "popuphidden outermain", + "DOMMenuInactive innermain", + "DOMMenuItemInactive outermenu", + "DOMMenuInactive outermain" ], + + test: () => $("outermain").hidePopup(), + result (testname) { + is($("outermain").triggerNode, null, testname + " outer"); + is($("innermain").triggerNode, null, testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + } +} +]; + +SimpleTest.waitForFocus(function runTest() { + return startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_editor_currentURI.xhtml b/toolkit/content/tests/widgets/test_editor_currentURI.xhtml new file mode 100644 index 0000000000..bbb1f33623 --- /dev/null +++ b/toolkit/content/tests/widgets/test_editor_currentURI.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Editor currentURI Tests" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="html" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var editor = document.getElementById("editor"); + // Check that currentURI is a property of editor. + var result = "currentURI" in editor; + is(result, true, "currentURI is a property of editor"); + is(editor.currentURI.spec, "about:blank", "currentURI.spec is about:blank"); + SimpleTest.finish(); + } +]]> +</script> +</window> diff --git a/toolkit/content/tests/widgets/test_image_recognition.html b/toolkit/content/tests/widgets/test_image_recognition.html new file mode 100644 index 0000000000..d4dfc6216e --- /dev/null +++ b/toolkit/content/tests/widgets/test_image_recognition.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Image recognition test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <img src="image.png" /> +</div> + +<pre id="test"> +<script class="testbody"> + const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + + function pushPref(preferenceName, value) { + return new Promise(resolve => { + const options = {"set": [[preferenceName, value]]}; + SpecialPowers.pushPrefEnv(options, resolve); + }); + } + + add_task(async () => { + // Performing text recognition in CI can take some time, and test verify runs have + // timed out. + SimpleTest.requestLongerTimeout(2); + + await pushPref("dom.text-recognition.shadow-dom-enabled", true); + const img = document.querySelector("#content img"); + + info("Recognizing the image text"); + const result = await SpecialPowers.wrap(img).recognizeCurrentImageText(); + is(result.length, 2, "Two words were found."); + const mozilla = result.find(r => r.string === "Mozilla"); + const firefox = result.find(r => r.string === "Firefox"); + + ok(mozilla, "The word Mozilla was found."); + ok(firefox, "The word Firefox was found."); + + ok(mozilla.quad.p1.x < firefox.quad.p2.x, "The Mozilla text is left of Firefox"); + ok(mozilla.quad.p1.y > firefox.quad.p2.y, "The Mozilla text is above Firefox"); + + const spans = await TestUtils.waitForCondition( + () => shadowRootQuerySelectorAll(img, "span"), + "Attempting to get image recognition spans." + ); + + const mozillaSpan = [...spans].find(s => s.innerText === "Mozilla"); + const firefoxSpan = [...spans].find(s => s.innerText === "Firefox"); + + ok(mozillaSpan, "The word Mozilla span was found."); + ok(firefoxSpan, "The word Firefox span was found."); + + ok(mozillaSpan.style.transform.startsWith("matrix3d("), "A matrix transform was applied"); + ok(firefoxSpan.style.transform.startsWith("matrix3d("), "A matrix transform was applied"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_image_recognition_unsupported.html b/toolkit/content/tests/widgets/test_image_recognition_unsupported.html new file mode 100644 index 0000000000..f67ea8eb00 --- /dev/null +++ b/toolkit/content/tests/widgets/test_image_recognition_unsupported.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Image recognition unsupported</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <img src="image.png" /> +</div> + +<pre id="test"> +<script class="testbody"> + /** + * This test is for platforms that do not support text recognition. + */ + add_task(async () => { + const img = document.querySelector("#content img"); + + info("Recognizing the current image text is not supported on this platform."); + try { + await SpecialPowers.wrap(img).recognizeCurrentImageText(); + ok(false, "Recognizing the text should not be supported."); + } catch (error) { + ok(error, "Expected unsupported message: " + error.message); + } + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_image_recognition_zh.html b/toolkit/content/tests/widgets/test_image_recognition_zh.html new file mode 100644 index 0000000000..ba8f3c94e1 --- /dev/null +++ b/toolkit/content/tests/widgets/test_image_recognition_zh.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Image recognition test for Chinese</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <img src="image-zh.png" /> +</div> + +<pre id="test"> +<script class="testbody"> + // Performing text recognition in CI can take some time, and test verify runs have + // timed out. + SimpleTest.requestLongerTimeout(2); + + /** + * This test exercises the code path where the image recognition APIs detect the + * document language and use it to choose the language. + */ + add_task(async () => { + const img = document.querySelector("#content img"); + + info("Recognizing the image text, but not as Chinese"); + { + const result = await SpecialPowers.wrap(img).recognizeCurrentImageText(); + for (const { string } of result) { + isnot(string, "火狐", 'The results are (as expected) incorrect, as Chinese was not set as the language.'); + } + } + + info("Setting the document to Chinese."); + document.documentElement.setAttribute("lang", "zh-Hans-CN"); + + info("Recognizing the image text"); + { + const result = await SpecialPowers.wrap(img).recognizeCurrentImageText(); + is(result.length, 1, "One word was found."); + is(result[0].string, "火狐", "The Chinese characters for Firefox are found."); + } + + document.documentElement.setAttribute("lang", "en-US"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_label_checkbox.xhtml b/toolkit/content/tests/widgets/test_label_checkbox.xhtml new file mode 100644 index 0000000000..da9d588409 --- /dev/null +++ b/toolkit/content/tests/widgets/test_label_checkbox.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Label Checkbox Tests" + onload="onLoad()" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Label Checkbox Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + runTest(); +} + +function runTest() +{ + window.open("window_label_checkbox.xhtml", "_blank", "width=600,height=600"); +} + +onmessage = function onMessage() +{ + SimpleTest.finish(); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_menubar.xhtml b/toolkit/content/tests/widgets/test_menubar.xhtml new file mode 100644 index 0000000000..16fae52b03 --- /dev/null +++ b/toolkit/content/tests/widgets/test_menubar.xhtml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menubar Popup Tests" + onload="setTimeout(onLoad, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Menubar Popup Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + window.open("window_menubar.xhtml", "_blank", "width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_mousecapture_area.html b/toolkit/content/tests/widgets/test_mousecapture_area.html new file mode 100644 index 0000000000..7217d971eb --- /dev/null +++ b/toolkit/content/tests/widgets/test_mousecapture_area.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Mouse capture on area elements tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <!-- The border="0" on the images is needed so that when we use + synthesizeMouse we don't accidentally target the border of the image and + miss the area because synthesizeMouse gets the rect of the primary frame + of the target (the area), which is the image due to bug 135040, which + includes the border, but the events targetted at the border aren't + targeted at the area. --> + + <!-- 20x20 of red --> + <img id="image" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#Map"/> + + <map name="Map"> + <!-- area over the whole image --> + <area id="area" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <!-- 20x20 of red --> + <img id="img1" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <!-- 20x20 of red --> + <img id="img2" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <map name="sharedMap"> + <!-- area over the whole image --> + <area id="sharedarea" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <div id="otherelement" style="width: 100px; height: 100px;"></div> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function runTests() { + // XXX We send a useless click to each image to force it to setup its image + // map, because flushing layout won't do it. Hopefully bug 135040 will make + // this not suck. + synthesizeMouse($("image"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("image"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img1"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img1"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img2"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img2"), 5, 5, { type: "mouseup" }); + + + // test that setCapture works on an area element (bug 517737) + var area = document.getElementById("area"); + synthesizeMouse(area, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + area, "mousemove", "setCapture works on areas"); + synthesizeMouse(area, 5, 5, { type: "mouseup" }); + + // test that setCapture works on an area element when it is part of an image + // map that is used by two images + + var img1 = document.getElementById("img1"); + var sharedarea = document.getElementById("sharedarea"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img1, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img1, 5, 5, { type: "mouseup" }); + + var img2 = document.getElementById("img2"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img2, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img2, 5, 5, { type: "mouseup" }); + + // Bug 862673 - nuke all content so assertions in this test are attributed to + // this test rather than the one which happens to follow. + var content = document.getElementById("content"); + content.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_button_group.html b/toolkit/content/tests/widgets/test_moz_button_group.html new file mode 100644 index 0000000000..92fd7776fa --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_button_group.html @@ -0,0 +1,235 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>moz-button-group tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better + solution for token variables for the new widgets --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-button-group.mjs"></script> + </head> + <body> + <p id="display"></p> + <div id="content"> + <button id="before-button">Before</button> + <div id="render"></div> + <button id="after-button">After</button> + </div> + <!-- This is here to ensure the stylesheet is loaded. It gets removed in setup. --> + <moz-button-group></moz-button-group> + <pre id="test"> + </pre> + + <script> + let html; + let render; + + let renderArea = document.getElementById("render"); + let beforeButton = document.getElementById("before-button"); + let afterButton = document.getElementById("after-button"); + + async function checkButtons(...buttons) { + const checkOrder = (a, b) => { + let firstBounds = a.getBoundingClientRect(); + let secondBounds = b.getBoundingClientRect(); + + ok(firstBounds.right < secondBounds.left, `First button comes first`); + let locationDiff = Math.abs(secondBounds.left - firstBounds.right - 8); + ok(locationDiff < 1, `Second button is 8px after first (${locationDiff}, ${firstBounds.right}, ${secondBounds.left})`); + }; + + ok(buttons.length >= 2, "There are at least 2 buttons to check"); + + // Verify tab order is correct. + beforeButton.focus(); + is(document.activeElement, beforeButton, "Before button is focused"); + + synthesizeKey("VK_TAB"); + is(document.activeElement, buttons[0], "Next button is focused"); + + for (let i = 1; i < buttons.length; i++) { + // Confirm button order in DOM + checkOrder(buttons[i - 1], buttons[i]); + + synthesizeKey("VK_TAB"); + is(document.activeElement, buttons[i], "Next button is focused"); + } + + synthesizeKey("VK_TAB"); + is(document.activeElement, afterButton, "After button is at the end in tab order"); + + // Verify light DOM order is correct, in case of manual tab management in JS. + let { parentElement } = buttons[0]; + let parentChildren = parentElement.children; + is(parentChildren.length, buttons.length, "Expected number of children"); + for (let i = 0; i < parentChildren.length; i++) { + is(parentChildren[i], buttons[i], `Button ${i} is in correct light DOM spot`); + } + } + + + add_setup(async function setup() { + ({ html, render} = await import("chrome://global/content/vendor/lit.all.mjs")); + document.querySelector("moz-button-group").remove(); + }); + + add_task(async function testButtonOrderingSlot() { + render( + html` + <moz-button-group> + <button slot="primary" id="primary-button">Primary</button> + <button id="secondary-button">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let primaryButton = document.getElementById("primary-button"); + let secondaryButton = document.getElementById("secondary-button"); + + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + await checkButtons(primaryButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, primaryButton); + }); + + add_task(async function testPrimaryButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button class="primary">Primary</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let primaryButton = buttonGroup.querySelector(".primary"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(primaryButton.slot, "primary", "primary button was auto-slotted") + await checkButtons(primaryButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, primaryButton); + }); + + add_task(async function testSubmitButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button type="submit">Submit</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let submitButton = buttonGroup.querySelector("[type=submit]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(submitButton.slot, "primary", "submit button was auto-slotted") + await checkButtons(submitButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, submitButton); + }); + + add_task(async function testAutofocusButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button autofocus>First</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let autofocusButton = buttonGroup.querySelector("[autofocus]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(autofocusButton.slot, "primary", "autofocus button was auto-slotted") + await checkButtons(autofocusButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, autofocusButton); + }); + + add_task(async function testDefaultButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button default>First</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let defaultButton = buttonGroup.querySelector("[default]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(defaultButton.slot, "primary", "default button was auto-slotted") + await checkButtons(defaultButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, defaultButton); + }); + + add_task(async function testInitialButtonLightDomReordering() { + const renderPlatform = platform => render( + html` + <moz-button-group .platform=${platform}> + <button class="primary">First</button> + <button class="secondary">Secondary</button> + <button default>Default</button> + </moz-button-group> + `, + renderArea + ); + + renderPlatform("win"); + let buttonGroup = document.querySelector("moz-button-group"); + await buttonGroup.updateComplete; + let primaryButton = buttonGroup.querySelector(".primary"); + let defaultButton = buttonGroup.querySelector("[default]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + + is(primaryButton.slot, "primary", "primary button was auto-slotted"); + is(defaultButton.slot, "primary", "default button was auto-slotted"); + await checkButtons(primaryButton, defaultButton, secondaryButton); + + renderPlatform("macosx"); + buttonGroup = document.querySelector("moz-button-group"); + await buttonGroup.updateComplete; + primaryButton = buttonGroup.querySelector(".primary"); + defaultButton = buttonGroup.querySelector("[default]"); + secondaryButton = buttonGroup.querySelector(".secondary"); + + is(primaryButton.slot, "primary", "primary button was auto-slotted"); + is(defaultButton.slot, "primary", "default button was auto-slotted"); + await checkButtons(secondaryButton, primaryButton, defaultButton); + }); + </script> + </body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_card.html b/toolkit/content/tests/widgets/test_moz_card.html new file mode 100644 index 0000000000..ef4e67d0fa --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_card.html @@ -0,0 +1,158 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>moz-card tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script type="module" src="chrome://global/content/elements/moz-card.mjs"></script> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> +</head> + +<body> + <p id="display"></p> + <div id="content"> + <moz-card id="default-card" data-l10n-id="test-id-1" data-l10n-attrs="heading"> + <div>TEST</div> + </moz-card> + <hr /> + + <moz-card id="accordion-card" data-l10n-id="test-id-2" data-l10n-attrs="heading" heading="accordion heading" + type="accordion"> + <div>accordion test content</div> + </moz-card> + <hr /> + + </div> + <pre id="test"></pre> + <script> + let generatedSlotText = "generated slotted element"; + let testHeading = "test heading"; + + function assertBasicProperties(card, expectedValues) { + info(`Testing card with ID: ${card.id}`); + ok(card, "The card element should exist"); + is(card.localName, "moz-card", "The card should have the correct tag"); + let l10nId = card.getAttribute("data-l10n-id"); + let l10nAttrs = card.getAttribute("data-l10n-attrs"); + if (expectedValues["data-l10n-id"]) { + is(l10nId, expectedValues["data-l10n-id"], "l10n id should be unchanged"); + } + if (expectedValues["data-l10n-attrs"]) { + is(l10nAttrs, expectedValues["data-l10n-attrs"], "l10n attrs should be unchanged"); + } + let cardContent = card.firstElementChild; + ok(cardContent, "The content should exist"); + is(cardContent.textContent, expectedValues.contentText, "The content should be unchanged"); + is(card.contentSlotEl.id, "content", "The content container should have the correct ID"); + if (card.type != "accordion") { + is(card.contentSlotEl.getAttribute("aria-describedby"), "content", "The content container should be described by the 'content' slot"); + } + + if (expectedValues.headingText) { + ok(card.headingEl, "Heading should exist"); + is(card.headingEl.textContent, expectedValues.headingText, "Heading should match the 'heading' attribute value"); + } + + } + + function assertAccordionCardProperties(card, expectedValues) { + ok(card.detailsEl, "The details element should exist"); + ok(card.detailsEl.querySelector("summary"), "There should be a summary element within the details element"); + ok(card.detailsEl.querySelector("summary").querySelector(".chevron-icon"), "There should be a chevron icon div within the summary element"); + } + + async function generateCard(values) { + let card = document.createElement("moz-card"); + for (let [key, value] of Object.entries(values)) { + card.setAttribute(key, value); + } + let div = document.createElement("div"); + div.innerText = generatedSlotText; + card.appendChild(div); + document.body.appendChild(card); + await card.updateComplete; + document.body.appendChild(document.createElement("hr")); + return card; + } + + add_task(async function testDefaultCard() { + assertBasicProperties(document.getElementById("default-card"), + { + "data-l10n-id": "test-id-1", + "data-l10n-attrs": "heading", + contentText: "TEST" + } + ); + + let defaultCard = await generateCard( + { + "data-l10n-id": "generated-id-1", + "data-l10n-attrs": "heading", + heading: testHeading, + id: "generated-default-card" + } + ); + + assertBasicProperties(defaultCard, + { + "data-l10n-id": "generated-id-1", + "data-l10n-attrs": "heading", + contentText: generatedSlotText, + heading: testHeading + } + ); + }); + + add_task(async function testAccordionCard() { + assertBasicProperties(document.getElementById("accordion-card"), + { + "data-l10n-id": "test-id-2", + "data-l10n-attrs": "heading", + contentText: "accordion test content", + headingText: "accordion heading", + } + ); + assertAccordionCardProperties(document.getElementById("accordion-card"), + { + "data-l10n-id": "test-id-2", + "data-l10n-attrs": "heading", + contentText: "accordion test content", + headingText: "accordion heading", + } + ); + + let accordionCard = await generateCard( + { + type: "accordion", + id: "generated-accordion-card", + "data-l10n-id": "generated-id-2", + "data-l10n-attrs": "heading", + heading: testHeading + } + ); + + assertBasicProperties(accordionCard, + { + "data-l10n-id": "generated-id-2", + "data-l10n-attrs": "heading", + headingText: testHeading, + contentText: generatedSlotText, + } + ); + assertAccordionCardProperties(accordionCard, + { + "data-l10n-id": "generated-id-2", + "data-l10n-attrs": "heading", + headingText: testHeading, + contentText: generatedSlotText, + } + ); + }); + + </script> +</body> + +</html> diff --git a/toolkit/content/tests/widgets/test_moz_five_star.html b/toolkit/content/tests/widgets/test_moz_five_star.html new file mode 100644 index 0000000000..593028f2c9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_five_star.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozFiveStar Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="module" src="chrome://global/content/elements/moz-five-star.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="max-width: fit-content"> + <moz-five-star label="Label" rating="2.5"></moz-five-star> +</div> +<pre id="test"> + <script class="testbody" type="application/javascript"> + const { BrowserTestUtils } = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); + add_task(async function testMozFiveStar() { + const mozFiveStar = document.querySelector("moz-five-star"); + ok(mozFiveStar, "moz-five-star is rendered"); + + const stars = mozFiveStar.starEls; + ok(stars, "moz-five-star has stars"); + is(stars.length, 5, "moz-five-star stars count is 5"); + + const rating = mozFiveStar.rating; + ok(rating, "moz-five-star has a rating"); + is(rating, 2.5, "moz-five-star rating is 2.5"); + }); + + add_task(async function testMozFiveStarsDisplay() { + const mozFiveStar = document.querySelector("moz-five-star"); + ok(mozFiveStar, "moz-five-star is rendered"); + + async function testRating(rating, ratingRounded, expectation) { + mozFiveStar.rating = rating; + await mozFiveStar.updateComplete; + if (mozFiveStar.ownerDocument.hasPendingL10nMutations) { + await BrowserTestUtils.waitForEvent( + mozFiveStar.ownerDocument, + "L10nMutationsFinished" + ); + } + let starsString = Array.from(mozFiveStar.starEls) + .map(star => star.getAttribute("fill")) + .join(","); + is(starsString, expectation, `Rendering of rating ${rating}`); + + is( + mozFiveStar.starsWrapperEl.title, + `Rated ${ratingRounded} out of 5`, + "Rendered title must contain at most one fractional digit" + ); + + let isImage = mozFiveStar.starsWrapperEl.getAttribute("role") == "img" + || mozFiveStar.starsWrapperEl.getAttribute("role") == "image"; + + ok( + isImage, + "Rating element is an image for the title to be announced" + ); + } + + await testRating(0.0, "0", "empty,empty,empty,empty,empty"); + await testRating(0.249, "0.2", "empty,empty,empty,empty,empty"); + await testRating(0.25, "0.3", "half,empty,empty,empty,empty"); + await testRating(0.749, "0.7", "half,empty,empty,empty,empty"); + await testRating(0.99, "1", "full,empty,empty,empty,empty"); + await testRating(1.0, "1", "full,empty,empty,empty,empty"); + await testRating(2, "2", "full,full,empty,empty,empty"); + await testRating(3.0, "3", "full,full,full,empty,empty"); + await testRating(4.001, "4", "full,full,full,full,empty"); + await testRating(4.249, "4.2", "full,full,full,full,empty"); + await testRating(4.25, "4.3", "full,full,full,full,half"); + await testRating(4.749, "4.7", "full,full,full,full,half"); + await testRating(4.89, "4.9", "full,full,full,full,full"); + await testRating(5.0, "5", "full,full,full,full,full"); + }); + </script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_label.html b/toolkit/content/tests/widgets/test_moz_label.html new file mode 100644 index 0000000000..80a9600930 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_label.html @@ -0,0 +1,142 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozLabel tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-label.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <label is="moz-label" for="checkbox" accesskey="c">For the checkbox:</label> + <input type="checkbox" id="checkbox" /> + + <label is="moz-label" accesskey="n"> + For the nested checkbox: + <input type="checkbox" /> + </label> + + <label is="moz-label" for="radio" accesskey="r">For the radio:</label> + <input type="radio" id="radio" /> + + <label is="moz-label" accesskey="F"> + For the nested radio: + <input type="radio" /> + </label> + + <label is="moz-label" for="button" accesskey="b">For the button:</label> + <button id="button">Click me</button> + + <label is="moz-label" accesskey="u"> + For the nested button: + <button>Click me too</button> + </label> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + let labels = document.querySelectorAll("label[is='moz-label']"); + let isMac = navigator.platform.includes("Mac"); + + function performAccessKey(key) { + synthesizeKey( + key, + navigator.platform.includes("Mac") + ? { altKey: true, ctrlKey: true } + : { altKey: true, shiftKey: true } + ); + } + + // Accesskey underlining is disabled by default on Mac. + // Reload the window and wait for load to ensure pref is applied. + add_setup(async function setup() { + if (isMac && !SpecialPowers.getIntPref("ui.key.menuAccessKey")) { + await SpecialPowers.pushPrefEnv( + { set: [["ui.key.menuAccessKey", 1]] }, + async () => { + window.location.reload(); + await new Promise(resolve => { + addEventListener("load", resolve, { once: true }); + }); + } + ); + } + }); + + add_task(async function testAccesskeyUnderlined() { + labels.forEach(label => { + let accessKey = label.getAttribute("accesskey"); + let wrapper = label.querySelector(".accesskey"); + is(wrapper.textContent, accessKey, "The accesskey character is wrapped.") + + let textDecoration = getComputedStyle(wrapper)["text-decoration"] + ok(textDecoration.includes("underline"), "The accesskey character is underlined.") + }) + }); + + add_task(async function testAccesskeyFocus() { + labels.forEach(label => { + let accessKey = label.getAttribute("accesskey"); + // Find the labelled element via the "for" attr if there's an ID + // association, or select the lastElementChild for nested elements + let element = document.getElementById(label.getAttribute("for")) || label.lastElementChild; + + isnot(document.activeElement, element, "Focus is not on the associated element."); + + performAccessKey(accessKey); + + is(document.activeElement, element, "Focus moved to the associated element.") + }) + }); + + add_task(async function testAccesskeyChange() { + let label = labels[0]; + let nextAccesskey = "x"; + let originalAccesskey = label.getAttribute("accesskey"); + let getWrapper = () => label.querySelector(".accesskey"); + is(getWrapper().textContent, originalAccesskey, "Original accesskey character is wrapped.") + + label.setAttribute("accesskey", nextAccesskey); + is(getWrapper().textContent, nextAccesskey, "New accesskey character is wrapped.") + + let elementId = label.getAttribute("for"); + let focusedEl = document.getElementById(elementId); + + performAccessKey(originalAccesskey); + isnot(document.activeElement.id, focusedEl.id, "Focus has not moved to the associated element.") + + performAccessKey(nextAccesskey); + is(document.activeElement.id, focusedEl.id, "Focus moved to the associated element.") + }); + + add_task(async function testAccesskeyAppended() { + let label = labels[0]; + let originalText = label.textContent; + let accesskey = "z"; // Letter not included in the label text. + label.setAttribute("accesskey", accesskey); + + let expectedText = `${originalText} (Z):`; + is(label.textContent, expectedText, "Access key is appended when not included in label text.") + }); + + add_task(async function testLabelClick() { + let label = labels[0]; + let input = document.getElementById(label.getAttribute("for")); + is(input.checked, false, "The associated input is not checked.") + + // Input state changes on label click. + synthesizeMouseAtCenter(label, {}); + ok(input.checked, "The associated input is checked.") + + // Input state doesn't change on label click when input is disabled. + input.disabled = true; + synthesizeMouseAtCenter(label, {}); + ok(input.checked, "The associated input is still checked.") + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_message_bar.html b/toolkit/content/tests/widgets/test_moz_message_bar.html new file mode 100644 index 0000000000..7ee6825ef3 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_message_bar.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozMessageBar tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="module" src="chrome://global/content/elements/moz-message-bar.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <moz-message-bar id="infoMessage" heading="Heading" message="Test message"></moz-message-bar> + <moz-message-bar id="infoMessage2" dismissable message="Test message"></moz-message-bar> + <moz-message-bar id="warningMessage" type="warning" message="Test message"></moz-message-bar> + <moz-message-bar id="successMessage" type="success" message="Test message"></moz-message-bar> + <moz-message-bar id="errorMessage" type="error" message="Test message"></moz-message-bar> +</div> +<pre id="test"> + <script class="testbody" type="application/javascript"> + add_task(async function test_component_declaration() { + const mozMessageBar = document.querySelector("#infoMessage"); + ok(mozMessageBar, "moz-message-bar component is rendered."); + + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("info-filled.svg"), "Info icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Info", "Alternate text for the info icon is present."); + + const heading = mozMessageBar.shadowRoot.querySelector(".heading"); + is(heading.textContent.trim(), "Heading", "Heading is showing up."); + const message = mozMessageBar.shadowRoot.querySelector(".message"); + is(message.textContent.trim(), "Test message", "Message is showing up."); + }); + + add_task(async function test_heading_display() { + const mozMessageBar = document.querySelector("#infoMessage2"); + let heading = mozMessageBar.shadowRoot.querySelector(".heading"); + ok(!heading, "The heading element isn't displayed if it hasn't been initialized."); + + mozMessageBar.heading = "Now there's a heading"; + await mozMessageBar.updateComplete; + heading = mozMessageBar.renderRoot.querySelector(".heading"); + is(heading.textContent.trim(), "Now there's a heading", "New heading element is displayed."); + }); + + add_task(async function test_close_button() { + const notDismissableComponent = document.querySelector("#infoMessage"); + let closeButton = notDismissableComponent.closeButtonEl; + ok(!closeButton, "Close button doesn't show when the message bar isn't dismissable."); + + let dismissableComponent = document.querySelector("#infoMessage2"); + closeButton = dismissableComponent.closeButtonEl; + ok(closeButton, "Close button is shown when the message bar is dismissable."); + + closeButton.click(); + dismissableComponent = document.querySelector("#infoMessage2"); + is(dismissableComponent, null, "Clicking on the close button removes the message bar element."); + }); + + add_task(async function test_warning_message_component() { + const mozMessageBar = document.querySelector("#warningMessage"); + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("warning.svg"), "Warning icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Warning", "Alternate text for the warning icon is present."); + }); + + add_task(async function test_success_message_component() { + const mozMessageBar = document.querySelector("#successMessage"); + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("check-filled.svg"), "Success icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Success", "Alternate text for the success icon is present."); + }); + + add_task(async function test_error_message_component() { + const mozMessageBar = document.querySelector("#errorMessage"); + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("error.svg"), "Error icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Error", "Alternate text for the error icon is present."); + }); + </script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_support_link.html b/toolkit/content/tests/widgets/test_moz_support_link.html new file mode 100644 index 0000000000..3f523c04a6 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_support_link.html @@ -0,0 +1,151 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozSupportLink tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better + solution for token variables for the new widgets --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-support-link.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <a + id="testElement" + is="moz-support-link" + data-l10n-id="test" + support-page="support-test" + >testElement</a> + + <a + id="testElement2" + is="moz-support-link" + data-l10n-id="test2" + support-page="support-test" + utm-content="utmcontent-test" + >testElement2</a> + + <a + id="testElement3" + is="moz-support-link" + data-l10n-name="name" + support-page="support-test" + ></a> + + <a + id="testElement4" + is="moz-support-link" + data-l10n-id="test4" + data-l10n-name="name" + support-page="support-test" +></a> + + <a + id="testElement5" + is="moz-support-link" + support-page="support-test" + ></a> + +</div> +<pre id="test"></pre> +<script> + function assertBasicProperties(link, { l10nId, l10nName, supportPage, supportUrl, utmContent }) { + is(link.localName, "a", "Check that it is an anchor"); + is(link.constructor.name, "MozSupportLink", "Element should be a 'moz-support-link'"); + if (l10nId) { + is(link.getAttribute("data-l10n-id"), l10nId, "Check data-l10n-id is correct"); + } + if (l10nName) { + is(link.getAttribute("data-l10n-name"), l10nName, "Check data-l10n-name is correct"); + } + if (supportPage && utmContent) { + is(link.getAttribute("utm-content"), utmContent, "Check utm-correct is correct"); + is(link.getAttribute("support-page"), supportPage, "Check support-page is correct"); + is(link.target, "_blank", "support link should open a new window"); + let expectedHref = `${supportUrl}${supportPage}?utm_source=firefox-browser&utm_medium=firefox-browser&utm_content=${utmContent}`; + is(link.href, expectedHref, "href should be generated correctly when using utm-content"); + } else if (supportPage) { + is(link.getAttribute("support-page"), supportPage, "Check support-page is correct"); + is(link.target, "_blank", "support link should open a new window"); + is(link.href, `${supportUrl}${supportPage}`, `href should be generated SUPPORT_URL plus ${supportPage}`); + } + } + add_task(async function test_component_declaration() { + let mozSupportLink = customElements.get("moz-support-link"); + let supportUrl = mozSupportLink.SUPPORT_URL; + let supportPage = "support-test"; + + // Ensure all the semantics of the primary link are present + let supportLink = document.getElementById("testElement"); + assertBasicProperties(supportLink, {l10nId: "test", supportPage, supportUrl}); + + // Ensure AMO support link has the correct values + let supportLinkAMO = document.getElementById("testElement2"); + assertBasicProperties(supportLinkAMO, { + l10nId: "test2", + supportPage, + supportUrl, + utmContent:"utmcontent-test" + }); + + // Ensure data-l10n-name is not overwritten by the component + let supportLinkL10nName = document.getElementById("testElement3"); + assertBasicProperties(supportLinkL10nName, { + l10nId: null, + l10nName: "name", + supportPage, + supportUrl + }); + + // Ensure data-l10n-id and data-l10n-name are not overwritten by the component + let linkWithNameAndId = document.getElementById("testElement4"); + assertBasicProperties(linkWithNameAndId, { + l10nId: "test4", + l10nName: "name", + supportPage, + supportUrl + }); + + // Ensure moz-support-link without assigned data-l10n-id gets the default id + let defaultLink = document.getElementById("testElement5"); + assertBasicProperties(defaultLink, { + l10nId: "moz-support-link-text", + supportPage, + supportUrl + }); + }); + + add_task(async function test_creating_component() { + // Ensure created support link behaves as expected + let mozSupportLink = customElements.get("moz-support-link"); + let supportUrl = mozSupportLink.SUPPORT_URL; + let l10nId = "constructedElement"; + let content = document.getElementById("content"); + let utmSupportLink = document.createElement("a", { is: "moz-support-link" }); + utmSupportLink.id = l10nId; + utmSupportLink.innerText = l10nId; + document.l10n.setAttributes(utmSupportLink, l10nId); + content.appendChild(utmSupportLink); + assertBasicProperties(utmSupportLink, { supportUrl, l10nId }); + + // Set href via "support-page" after creating the element + utmSupportLink.setAttribute("support-page", "created-page"); + assertBasicProperties(utmSupportLink, { + supportUrl, + supportPage: "created-page" + }); + + // Set href via "utm-content" + utmSupportLink.setAttribute("utm-content", "created-content"); + assertBasicProperties(utmSupportLink, { + supportUrl, + supportPage: "created-page", + utmContent: "created-content" + }); + }); +</script> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_toggle.html b/toolkit/content/tests/widgets/test_moz_toggle.html new file mode 100644 index 0000000000..62f248c599 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_toggle.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozToggle tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better + solution for token variables for the new widgets --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-toggle.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="max-width: fit-content"> + <moz-toggle label="Label" description="Description" pressed="true"></moz-toggle> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + add_task(async function testMozToggleDisplay() { + const mozToggle = document.querySelector("moz-toggle"); + ok(mozToggle, "moz-toggle is rendered"); + + const label = mozToggle.labelEl; + ok(label, "moz-toggle contains a label"); + ok(label.textContent.includes("Label"), "The expected label text is shown"); + + const description = mozToggle.descriptionEl; + ok(description, "moz-toggle contains a description"); + ok(description.textContent.includes("Description"), "The expected description text is shown"); + + const button = mozToggle.buttonEl; + ok(button, "moz-toggle contains a button"); + is(button.getAttribute("aria-pressed"), "true", "The button is pressed"); + }); + + add_task(async function testMozToggleInteraction() { + const mozToggle = document.querySelector("moz-toggle"); + const button = mozToggle.buttonEl; + is(mozToggle.pressed, true, "moz-toggle is pressed initially"); + is(button.getAttribute("aria-pressed"), "true", "aria-pressed reflects the pressed state"); + + synthesizeMouseAtCenter(button, {}); + await mozToggle.updateComplete; + + is(mozToggle.pressed, false, "The toggle pressed state changes on click"); + is(button.getAttribute("aria-pressed"), "false", "aria-pressed reflects this change"); + + synthesizeMouseAtCenter(mozToggle.labelEl, {}); + await mozToggle.updateComplete; + + is(mozToggle.pressed, true, "The toggle pressed state changes on label click"); + is(button.getAttribute("aria-pressed"), "true", "aria-pressed reflects this change"); + + mozToggle.focus(); + synthesizeKey(" ", {}); + await mozToggle.updateComplete; + + is(mozToggle.pressed, false, "The toggle pressed state can be changed via space bar"); + is(button.getAttribute("aria-pressed"), "false", "aria-pressed reflects this change"); + }); + + add_task(async function testSupportsAccesskey() { + const mozToggle = document.querySelector("moz-toggle"); + let nextAccesskey = "l"; + mozToggle.setAttribute("accesskey", nextAccesskey); + + synthesizeKey( + nextAccesskey, + navigator.platform.includes("Mac") + ? { altKey: true, ctrlKey: true } + : { altKey: true, shiftKey: true } + ); + + is( + mozToggle.shadowRoot.activeElement, + mozToggle.buttonEl, + "Focus has moved to the toggle button." + ); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_nac_mutations.html b/toolkit/content/tests/widgets/test_nac_mutations.html new file mode 100644 index 0000000000..3e4896bec2 --- /dev/null +++ b/toolkit/content/tests/widgets/test_nac_mutations.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<title>UA Widget mutation observer test</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<video controls id="video"></video> +<div style="overflow: scroll; width: 100px; height: 100px" id="scroller"></div> +<script> +const video = document.getElementById("video"); +const scroller = document.getElementById("scroller"); + +async function test_mutations_internal(observedNode, elementToMutate, expectMutations) { + let resolveMutations; + let mutations = new Promise(r => { + resolveMutations = r; + }); + + let observer = new MutationObserver(function(m) { + ok(expectMutations, "Mutations should be expected"); + resolveMutations(m) + }); + + SpecialPowers.wrap(observer).observe(observedNode, { + subtree: true, + attributes: true, + chromeOnlyNodes: expectMutations, + }); + + elementToMutate.setAttribute("unlikely", `value-${expectMutations}`); + + if (expectMutations) { + await mutations; + } else { + await new Promise(r => SimpleTest.executeSoon(r)); + } + + observer.disconnect(); +} + +async function test_mutations(observedNode, elementToMutate) { + for (let chromeOnlyNodes of [true, false]) { + info(`Testing chromeOnlyNodes: ${chromeOnlyNodes}`); + await test_mutations_internal(observedNode, elementToMutate, chromeOnlyNodes); + } +} + +add_task(async function test_ua_mutations() { + let shadow = SpecialPowers.wrap(video).openOrClosedShadowRoot; + ok(!!shadow, "UA Widget ShadowRoot exists"); + + await test_mutations(shadow, shadow.querySelector("*")); +}); + +add_task(async function test_scrollbar_mutations_same_anon_tree() { + let scrollbar = SpecialPowers.wrap(window).InspectorUtils.getChildrenForNode(scroller, true, false)[0]; + is(scrollbar.tagName, "scrollbar", "should find a scrollbar"); + await test_mutations(scrollbar, scrollbar); +}); + +add_task(async function test_scrollbar_mutations_same_tree() { + let scrollbar = SpecialPowers.wrap(window).InspectorUtils.getChildrenForNode(scroller, true, false)[0]; + is(scrollbar.tagName, "scrollbar", "should find a scrollbar"); + await test_mutations(scroller, scrollbar); +}); +</script> diff --git a/toolkit/content/tests/widgets/test_panel_item_accesskey.html b/toolkit/content/tests/widgets/test_panel_item_accesskey.html new file mode 100644 index 0000000000..a35e94d456 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_item_accesskey.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel Item Accesskey Support</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <panel-list> + <panel-item accesskey="F">First item</panel-item> + <panel-item accesskey="S">Second item</panel-item> + <panel-item>Third item</panel-item> + </panel-list> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); + +add_task(async function testAccessKey() { + function assertAccessKeys(items, keys, { checkLabels = false } = {}) { + is(items.length, keys.length, "Got the same number of items and keys"); + for (let i = 0; i < items.length; i++) { + is(items[i].accessKey, keys[i], `Item ${i} has the right key`); + if (checkLabels) { + let label = items[i].shadowRoot.querySelector("label"); + is(label.accessKey, keys[i] || null, `Label ${i} has the right key`); + } + } + } + + let panelList = document.querySelector("panel-list"); + let panelItems = [...panelList.children]; + + info("Accesskeys should be removed when closed"); + assertAccessKeys(panelItems, ["", "", ""]); + + info("Accesskeys should be set when open"); + let panelShown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await panelShown; + assertAccessKeys(panelItems, ["F", "S", ""], { checkLabels: true }); + + info("Changing accesskeys should happen right away"); + panelItems[1].accessKey = "c"; + panelItems[2].accessKey = "T"; + assertAccessKeys(panelItems, ["F", "c", "T"], { checkLabels: true }); + + info("Accesskeys are removed again on hide"); + let panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + panelList.hide(); + await panelHidden; + assertAccessKeys(panelItems, ["", "", ""]); + + info("Accesskeys are set again on show"); + panelItems[0].removeAttribute("accesskey"); + panelShown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await panelShown; + assertAccessKeys(panelItems, ["", "c", "T"], { checkLabels: true }); + + info("Check that accesskeys can be used without the modifier when open"); + let secondClickCount = 0; + let thirdClickCount = 0; + panelItems[1].addEventListener("click", () => secondClickCount++); + panelItems[2].addEventListener("click", () => thirdClickCount++); + + // Make sure the focus is in the window. + panelItems[0].focus(); + + panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("c", {}); + await panelHidden; + + is(secondClickCount, 1, "The accesskey worked unmodified"); + is(thirdClickCount, 0, "The other listener wasn't fired"); + + synthesizeKey("c", {}); + synthesizeKey("t", {}); + + is(secondClickCount, 1, "The key doesn't trigger when closed"); + is(thirdClickCount, 0, "The key doesn't trigger when closed"); + + panelShown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await panelShown; + + panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("t", {}); + await panelHidden; + + is(secondClickCount, 1, "The other listener wasn't fired"); + is(thirdClickCount, 1, "The accesskey worked unmodified"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_accessibility.html b/toolkit/content/tests/widgets/test_panel_list_accessibility.html new file mode 100644 index 0000000000..c53b48e0a9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_accessibility.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel List Accessibility</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <button id="anchor-button">Open</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + </panel-list> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); +let anchorButton, panelList, panelItems; + + +add_setup(function setup() { + // Clear out the other elements so only our test content is on the page. + panelList = document.getElementById("panel-list"); + panelItems = [...panelList.children]; + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); +}); + +add_task(async function testItemFocusOnOpen() { + ok(document.activeElement, "There is an active element"); + ok(!document.activeElement.closest("panel-list"), "Focus isn't in the list"); + + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + + // Focus shouldn't enter the list on a click. + ok(!document.activeElement.closest("panel-list"), "Focus isn't in the list"); + + shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + anchorButton.focus(); + synthesizeKey(" ", {}); + await shown; + + // Focus enters list with keyboard interaction. + is(document.activeElement, panelItems[0], "The first item is focused"); + + hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + + is(document.activeElement, anchorButton, "The anchor is focused again on close"); +}); + +add_task(async function testAriaAttributes() { + is(panelList.getAttribute("role"), "menu", "The panel is a menu"); + + is(panelItems.length, 3, "There are 3 items"); + for (let item of panelItems) { + is(item.button.getAttribute("role"), "menuitem", "button is a menuitem"); + } +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_anchoring.html b/toolkit/content/tests/widgets/test_panel_list_anchoring.html new file mode 100644 index 0000000000..b1e11d3e4d --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_anchoring.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test Panel List Anchoring</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> + <button id="anchor-button">Open</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + </panel-list> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); + let anchorButton, panelList, panelItems; + + add_setup(function setup() { + panelList = document.getElementById("panel-list"); + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); + }); + + add_task(async function checkAlignment() { + async function getBounds() { + await new Promise(resolve => requestAnimationFrame(resolve)); + let button = anchorButton.getBoundingClientRect(); + let menu = panelList.getBoundingClientRect(); + return { + button: { + top: Math.round(button.top), + right: Math.round(button.right), + bottom: Math.round(button.bottom), + left: Math.round(button.left), + }, + menu: { + top: Math.round(menu.top), + right: Math.round(menu.right), + bottom: Math.round(menu.bottom), + left: Math.round(menu.left), + }, + } + }; + + async function showPanel() { + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + anchorButton.click(); + await shown; + } + + async function hidePanel() { + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + panelList.hide(); + await hidden; + } + + anchorButton.style.position = "fixed"; + + info("Verify menu alignment in the top left corner of the browser window"); + + anchorButton.style.top = "32px"; + anchorButton.style.right = "unset"; + anchorButton.style.bottom = "unset"; + anchorButton.style.left = "32px"; + + await showPanel(); + let bounds = await getBounds(); + is(bounds.menu.top, bounds.button.bottom, "Button's bottom matches Menu's top"); + is(bounds.menu.left, bounds.button.left, "Button and Menu have the same left value"); + await hidePanel(); + + info("Verify menu alignment in the top right corner of the browser window"); + + anchorButton.style.top = "32px"; + anchorButton.style.right = "32px"; + anchorButton.style.bottom = "unset"; + anchorButton.style.left = "unset"; + + await showPanel(); + bounds = await getBounds(); + is(bounds.menu.top, bounds.button.bottom, "Button's bottom matches Menu's top"); + is(bounds.menu.right, bounds.button.right, "Button and Menu have the same right value"); + await hidePanel(); + + info("Verify menu alignment in the bottom right corner of the browser window"); + + anchorButton.style.top = "unset"; + anchorButton.style.right = "32px"; + anchorButton.style.bottom = "32px"; + anchorButton.style.left = "unset"; + + await showPanel(); + bounds = await getBounds(); + is(bounds.menu.bottom, bounds.button.top, "Button's top matches Menu's bottom"); + is(bounds.menu.right, bounds.button.right, "Button and Menu have the same right value"); + await hidePanel(); + + info("Verify menu alignment in the bottom left corner of the browser window"); + + anchorButton.style.top = "unset"; + anchorButton.style.right = "unset"; + anchorButton.style.bottom = "32px"; + anchorButton.style.left = "32px"; + + await showPanel(); + bounds = await getBounds(); + is(bounds.menu.bottom, bounds.button.top, "Button's top matches Menu's bottom"); + is(bounds.menu.left, bounds.button.left, "Button and Menu have the same left value"); + await hidePanel(); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html b/toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html new file mode 100644 index 0000000000..ea5a9fc25c --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel List In XUL Panel</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <button id="anchor-button">Open</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + <panel-item>four</panel-item> + <panel-item>five</panel-item> + <panel-item>six</panel-item> + </panel-list> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); +let xulPanel, anchorButton, panelList; + + +add_setup(function setup() { + // The HTML document parser doesn't let us put XUL elements in the markup, so + // we have to create the <xul:panel> programmatically with script. + let content = document.getElementById("content"); + xulPanel = document.createXULElement("panel"); + panelList = document.getElementById("panel-list"); + xulPanel.appendChild(panelList); + content.appendChild(xulPanel); + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); +}); + +add_task(async function testXULPanelOpenFromClicks() { + let xulPanelShown = BrowserTestUtils.waitForPopupEvent(xulPanel, "shown"); + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + await xulPanelShown; + + ok(panelList.hasAttribute("inxulpanel"), "Should have inxulpanel attribute set"); + + let style = window.getComputedStyle(panelList); + is(style.top, "0px", "computed top inline style should be 0px."); + is(style.left, "0px", "computed left inline style should be 0px."); + + let xulPanelHidden = BrowserTestUtils.waitForPopupEvent(xulPanel, "hidden"); + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + await xulPanelHidden; +}); + +add_task(async function testXULPanelOpenProgrammatically() { + let xulPanelShown = BrowserTestUtils.waitForPopupEvent(xulPanel, "shown"); + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await shown; + await xulPanelShown; + + ok(panelList.hasAttribute("inxulpanel"), "Should have inxulpanel attribute set"); + let style = window.getComputedStyle(panelList); + is(style.top, "0px", "computed top inline style should be 0px."); + is(style.left, "0px", "computed left inline style should be 0px."); + + let xulPanelHidden = BrowserTestUtils.waitForPopupEvent(xulPanel, "hidden"); + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + panelList.hide(); + await hidden; + await xulPanelHidden; +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html b/toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html new file mode 100644 index 0000000000..0708174a88 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel List Min-width From Anchor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <button id="anchor-button">This is a button with a long string to act as a wide anchor.</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + <panel-item>four</panel-item> + <panel-item>five</panel-item> + <panel-item>six</panel-item> + </panel-list> +</div> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); +let anchorButton, panelList; + +add_setup(function setup() { + panelList = document.getElementById("panel-list"); + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); +}); + +add_task(async function minWidthFromAnchor() { + let anchorWidth = anchorButton.getBoundingClientRect().width; + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + + let panelWidth = panelList.getBoundingClientRect().width; + isnot(anchorWidth, panelWidth, "Without min-width-from-anchor, panel should not have anchor width."); + + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + + panelList.toggleAttribute("min-width-from-anchor", true); + + shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + + panelWidth = panelList.getBoundingClientRect().width; + is(anchorWidth, panelWidth, "With min-width-from-anchor, panel should have anchor width."); + + hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html b/toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html new file mode 100644 index 0000000000..9f265d4cf9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1802215 - Allow <panel-list> to be anchored to shadow DOM nodes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="./panel-list.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" href="./panel-list.css"/> + <link rel="stylesheet" href="./panel-item.css"/> + <script> + /** + * Define a simple custom element that includes a <button> in its + * shadow DOM to anchor a panel-list on. The TestElement is slotted, + * putting any direct descendents right after a 400px tall <div> + * with a red border. + */ + class TestElement extends HTMLElement { + static get fragment() { + const MARKUP = ` + <template> + <div style="height: 100px; border: 1px solid red;"> + <button id="anchor">Anchor</button> + </div> + <slot></slot> + </template> + `; + + let parser = new DOMParser(); + let doc = parser.parseFromString(MARKUP, "text/html"); + TestElement.template = document.importNode( + doc.querySelector("template"), + true + ); + let fragment = TestElement.template.content.cloneNode(true); + return fragment; + } + + connectedCallback() { + this.shadow = this.attachShadow({ mode: "closed" }); + this.shadow.appendChild(TestElement.fragment); + this.anchor = this.shadow.querySelector("#anchor"); + this.anchor.addEventListener("click", event => { + let panelList = this.querySelector("panel-list"); + panelList.toggle(event); + }); + } + + doClick() { + this.anchor.click(); + } + } + + customElements.define("test-element", TestElement); + + /** + * Tests that if a <panel-list> is anchored on a node within a custom + * element shadow DOM, that it anchors properly to that shadow node. + */ + add_task(async function test_panel_list_anchor_on_shadow_node() { + let testElement = document.getElementById("test-element"); + let panelList = document.getElementById("test-list"); + + let openPromise = new Promise(resolve => { + panelList.addEventListener("shown", resolve, { once: true }); + }); + testElement.doClick(); + await openPromise; + + let panelRect = panelList.getBoundingClientRect(); + let anchorRect = testElement.anchor.getBoundingClientRect(); + // Recalculate the <panel-list> rect top value relative to the top-left + // of the anchorRect. We expect the <panel-list> to be tightly anchored + // to the bottom of the <button>, so we expect this new value to be 0. + let panelTopLeftRelativeToAnchorTopLeft = panelRect.top - anchorRect.top - anchorRect.height; + is( + panelTopLeftRelativeToAnchorTopLeft, + 0, + "Panel should be tightly anchored to the bottom of the button shadow node." + ); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <test-element id="test-element"> + <panel-list id="test-list"> + <panel-item>An item</panel-item> + <panel-item>Another item</panel-item> + </panel-list> + </test-element> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_popupanchor.xhtml b/toolkit/content/tests/widgets/test_popupanchor.xhtml new file mode 100644 index 0000000000..5c78cf62fc --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupanchor.xhtml @@ -0,0 +1,387 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + animate="false" + noautohide="true"> + </panel> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +var anchor, panel; + +function is_close(got, exp, msg) { + // on some platforms we see differences of a fraction of a pixel - so + // allow any difference of < 1 pixels as being OK. + ok(Math.abs(got - exp) < 1, msg + ": " + got + " should be equal(-ish) to " + exp); +} + +function margins(popup) { + let ret = {}; + let cs = getComputedStyle(popup); + for (let side of ["top", "right", "bottom", "left"]) { + ret[side] = parseFloat(cs.getPropertyValue("margin-" + side)); + } + return ret; +} + +function checkPositionRelativeToAnchor(side) { + var panelRect = panel.getBoundingClientRect(); + var anchorRect = anchor.getBoundingClientRect(); + switch (side) { + case "left": + case "right": + is_close(panelRect.top - margins(panel).top, anchorRect.bottom, "top of panel should be at bottom of anchor"); + break; + case "top": + case "bottom": + is_close(panelRect.right + margins(panel).left, anchorRect.left, "right of panel should be left of anchor"); + break; + default: + ok(false, "unknown side " + side); + break; + } +} + +function openSlidingPopup(position, callback) { + panel.setAttribute("flip", "slide"); + _openPopup(position, callback); +} + +function openPopup(position, callback) { + panel.setAttribute("flip", "both"); + _openPopup(position, callback); +} + +async function waitForPopupPositioned(actionFn, callback) +{ + info("waitForPopupPositioned"); + let a = new Promise(resolve => { + panel.addEventListener("popuppositioned", () => resolve(true), { once: true }); + }); + + actionFn(); + + // Ensure we get at least one event, but we might get more than one, so wait for them as needed. + // + // The double-raf ensures layout runs once. setTimeout is needed because that's how popuppositioned is scheduled. + let b = new Promise(resolve => { + requestAnimationFrame(() => requestAnimationFrame(() => { + setTimeout(() => resolve(false), 0); + })); + }); + + let gotEvent = await Promise.race([a, b]); + info("got event: " + gotEvent); + if (gotEvent) { + waitForPopupPositioned(() => {}, callback); + } else { + SimpleTest.executeSoon(callback); + } +} + +function _openPopup(position, callback) { + panel.setAttribute("side", "noside"); + panel.addEventListener("popupshown", callback, {once: true}); + panel.openPopup(anchor, position); +} + +var tests = [ + // A panel with the anchor after_end - the anchor should not move on resize + ['simpleResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + checkPositionRelativeToAnchor("right"); // should not have flipped, so still "right" + panel.sizeTo(origPanelRect.width, origPanelRect.height); + checkPositionRelativeToAnchor("right"); // should not have flipped, so still "right" + next(); + }); + }], + + ['simpleResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + checkPositionRelativeToAnchor("bottom"); // should not have flipped. + panel.sizeTo(origPanelRect.width, origPanelRect.height); + checkPositionRelativeToAnchor("bottom"); // should not have flipped. + next(); + }); + }], + ['flippingResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + waitForPopupPositioned( + () => { panel.sizeTo(anchor.getBoundingClientRect().left + 50, 50); }, + () => { + checkPositionRelativeToAnchor("left"); // check it flipped. + next(); + }); + }); + }], + ['flippingResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + waitForPopupPositioned( + () => { panel.sizeTo(50, anchor.getBoundingClientRect().top + 50); }, + () => { + checkPositionRelativeToAnchor("top"); // check it flipped. + next(); + }); + }); + }], + + ['simpleMoveToAnchorHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "after_end", 20, 0); }, + () => { + // the anchor and the panel should have moved 20px right without flipping. + checkPositionRelativeToAnchor("right"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "after_end", -20, 0); }, + () => { + // the anchor and the panel should have moved 20px left without flipping. + checkPositionRelativeToAnchor("right"); + next(); + }); + }); + }); + }], + + ['simpleMoveToAnchorVertical', 'middle', function(next) { + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "start_after", 0, 20); }, + () => { + // the anchor and the panel should have moved 20px down without flipping. + checkPositionRelativeToAnchor("bottom"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "start_after", 0, -20) }, + () => { + // the anchor and the panel should have moved 20px up without flipping. + checkPositionRelativeToAnchor("bottom"); + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip horizontally + ['flippingMoveToAnchorHorizontal', 'middle', function(next) { + var anchorRight = anchor.getBoundingClientRect().right; + // Size the panel such that it only just fits from the left-hand side of + // the window to the right of the anchor - thus, it will fit when + // anchored to the right-hand side of the anchor. + panel.sizeTo(anchorRight - 10, 100); + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + // Ask for it to be anchored 1/2 way between the left edge of the window + // and the anchor right - it can't fit with the panel on the left/arrow + // on the right, so it must flip (arrow on the left, panel on the right) + var offset = Math.floor(-anchorRight / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", offset, 0), + () => { + checkPositionRelativeToAnchor("left"); + // resize back to original and move to a zero offset - it should flip back. + + panel.sizeTo(anchorRight - 10, 100); + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", 0, 0), + () => { + checkPositionRelativeToAnchor("right"); // should have flipped back. + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip vertically + ['flippingMoveToAnchorVertical', 'middle', function(next) { + var anchorBottom = anchor.getBoundingClientRect().bottom; + // See comments above in flippingMoveToAnchorHorizontal, but read + // "top/bottom" instead of "left/right" + panel.sizeTo(100, anchorBottom - 10); + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + var offset = Math.floor(-anchorBottom / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, offset), + () => { + checkPositionRelativeToAnchor("top"); + panel.sizeTo(100, anchorBottom - 10); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, 0), + () => { + checkPositionRelativeToAnchor("bottom"); + next(); + }); + }); + }); + }], + + ['veryWidePanel-after_end', 'middle', function(next) { + openSlidingPopup("after_end", function() { + waitForPopupPositioned( + () => { panel.sizeTo(window.innerWidth - 10, 60); }, + () => { + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested.") + next(); + }); + }); + }], + + ['veryWidePanel-before_start', 'middle', function(next) { + openSlidingPopup("before_start", function() { + waitForPopupPositioned( + () => { panel.sizeTo(window.innerWidth - 10, 60); }, + () => { + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested") + next(); + }); + }); + }], + + ['veryTallPanel-start_after', 'middle', function(next) { + openSlidingPopup("start_after", function() { + waitForPopupPositioned( + () => { panel.sizeTo(100, window.innerHeight - 10); }, + () => { + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested.") + next(); + }); + }); + }], + + ['veryTallPanel-start_before', 'middle', function(next) { + openSlidingPopup("start_before", function() { + waitForPopupPositioned( + () => { panel.sizeTo(100, window.innerHeight - 10); }, + () => { + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested") + next(); + }); + }); + }], + + // Tests against the anchor at the right-hand side of the window + ['afterend', 'right', function(next) { + openPopup("after_end", function() { + // when we request too far to the right/bottom, the panel gets shrunk + // and moved. The amount it is shrunk by is how far it is moved. + checkPositionRelativeToAnchor("right"); + next(); + }); + }], + + ['after_start', 'right', function(next) { + openPopup("after_start", function() { + // See above - we are still too far to the right, but the anchor is + // on the other side. + checkPositionRelativeToAnchor("right"); + next(); + }); + }], + + // Tests against the anchor at the left-hand side of the window + ['after_start', 'left', function(next) { + openPopup("after_start", function() { + var panelRect = panel.getBoundingClientRect(); + ok(panelRect.left - margins(panel).left >= 0, "panel remains within the screen"); + checkPositionRelativeToAnchor("left"); + next(); + }); + }], +] + +function runTests() { + function runNextTest() { + let result = tests.shift(); + if (!result) { + // out of tests + panel.hidePopup(); + SimpleTest.finish(); + return; + } + let [name, anchorPos, test] = result; + SimpleTest.info("sub-test " + anchorPos + "." + name + " starting"); + // first arrange for the anchor to be where the test requires it. + panel.hidePopup(); + panel.sizeTo(100, 50); + // hide all the anchors here, then later we make one of them visible. + document.getElementById("anchor-left-wrapper").style.display = "none"; + document.getElementById("anchor-middle-wrapper").style.display = "none"; + document.getElementById("anchor-right-wrapper").style.display = "none"; + switch(anchorPos) { + case 'middle': + anchor = document.getElementById("anchor-middle"); + document.getElementById("anchor-middle-wrapper").style.display = "block"; + break; + case 'left': + anchor = document.getElementById("anchor-left"); + document.getElementById("anchor-left-wrapper").style.display = "block"; + break; + case 'right': + anchor = document.getElementById("anchor-right"); + document.getElementById("anchor-right-wrapper").style.display = "block"; + break; + default: + SimpleTest.ok(false, "Bad anchorPos: " + anchorPos); + runNextTest(); + return; + } + try { + test(runNextTest); + } catch (ex) { + SimpleTest.ok(false, "sub-test " + anchorPos + "." + name + " failed: " + ex.toString() + "\n" + ex.stack); + runNextTest(); + } + } + runNextTest(); +} + +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + // anchor is set by the test runner above + panel = document.getElementById("testPanel"); + + runTests(); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<!-- Our tests assume at least 100px around the anchor on all sides, else the + panel may flip when we don't expect it to +--> +<div id="anchor-middle-wrapper" style="margin: 100px 100px 100px 100px;"> + <p>The anchor --> <span id="anchor-middle">v</span> <--</p> +</div> +<div id="anchor-left-wrapper" style="text-align: left; display: none;"> + <p><span id="anchor-left">v</span> <-- The anchor;</p> +</div> +<div id="anchor-right-wrapper" style="text-align: right; display: none;"> + <p>The anchor --> <span id="anchor-right">v</span></p> +</div> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_popupreflows.xhtml b/toolkit/content/tests/widgets/test_popupreflows.xhtml new file mode 100644 index 0000000000..c3f8068779 --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupreflows.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Reflow Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + noautohide="true"> + </panel> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +let panel, anchor; + +// A reflow observer - it just remembers the stack trace of all sync reflows +// done by the panel. +let observer = { + reflows: [], + reflow (start, end) { + // Ignore reflows triggered by native code + // (Reflows from native code only have an empty stack after the first frame) + var path = (new Error().stack).split("\n").slice(1).join(""); + if (path === "") { + return; + } + + this.reflows.push(new Error().stack); + }, + + reflowInterruptible (start, end) { + // We're not interested in interruptible reflows. Why, you ask? Because + // we've simply cargo-culted this test from browser_tabopen_reflows.js! + }, + + QueryInterface: ChromeUtils.generateQI(["nsIReflowObserver", + "nsISupportsWeakReference"]) +}; + +// A test utility that counts the reflows caused by a test function. If the +// count of reflows isn't what is expected, it causes a test failure and logs +// the stack trace of all seen reflows. +function countReflows(testfn, expected) { + return new Promise(resolve => { + observer.reflows = []; + let docShell = panel.ownerGlobal.docShell; + docShell.addWeakReflowObserver(observer); + testfn().then(() => { + docShell.removeWeakReflowObserver(observer); + SimpleTest.is(observer.reflows.length, expected, "correct number of reflows"); + if (observer.reflows.length != expected) { + SimpleTest.info("stack traces of reflows:\n" + observer.reflows.join("\n") + "\n"); + } + resolve(); + }); + }); +} + +function openPopup() { + return new Promise(resolve => { + panel.addEventListener("popupshown", function popupshown() { + resolve(); + }, {once: true}); + panel.openPopup(anchor, "before_start"); + }); +} + +// ******************** +// The actual tests... +// We only have one atm - simply open a popup. +// +function testSimplePanel() { + return openPopup(); +} + +// ******************** +// The test harness... +// +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + anchor = document.getElementById("anchor"); + panel = document.getElementById("testPanel"); + + // and off we go... + countReflows(testSimplePanel, 0).then(SimpleTest.finish); +}); +]]> +</script> +<body xmlns="http://www.w3.org/1999/xhtml"> + <p>The anchor --> <span id="anchor">v</span> <--</p> +</body> +</window> diff --git a/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml b/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml new file mode 100644 index 0000000000..d1dc9c1c20 --- /dev/null +++ b/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- + XUL Widget Test for reordering tree columns + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree_column_reorder, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script src="tree_shared.js"/> + +<tree id="tree-column-reorder" rows="1" enableColumnDrag="true"> + <treecols> + <treecol id="col_0" label="col_0" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_1" label="col_1" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_2" label="col_2" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_3" label="col_3" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_4" label="col_4" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_5" label="col_5" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_6" label="col_6" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_7" label="col_7" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_8" label="col_8" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_9" label="col_9" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_10" label="col_10" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_11" label="col_11" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_12" label="col_12" flex="1"/> + </treecols> + <treechildren id="treechildren-column-reorder"> + <treeitem> + <treerow> + <treecell label="col_0"/> + <treecell label="col_1"/> + <treecell label="col_2"/> + <treecell label="col_3"/> + <treecell label="col_4"/> + <treecell label="col_5"/> + <treecell label="col_6"/> + <treecell label="col_7"/> + <treecell label="col_8"/> + <treecell label="col_9"/> + <treecell label="col_10"/> + <treecell label="col_11"/> + <treecell label="col_12"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html b/toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html new file mode 100644 index 0000000000..d52ec48f22 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html @@ -0,0 +1,22 @@ +<!doctype html> +<title>UA Widget getElementFromPoint</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<div id="host" style="width: 100px; height: 100px;"></div> +<script> +const host = document.getElementById("host"); +SpecialPowers.wrap(host.attachShadow({ mode: "open"})).setIsUAWidget(); +host.shadowRoot.innerHTML = ` + <div style="width: 100px; height: 100px; background-color: green;"></div> +`; +let hostRect = host.getBoundingClientRect(); +let point = { + x: hostRect.x + 50, + y: hostRect.y + 50, +}; +is(document.elementFromPoint(point.x, point.y), host, + "Host should be found from the document"); +is(host.shadowRoot.elementFromPoint(point.x, point.y), host.shadowRoot.firstElementChild, + "Should not have retargeted UA widget content to host unnecessarily"); +</script> diff --git a/toolkit/content/tests/widgets/test_ua_widget_sandbox.html b/toolkit/content/tests/widgets/test_ua_widget_sandbox.html new file mode 100644 index 0000000000..cc53e1c6d9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_sandbox.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>UA Widget sandbox test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const content = document.getElementById("content"); + +const div = content.appendChild(document.createElement("div")); +div.attachShadow({ mode: "open"}); +SpecialPowers.wrap(div.shadowRoot).setIsUAWidget(); + +const sandbox = SpecialPowers.Cu.getUAWidgetScope(SpecialPowers.wrap(div).nodePrincipal); + +SpecialPowers.setWrapped(sandbox, "info", SpecialPowers.wrapFor(info, sandbox)); +SpecialPowers.setWrapped(sandbox, "is", SpecialPowers.wrapFor(is, sandbox)); +SpecialPowers.setWrapped(sandbox, "ok", SpecialPowers.wrapFor(ok, sandbox)); + +const sandboxScript = function(shadowRoot) { + info("UA Widget scope tests"); + is(typeof window, "undefined", "The sandbox has no window"); + is(typeof document, "undefined", "The sandbox has no document"); + + let element = shadowRoot.host; + let doc = element.ownerDocument; + let win = doc.defaultView; + + ok(win.ShadowRoot.isInstance(shadowRoot), "shadowRoot is a ShadowRoot"); + ok(win.HTMLDivElement.isInstance(element), "Element is a <div>"); + + is("createElement" in doc, false, "No document.createElement"); + is("createElementNS" in doc, false, "No document.createElementNS"); + is("createTextNode" in doc, false, "No document.createTextNode"); + is("createComment" in doc, false, "No document.createComment"); + is("importNode" in doc, false, "No document.importNode"); + is("adoptNode" in doc, false, "No document.adoptNode"); + + is("insertBefore" in element, false, "No element.insertBefore"); + is("appendChild" in element, false, "No element.appendChild"); + is("replaceChild" in element, false, "No element.replaceChild"); + is("cloneNode" in element, false, "No element.cloneNode"); + + ok("importNodeAndAppendChildAt" in shadowRoot, "shadowRoot.importNodeAndAppendChildAt"); + ok("createElementAndAppendChildAt" in shadowRoot, "shadowRoot.createElementAndAppendChildAt"); + + info("UA Widget special methods tests"); + + const span = shadowRoot.createElementAndAppendChildAt(shadowRoot, "span"); + span.textContent = "Hello from <span>!"; + + is(shadowRoot.lastChild, span, "<span> inserted"); + + const parser = new win.DOMParser(); + let parserDoc = parser.parseFromString( + `<div xmlns="http://www.w3.org/1999/xhtml">Hello from DOMParser!</div>`, "application/xml"); + shadowRoot.importNodeAndAppendChildAt(shadowRoot, parserDoc.documentElement, true); + + ok(win.HTMLDivElement.isInstance(shadowRoot.lastChild), "<div> inserted"); + is(shadowRoot.lastChild.textContent, "Hello from DOMParser!", "Deep import node worked"); + + info("UA Widget reflectors tests"); + + win.wrappedJSObject.spanElementFromUAWidget = span; + win.wrappedJSObject.divElementFromUAWidget = shadowRoot.lastChild; +}; +SpecialPowers.Cu.evalInSandbox("this.script = " + sandboxScript.toString(), sandbox); +sandbox.script(div.shadowRoot); + +ok(SpecialPowers.wrap(HTMLSpanElement).isInstance(window.spanElementFromUAWidget), "<span> exposed"); +ok(SpecialPowers.wrap(HTMLDivElement).isInstance(window.divElementFromUAWidget), "<div> exposed"); + +try { + window.spanElementFromUAWidget.textContent; + ok(false, "Should throw."); +} catch (err) { + ok(/denied/.test(err), "Permission denied to access <span>"); +} + +try { + window.divElementFromUAWidget.textContent; + ok(false, "Should throw."); +} catch (err) { + ok(/denied/.test(err), "Permission denied to access <div>"); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_ua_widget_unbind.html b/toolkit/content/tests/widgets/test_ua_widget_unbind.html new file mode 100644 index 0000000000..89ae6c0f00 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_unbind.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>UA Widget unbind test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody"> + +const content = document.getElementById("content"); + +add_task(function() { + const video = document.createElement("video"); + video.controls = true; + ok(!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(video); + ok(!!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(video).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(video); + ok(!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is removed"); +}); + +add_task(function() { + const marquee = document.createElement("marquee"); + ok(!SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(marquee); + ok(!!SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(marquee).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(marquee); + ok(SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is not removed for marquee"); +}); + +add_task(function() { + const input = document.createElement("input"); + input.type = "date"; + ok(!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(input); + ok(!!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(input).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(input); + ok(!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is removed"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols.html b/toolkit/content/tests/widgets/test_videocontrols.html new file mode 100644 index 0000000000..32cd23df6a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols.html @@ -0,0 +1,564 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/* + * Positions of the UI elements, relative to the upper-left corner of the + * <video> box. + */ +const videoWidth = 320; +const videoHeight = 240; +const videoDuration = 3.8329999446868896; + +const controlBarMargin = 9; + +const playButtonWidth = 30; +const playButtonHeight = 40; +const muteButtonWidth = 30; +const muteButtonHeight = 40; +const positionAndDurationWidth = 75; +const fullscreenButtonWidth = 30; +const fullscreenButtonHeight = 40; +const volumeSliderWidth = 48; +const volumeSliderMarginStart = 4; +const volumeSliderMarginEnd = 6; +const scrubberMargin = 9; +const scrubberWidth = videoWidth - controlBarMargin - playButtonWidth - scrubberMargin * 2 - positionAndDurationWidth - muteButtonWidth - volumeSliderMarginStart - volumeSliderWidth - volumeSliderMarginEnd - fullscreenButtonWidth - controlBarMargin; +const scrubberHeight = 40; + +// Play button is on the bottom-left +const playButtonCenterX = 0 + Math.round(playButtonWidth / 2); +const playButtonCenterY = videoHeight - Math.round(playButtonHeight / 2); +// Mute button is on the bottom-right before the full screen button and volume slider +const muteButtonCenterX = videoWidth - Math.round(muteButtonWidth / 2) - volumeSliderWidth - fullscreenButtonWidth - controlBarMargin; +const muteButtonCenterY = videoHeight - Math.round(muteButtonHeight / 2); +// Fullscreen button is on the bottom-right at the far end +const fullscreenButtonCenterX = videoWidth - Math.round(fullscreenButtonWidth / 2) - controlBarMargin; +const fullscreenButtonCenterY = videoHeight - Math.round(fullscreenButtonHeight / 2); +// Scrubber bar is between the play and mute buttons. We don't need it's +// X center, just the offset of its box. +const scrubberOffsetX = controlBarMargin + playButtonWidth + scrubberMargin; +const scrubberCenterY = videoHeight - Math.round(scrubberHeight / 2); + +const video = document.getElementById("video"); + +let requiredEvents = []; +let forbiddenEvents = []; +let receivedEvents = []; +let expectingEventPromise; + +async function isMuteButtonMuted() { + const muteButton = getElementWithinVideo(video, "muteButton"); + await new Promise(SimpleTest.executeSoon); + return muteButton.getAttribute("muted") === "true"; +} + +async function isVolumeSliderShowingCorrectVolume(expectedVolume) { + const volumeControl = getElementWithinVideo(video, "volumeControl"); + await new Promise(SimpleTest.executeSoon); + is(+volumeControl.value, expectedVolume * 100, "volume slider should match expected volume"); +} + +function forceReframe() { + // Setting display then getting the bounding rect to force a frame + // reconstruction on the video element. + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + video.getBoundingClientRect(); +} + +function captureEventThenCheck(event) { + if (event) { + info(`Received event ${event.type}.`); + receivedEvents.push(event.type); + } + + const cleanupExpectations = () => { + requiredEvents.length = 0; + forbiddenEvents.length = 0; + receivedEvents.length = 0; + } + + // If receivedEvents contains any of the forbiddenEvents, reject the expectingEventPromise. + for (const forbidden of forbiddenEvents) { + if (receivedEvents.includes(forbidden)) { + // Capture list of requiredEvents for later reporting. + const oldRequiredEvents = requiredEvents.slice(); + cleanupExpectations(); + expectingEventPromise.reject(new Error(`Got forbidden event ${forbidden} while expecting ${oldRequiredEvents}`)); + return; + } + } + + if (!requiredEvents.length) { + // We might be getting an event before we started waiting for it. That's fine, + // just early exit. + return; + } + + // We are expecting at least one event. If receivedEvents is lacking one of the + // requiredEvents, exit. + for (const required of requiredEvents) { + if (!receivedEvents.includes(required)) { + return; + } + } + + // We've received all the events we required. Resolve the expectingEventPromise. + info(`No longer waiting for expected event(s) ${requiredEvents}.`); + cleanupExpectations(); + + // Don't resolve this right away, because this is called from within event handlers and + // we want all other event handlers to have a chance to respond to this event before we + // proceed with the test. This solves problems with things like a play-pause-play, where + // some of the actions will be discarded if the video controls themselves aren't in the + // expected state. + SimpleTest.executeSoon(expectingEventPromise.resolve); +} + +function waitForEvent(required, forbidden) { + return new Promise((resolve, reject) => { + expectingEventPromise = {resolve, reject}; + + info(`Waiting for ${required}` + (forbidden ? ` but not ${forbidden}...` : `...`)); + if (Array.isArray(required)) { + requiredEvents.push(...required); + } else { + requiredEvents.push(required); + } + if (forbidden) { + if (Array.isArray(forbidden)) { + forbiddenEvents.push(...forbidden); + } else { + forbiddenEvents.push(forbidden); + } + } + + // Immediately check the received events, since the calling pattern used in this test is + // calling this method *after* the events could have been triggered. + captureEventThenCheck(); + }); +} + +async function repeatUntilSuccessful(f) { + let successful = false; + do { + try { + // Delay one event loop. + await new Promise(r => SimpleTest.executeSoon(r)); + await f(); + successful = true; + } catch (error) { + info(`repeatUntilSuccessful: error ${error}.`); + } + } while(!successful); +} + +add_task(async function setup() { + SimpleTest.requestCompleteLog(); + await SpecialPowers.pushPrefEnv({ + "set": [ + ["media.cache_size", 40000], + ["full-screen-api.enabled", true], + ["full-screen-api.allow-trusted-requests-only", false], + ["full-screen-api.transition-duration.enter", "0 0"], + ["full-screen-api.transition-duration.leave", "0 0"], + ]}); + await new Promise(resolve => { + video.addEventListener("canplaythrough", resolve, {once: true}); + video.src = "seek_with_sound.ogg"; + }); + + video.addEventListener("play", captureEventThenCheck); + video.addEventListener("pause", captureEventThenCheck); + video.addEventListener("volumechange", captureEventThenCheck); + video.addEventListener("seeking", captureEventThenCheck); + video.addEventListener("seeked", captureEventThenCheck); + document.addEventListener("mozfullscreenchange", captureEventThenCheck); + document.addEventListener("fullscreenerror", captureEventThenCheck); + + ["mousedown", "mouseup", "dblclick", "click"] + .forEach((eventType) => { + window.addEventListener(eventType, (evt) => { + // Prevent default action of leaked events and make the tests fail. + evt.preventDefault(); + ok(false, "Event " + eventType + " in videocontrol should not leak to content;" + + "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element."); + }); + }); + + // Check initial state upon load + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function click_playbutton() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function click_pausebutton() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("pause"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function mute_volume() { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); +}); + +add_task(async function unmute_volume() { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +/* + * Bug 470596: Make sure that having CSS border or padding doesn't + * break the controls (though it should move them) + */ +add_task(async function styled_video() { + video.style.border = "medium solid purple"; + video.style.borderWidth = "30px 40px 50px 60px"; + video.style.padding = "10px 20px 30px 40px"; + // totals: top: 40px, right: 60px, bottom: 80px, left: 100px + + // Click the play button + synthesizeMouse(video, 100 + playButtonCenterX, 40 + playButtonCenterY, { }); + await waitForEvent("play"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Pause the video + video.pause(); + await waitForEvent("pause"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Click the mute button + synthesizeMouse(video, 100 + muteButtonCenterX, 40 + muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); + + // Clear the style set + video.style.border = ""; + video.style.borderWidth = ""; + video.style.padding = ""; + + // Unmute the video + video.muted = false; + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +/* + * Previous tests have moved playback postion, reset it to 0. + */ +add_task(async function reset_currentTime() { + ok(true, "video position is at " + video.currentTime); + video.currentTime = 0.0; + await waitForEvent(["seeking", "seeked"]); + // Bug 477434 -- sometimes we get 0.098999 here instead of 0! + // is(video.currentTime, 0.0, "checking playback position"); + ok(true, "video position is at " + video.currentTime); +}); + +/* + * Drag the slider's thumb to the halfway point with the mouse. + */ +add_task(async function drag_slider() { + const beginDragX = scrubberOffsetX; + const endDragX = scrubberOffsetX + (scrubberWidth / 2); + const expectedTime = videoDuration / 2; + + function mousemoved(evt) { + ok(false, "Mousemove event should not be handled by content while dragging; " + + "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element."); + } + + window.addEventListener("mousemove", mousemoved); + + synthesizeMouse(video, beginDragX, scrubberCenterY, {type: "mousedown", button: 0}); + synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mousemove", button: 0}); + synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mouseup", button: 0}); + await waitForEvent(["seeking", "seeked"]); + ok(true, "video position is at " + video.currentTime); + // The width of srubber is not equal in every platform as we use system default font + // in duration and position box. We can not precisely drag to expected position, so + // we just make sure the difference is within 10% of video duration. + ok(Math.abs(video.currentTime - expectedTime) < videoDuration / 10, "checking expected playback position"); + + window.removeEventListener("mousemove", mousemoved); +}); + +/* + * Click the slider at the 1/4 point with the mouse (jump backwards) + */ +add_task(async function click_slider() { + synthesizeMouse(video, scrubberOffsetX + (scrubberWidth / 4), scrubberCenterY, {}); + await waitForEvent(["seeking", "seeked"]); + ok(true, "video position is at " + video.currentTime); + // The scrubber currently just jumps towards the nearest pageIncrement point, not + // precisely to the point clicked. So, expectedTime isn't (videoDuration / 4). + // We should end up at 1.733, but sometimes we end up at 1.498. I guess + // it's timing depending if the <scale> things it's click-and-hold, or a + // single click. So, just make sure we end up less that the previous + // position. + const lastPosition = (videoDuration / 2) - 0.1; + ok(video.currentTime < lastPosition, "checking expected playback position"); + + // Set volume to 0.1 so one down arrow hit will decrease it to 0. + video.volume = 0.1; + await waitForEvent("volumechange"); + is(video.volume, 0.1, "Volume should be set."); + ok(!video.muted, "Video is not muted."); +}); + +// See bug 694696. +add_task(async function change_volume() { + video.focus(); + + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeKey("KEY_ArrowUp"); + await waitForEvent("volumechange"); + is(video.volume, 0.1, "Volume is increased."); + ok(!video.muted, "Video is not muted."); + ok(!(await isMuteButtonMuted()), "Mute button says it's not muted"); + + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.5, "Volume should be 0.5."); + ok(!video.muted, "Video is not muted."); + + synthesizeKey("KEY_ArrowUp"); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(video.muted, "Video is muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + ok(!(await isMuteButtonMuted()), "Mute button says it's not muted"); + + await repeatUntilSuccessful(async () => { + synthesizeMouse(video, fullscreenButtonCenterX, fullscreenButtonCenterY, {}); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + is(video.volume, 0.6, "Volume should still be 0.6"); + await isVolumeSliderShowingCorrectVolume(video.volume); + + await repeatUntilSuccessful(async () => { + video.focus(); + synthesizeKey("KEY_Escape"); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + is(video.volume, 0.6, "Volume should still be 0.6"); + await isVolumeSliderShowingCorrectVolume(video.volume); + forceReframe(); + + video.focus(); + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0.5, "Volume should be decreased by 0.1"); + await isVolumeSliderShowingCorrectVolume(video.volume); +}); + +add_task(async function whitespace_pause_video() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); + + video.focus(); + sendString(" "); + await waitForEvent("pause"); + + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); +}); + +/* + * Bug 1352724: Click and hold on timeline should pause video immediately. + */ +add_task(async function click_and_hold_slider() { + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {type: "mousedown", button: 0}); + await waitForEvent(["pause", "seeking", "seeked"]); + + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {}); + await waitForEvent("play"); +}); + +/* + * Bug 1402877: Don't let click event dispatch through media controls to video element. + */ +add_task(async function click_event_dispatch() { + const clientScriptClickHandler = (e) => { + ok(false, "Should not receive the event"); + }; + video.addEventListener("click", clientScriptClickHandler); + + video.pause(); + await waitForEvent("pause"); + video.currentTime = 0.0; + await waitForEvent(["seeking", "seeked"]); + is(video.paused, true, "checking video play state"); + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {}); + await waitForEvent(["seeking", "seeked"]); + + video.removeEventListener("click", clientScriptClickHandler); +}); + +// Bug 1367194: Always ensure video is paused before finishing the test. +add_task(async function ensure_video_pause() { + if (!video.paused) { + video.pause(); + await waitForEvent("pause"); + } +}); + +// Bug 1452342: Make sure the cursor hides and shows on full screen mode. +add_task(async function ensure_fullscreen_cursor() { + video.removeAttribute("mozNoDynamicControls"); + video.play(); + await waitForEvent("play"); + + await repeatUntilSuccessful(async () => { + video.focus(); + await video.mozRequestFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + const controlsSpacer = getElementWithinVideo(video, "controlsSpacer"); + is(controlsSpacer.hasAttribute("hideCursor"), true, "Cursor is hidden"); + + let delta = 1; + await SimpleTest.promiseWaitForCondition(() => { + // Wiggle the mouse a bit + synthesizeMouse(video, playButtonCenterX + delta, playButtonCenterY + delta, { type: "mousemove" }); + delta = !delta; + return !controlsSpacer.hasAttribute("hideCursor"); + }, "Waiting for hideCursor attribute to disappear"); + is(controlsSpacer.hasAttribute("hideCursor"), false, "Cursor is shown"); + + // Restore + video.setAttribute("mozNoDynamicControls", ""); + + await repeatUntilSuccessful(async () => { + await document.mozCancelFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + if (!video.paused) { + video.pause(); + await waitForEvent("pause"); + } +}); + +// Bug 1505547: Make sure the fullscreen button works if the video element is in shadow tree. +add_task(async function ensure_fullscreen_button() { + video.removeAttribute("mozNoDynamicControls"); + let host = document.getElementById("host"); + let root = host.attachShadow({ mode: "open" }); + root.appendChild(video); + forceReframe(); + + await repeatUntilSuccessful(async () => { + await video.mozRequestFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + await repeatUntilSuccessful(async () => { + // Compute the location to click on to hit the fullscreen button. + // Use the video size instead of the screen size here, because mozfullscreenchange + // does not guarantee that our document covers the screen, see bug 1575630. + const r = video.getBoundingClientRect(); + const buttonCenterX = r.right - fullscreenButtonWidth / 2 - controlBarMargin; + const buttonCenterY = r.bottom - fullscreenButtonHeight / 2; + + // Though the video no longer has mozNoDynamicControls, it sometimes appears + // in the shadow DOM without visible controls. This might happen because + // toggling the attribute doesn't force the controls to appear or disappear; + // it just affects the timed fadeout behavior. So we wiggle the mouse here + // as if we were still using dynamic controls. + synthesizeMouse(video, buttonCenterX, buttonCenterY, { type: "mousemove" }); + + info(`Clicking at ${buttonCenterX}, ${buttonCenterY}.`); + synthesizeMouse(video, buttonCenterX, buttonCenterY, {}); + await waitForEvent("mozfullscreenchange", ["fullscreenerror", "play", "pause"]); + }); + + // Restore + video.setAttribute("mozNoDynamicControls", ""); + document.getElementById("content").appendChild(video); + forceReframe(); +}); + +add_task(async function ensure_doubleclick_triggers_fullscreen() { + const { x, y } = video.getBoundingClientRect(); + info("Simulate double click on media player."); + + await repeatUntilSuccessful(async () => { + synthesizeMouse(video, x, y, { clickCount: 2 }); + // TODO: A double-click for fullscreen should *not* cause the video to play, + // but it does. Adding the "play" event to the forbidden events makes the + // test timeout. + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + ok(true, "Double clicking should trigger fullscreen event"); + + await repeatUntilSuccessful(async () => { + await document.mozCancelFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio.html b/toolkit/content/tests/widgets/test_videocontrols_audio.html new file mode 100644 index 0000000000..ad528f4c27 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls with Audio file test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="metadata"></video> +</div> + +<pre id="test"> +<script> + + const video = document.getElementById("video"); + function loadedmetadata(event) { + SimpleTest.executeSoon(function() { + const controlBar = SpecialPowers.wrap(video).openOrClosedShadowRoot.querySelector(".controlBar"); + is(controlBar.getAttribute("fullscreen-unavailable"), "true", "Fullscreen button is hidden"); + SimpleTest.finish(); + }); + } + + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startTest); + function startTest() { + // Kick off test once audio has loaded. + video.addEventListener("loadedmetadata", loadedmetadata, { once: true }); + video.src = "audio.ogg"; + } + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html new file mode 100644 index 0000000000..b656de0103 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-2a.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2b.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2c.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2d.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2e.html", ref: "videocontrols_direction-2-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html b/toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html new file mode 100644 index 0000000000..a787358394 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - clickToPlayAriaLabel</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"> + <video controls preload="auto" width="480" height="320"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const videoElems = [...document.getElementsByTagName("video")]; + const testCases = []; + + function testUI(video) { + const clickToPlay = getElementWithinVideo(video, "clickToPlay"); + ok(!!clickToPlay.getAttribute("aria-label"), "clickToPlay has aria-label attribute"); + }; + + videoElems.forEach(video => { + testCases.push(() => new Promise(resolve => { + SimpleTest.executeSoon(async () => { + const { widget } = SpecialPowers.wrap(window) + .windowGlobalChild.getActor("UAWidgets") + .widgets.get(video); + await widget.impl.Utils.l10n.translateRoots(); + testUI(video); + resolve(); + }); + })); + }); + + function executeTasks(tasks) { + return tasks.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function start() { + executeTasks(testCases).then(SimpleTest.finish); + } + + function loadevent() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + window.addEventListener("load", loadevent); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html b/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html new file mode 100644 index 0000000000..39d6ff494f --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - KeyHandler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"> + <track + id="track1" + kind="subtitles" + label="[test] en" + srclang="en" + src="test-webvtt-1.vtt" + /> + <track + id="track2" + kind="subtitles" + label="[test] fr" + srclang="fr" + src="test-webvtt-2.vtt" + /> + </video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + const video = document.getElementById("video"); + const closedCaptionButton = getElementWithinVideo(video, "closedCaptionButton"); + const fullscreenButton = getElementWithinVideo(video, "fullscreenButton"); + const textTrackList = getElementWithinVideo(video, "textTrackList"); + const textTrackListContainer = getElementWithinVideo(video, "textTrackListContainer"); + + function isClosedCaptionVisible() { + return !textTrackListContainer.hidden; + } + + // Setup video + tests.push(done => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}, done); + }, done => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", done); + }, cleanup); + + tests.push(done => { + info("Opening the CC menu should focus the first item in the menu"); + info("Focusing and clicking the closed caption button"); + closedCaptionButton.focus(); + synthesizeKey(" "); + ok(isClosedCaptionVisible(), "The CC menu is visible"); + ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be in focus"); + done(); + }); + + tests.push(done => { + info("aria-expanded should be reflected whether the CC menu is open or not"); + ok(closedCaptionButton.getAttribute("aria-expanded") === "false", "Closed CC menu has aria-expanded set to false"); + info("Focusing and clicking the closed caption button"); + closedCaptionButton.focus(); + synthesizeKey(" "); + ok(isClosedCaptionVisible(), "The CC menu is visible"); + ok(closedCaptionButton.getAttribute("aria-expanded") === "true", "Open CC menu has aria-expanded set to true"); + done(); + }); + + tests.push(done => { + info("If CC menu is open, then arrow keys should navigate menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be in focus first"); + info("Pressing down arrow"); + synthesizeKey("KEY_ArrowDown"); + ok(textTrackList.children[1].matches(":focus"), "The second item in CC menu should now be in focus"); + info("Pressing up arrow"); + synthesizeKey("KEY_ArrowUp"); + ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be back in focus again"); + done(); + }); + + tests.push(done => { + info("Escape should close the CC menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + info("Pressing Escape key"); + synthesizeKey("KEY_Escape"); + ok(closedCaptionButton.matches(":focus"), "The CC button should be in focus"); + ok(!isClosedCaptionVisible(), "The CC menu should be closed"); + done(); + }); + + tests.push(done => { + info("Tabbing away should close the CC menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + info("Pressing Tab key 3x"); + synthesizeKey("KEY_Tab"); + synthesizeKey("KEY_Tab"); + synthesizeKey("KEY_Tab"); + ok(fullscreenButton.matches(":focus"), "The fullscreen button should be in focus"); + ok(!isClosedCaptionVisible(), "The CC menu should be closed"); + done(); + }); + + tests.push(done => { + info("Shift + Tabbing away should close the CC menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + info("Pressing Shift + Tab key"); + synthesizeKey("KEY_Tab", { shiftKey: true }); + ok(closedCaptionButton.matches(":focus"), "The CC button should be in focus"); + ok(!isClosedCaptionVisible(), "The CC menu should be closed"); + done(); + }); + + function cleanup(done) { + if (isClosedCaptionVisible()) { + closedCaptionButton.click(); + } + done(); + } + // add cleanup after every test + tests = tests.reduce((a, v) => a.concat([v, cleanup]), []); + + tests.push(SimpleTest.finish); + window.addEventListener("load", executeTests); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_error.html b/toolkit/content/tests/widgets/test_videocontrols_error.html new file mode 100644 index 0000000000..af90a4672a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_error.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - Error</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + const video = document.getElementById("video"); + const statusOverlay = getElementWithinVideo(video, "statusOverlay"); + const statusIcon = getElementWithinVideo(video, "statusIcon"); + const statusLabelErrorNoSource = getElementWithinVideo(video, "errorNoSource"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + }); + + add_task(async function check_normal_status() { + await new Promise(resolve => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); + }); + + // Wait for the fade out transition to complete in case the throbber + // shows up on slower platforms. + await SimpleTest.promiseWaitForCondition(() => statusOverlay.hidden, + "statusOverlay should not present without error"); + + ok(!statusOverlay.hasAttribute("status"), "statusOverlay should not be showing a state message."); + isnot(statusIcon.getAttribute("type"), "error", "should not show error icon"); + }); + + add_task(async function invalid_source() { + const errorType = "errorNoSource"; + + await new Promise(resolve => { + video.src = "invalid_source.ogg"; + video.addEventListener("error", () => SimpleTest.executeSoon(resolve)); + }); + + ok(!statusOverlay.hidden, `statusOverlay should show when ${errorType}`); + is(statusOverlay.getAttribute("status"), errorType, `statusOverlay should have correct error state: ${errorType}`); + is(statusIcon.getAttribute("type"), "error", `should show error icon when ${errorType}`); + isnot(statusLabelErrorNoSource.getBoundingClientRect().height, 0, + "errorNoSource status label should be visible."); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_focus.html b/toolkit/content/tests/widgets/test_videocontrols_focus.html new file mode 100644 index 0000000000..0982947ffe --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_focus.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Focus</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +let video, controlBar, playButton; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}); + + // We must create the video after the keyboard-tab-to-all-controls pref is + // set. Otherwise, the tabindex won't be set correctly. + video = document.createElement("video"); + video.id = "video"; + video.controls = true; + video.preload = "auto"; + video.loop = true; + video.src = "video.ogg"; + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + const content = document.getElementById("content"); + content.append(video); + controlBar = getElementWithinVideo(video, "controlBar"); + playButton = getElementWithinVideo(video, "playButton"); + info("Waiting for video to load"); + // We must wait for loadeddata here, not loadedmetadata, as the first frame + // isn't shown until loadeddata occurs and the controls won't hide until the + // first frame is shown. + await BrowserTestUtils.waitForEvent(video, "loadeddata"); + + // Play and mouseout to hide the controls. + info("Playing video"); + const playing = BrowserTestUtils.waitForEvent(video, "play"); + video.play(); + await playing; + // controlBar.hidden returns true while the animation is happening. We use + // the controlbarchange event to know when it's fully hidden. Aside from + // avoiding waitForCondition, this is necessary to avoid racing with the + // animation. + const hidden = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + sendMouseEvent({type: "mouseout"}, controlBar); + info("Waiting for controls to hide"); + await hidden; +}); + +add_task(async function testShowControlsOnFocus() { + ok(controlBar.hidden, "Controls initially hidden"); + const shown = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + info("Focusing play button"); + playButton.focus(); + await shown; + ok(!controlBar.hidden, "Controls shown after focus"); + await BrowserTestUtils.waitForEvent(video, "controlbarchange"); + ok(controlBar.hidden, "Controls hidden after timeout"); +}); + +add_task(async function testCcMenuStaysVisible() { + ok(controlBar.hidden, "Controls initially hidden"); + const shown = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + info("Focusing CC button"); + const ccButton = getElementWithinVideo(video, "closedCaptionButton"); + ccButton.focus(); + await shown; + ok(!controlBar.hidden, "Controls shown after focus"); + // Checking this using an implementation detail is ugly, but there's no other + // way to do it without fragile timing. + const { widget } = window.windowGlobalChild.getActor("UAWidgets").widgets.get( + video); + ok(widget.impl.Utils._hideControlsTimeout, "Hide timeout set"); + const ttList = getElementWithinVideo(video, "textTrackListContainer"); + ok(ttList.hidden, "Text track list initially hidden"); + + synthesizeKey(" "); + ok(!ttList.hidden, "Text track list shown after space"); + ok( + !widget.impl.Utils._hideControlsTimeout, + "Hide timeout cleared (controls won't hide)" + ); + const ccOff = ttList.querySelector("button"); + ccOff.focus(); + synthesizeKey(" "); + ok(ttList.hidden, "Text track list hidden after activating Off button"); + ok(!controlBar.hidden, "Controls still shown"); + ok(widget.impl.Utils._hideControlsTimeout, "Hide timeout set"); + + await BrowserTestUtils.waitForEvent(video, "controlbarchange"); + ok(controlBar.hidden, "Controls hidden after timeout"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html new file mode 100644 index 0000000000..0a74b25609 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - iframe</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +<iframe id="ifr1"></iframe> +<iframe id="ifr2" allowfullscreen></iframe> +<iframe id="ifr1" allow="fullscreen 'none'"></iframe> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const testCases = []; + + function checkIframeFullscreenAvailable(ifr) { + let video; + + return () => new Promise(resolve => { + ifr.srcdoc = `<video id="video" controls preload="auto"></video>`; + ifr.addEventListener("load", resolve); + }).then(() => new Promise(resolve => { + video = ifr.contentDocument.getElementById("video"); + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve); + })).then(() => new Promise(resolve => { + const available = video.ownerDocument.fullscreenEnabled; + const controlBar = getElementWithinVideo(video, "controlBar"); + + is(controlBar.getAttribute("fullscreen-unavailable") == "true", !available, "The controlbar should have an attribute marking whether fullscreen is available that corresponds to if the iframe has the allowfullscreen attribute."); + resolve(); + })); + } + + function start() { + testCases.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function load() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + for (let iframe of document.querySelectorAll("iframe")) + testCases.push(checkIframeFullscreenAvailable(iframe)); + testCases.push(SimpleTest.finish); + + window.addEventListener("load", load); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..f3fdecc47f --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function runTest(event) { + info(true, "----- test #" + testnum + " -----"); + + switch (testnum) { + case 1: + is(event.type, "timeupdate", "checking event type"); + is(video.paused, false, "checking video play state"); + video.removeEventListener("timeupdate", runTest); + + // Click to toggle play/pause (now pausing) + synthesizeMouseAtCenter(video, {}, win); + break; + + case 2: + is(event.type, "pause", "checking event type"); + is(video.paused, true, "checking video play state"); + win.close(); + + SimpleTest.finish(); + break; + + default: + ok(false, "unexpected test #" + testnum + " w/ event " + event.type); + throw new Error(`unexpected test #${testnum} w/ event ${event.type}`); + } + + testnum++; +} + +SpecialPowers.pushPrefEnv({"set": [["javascript.enabled", false]]}, startTest); + +var testnum = 1; + +var video; +function loadevent(event) { + is(win.testExpando, undefined, "expando shouldn't exist because js is disabled"); + video = win.document.querySelector("video"); + // Other events expected by the test. + video.addEventListener("timeupdate", runTest); + video.addEventListener("pause", runTest); +} + +var win; +function startTest() { + const TEST_FILE = location.href.replace("test_videocontrols_jsdisabled.html", + "file_videocontrols_jsdisabled.html"); + win = window.open(TEST_FILE); + win.addEventListener("load", loadevent); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html new file mode 100644 index 0000000000..5b771fc745 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - KeyHandler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + const video = document.getElementById("video"); + + const playButton = getElementWithinVideo(video, "playButton"); + const scrubber = getElementWithinVideo(video, "scrubber"); + const volumeControl = getElementWithinVideo(video, "volumeControl"); + const muteButton = getElementWithinVideo(video, "muteButton"); + + // Setup video + tests.push(done => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}, done); + }, done => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", done); + }); + + // Bug 1350191, video should not seek while changing volume by + // pressing up/down arrow key after clicking the scrubber. + tests.push(done => { + video.addEventListener("play", done, { once: true }); + synthesizeMouseAtCenter(playButton, {}); + }, done => { + video.addEventListener("seeked", done, { once: true }); + synthesizeMouseAtCenter(scrubber, {}); + }, done => { + let counter = 0; + let keys = ["KEY_ArrowDown", "KEY_ArrowDown", "KEY_ArrowUp", "KEY_ArrowDown", "KEY_ArrowUp", "KEY_ArrowUp"]; + + const onSeeked = () => ok(false, "should not trigger seeked event"); + video.addEventListener("seeked", onSeeked); + const onVolumeChange = () => { + if (++counter === keys.length) { + ok(true, "change volume by up/down arrow key without trigger 'seeked' event"); + video.removeEventListener("seeked", onSeeked); + video.removeEventListener("volumechange", onVolumeChange); + done(); + } + + if (counter > keys.length) { + ok(false, "trigger too much volumechange events"); + } + }; + video.addEventListener("volumechange", onVolumeChange); + + for (let key of keys) { + synthesizeKey(key); + } + }); + + // However, if the scrubber is *focused* (e.g. by keyboard), it should handle + // up/down arrow keys. + tests.push(done => { + info("Focusing the scrubber"); + scrubber.focus(); + video.addEventListener("seeked", () => { + ok(true, "DownArrow seeked the video"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowDown"); + }, done => { + video.addEventListener("seeked", () => { + ok(true, "UpArrow seeked the video"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowUp"); + }); + + // Similarly, if the volume control is focused, left/right arrows should + // adjust the volume. + tests.push(done => { + info("Focusing the volume control"); + volumeControl.focus(); + video.addEventListener("volumechange", () => { + ok(true, "LeftArrow changed the volume"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowLeft"); + }, done => { + video.addEventListener("volumechange", () => { + ok(true, "RightArrow changed the volume"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowRight"); + }); + + // If something other than a button has focus, space should pause/play. + tests.push(done => { + ok(volumeControl.matches(":focus"), "Volume control still has focus"); + video.addEventListener("pause", () => { + ok(true, "Space paused the video"); + done(); + }, {once: true}); + synthesizeKey(" "); + }, done => { + video.addEventListener("play", () => { + ok(true, "Space played the video"); + done(); + }, {once: true}); + synthesizeKey(" "); + }); + + // If a button has focus, space should activate it, *not* pause/play. + tests.push(done => { + info("Focusing the mute button"); + muteButton.focus(); + const onPause = () => ok(false, "Shouldn't pause the video"); + video.addEventListener("pause", onPause); + let volChanges = 0; + const onVolChange = () => { + if (++volChanges == 2) { + ok(true, "Space twice muted then unmuted the video"); + video.removeEventListener("pause", onPause); + video.removeEventListener("volumechange", onVolChange); + done(); + } + }; + video.addEventListener("volumechange", onVolChange); + // Press space twice. The first time should mute, the second should unmute. + synthesizeKey(" "); + synthesizeKey(" "); + }); + + tests.push(SimpleTest.finish); + + window.addEventListener("load", executeTests); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html new file mode 100644 index 0000000000..9023512ab7 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var video = document.getElementById("video"); + +function startMediaLoad() { + // Kick off test once video has loaded, in its canplaythrough event handler. + video.src = "seek_with_sound.ogg"; + video.addEventListener("canplaythrough", runTest); +} + +function loadevent(event) { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); +} + +window.addEventListener("load", loadevent); + +function runTest() { + video.addEventListener("click", function() { + this.play(); + }); + ok(video.paused, "video should be paused initially"); + + new Promise(resolve => { + let timeupdates = 0; + video.addEventListener("timeupdate", function timeupdate() { + ok(!video.paused, "video should not get paused after clicking in middle"); + + if (++timeupdates == 3) { + video.removeEventListener("timeupdate", timeupdate); + resolve(); + } + }); + + synthesizeMouseAtCenter(video, {}, window); + }).then(function() { + new Promise(resolve => { + video.addEventListener("pause", function onpause() { + setTimeout(() => { + ok(video.paused, "video should still be paused 200ms after pause request"); + // When the video reaches the end of playback it is automatically paused. + // Check during the pause event that the video has not reachd the end + // of playback. + ok(!video.ended, "video should not have paused due to playback ending"); + resolve(); + }, 200); + }); + + synthesizeMouse(video, 10, video.clientHeight - 10, {}, window); + }).then(SimpleTest.finish); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html new file mode 100644 index 0000000000..b1d2ab9e74 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - https://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Initial scrubber position</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" mozNoDynamicControls preload="auto"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const video = document.getElementById("video"); + +add_task(async function setup() { + await new Promise(resolve => { + video.addEventListener("canplaythrough", resolve, {once: true}); + video.src = "seek_with_sound.ogg"; + }); + + // Check initial state upon load + is(video.paused, true, "checking video play state"); +}); + +add_task(function test_initial_scrubber_position() { + // When the controls are shown after the initial video frame, + // reflowedDimensions might not be set... + video.setAttribute("controls", "true"); + + // ... but we still want to ensure the initial scrubber position + // is reasonable. + const scrubber = getElementWithinVideo(video, "scrubber"); + ok(scrubber.max, "The max value should be set on the scrubber"); + is(parseInt(scrubber.value), 0, "The initial position should be 0"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html new file mode 100644 index 0000000000..9fbb6fbcb5 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - https://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Initial scrubber position when preload is turned off</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" mozNoDynamicControls controls="true" preload="none" src="seek_with_sound.ogg"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const video = document.getElementById("video"); + +add_task(function test_initial_scrubber_position() { + // Check initial state upon load + is(video.paused, true, "checking video play state"); + + const scrubber = getElementWithinVideo(video, "scrubber"); + ok(scrubber.max, "The max value should be set on the scrubber"); + is(parseInt(scrubber.value), 0, "The initial position should be 0"); +}); + +add_task(async function test_scrubber_after_manual_move() { + // Kick-start the video before trying to change the scrubber. + let loadedPromise = video.readyState == video.HAVE_ENOUGH_DATA ? + Promise.resolve() : + new Promise(r => { + video.addEventListener("canplaythrough", r, {once: true}); + }); + video.play(); + await loadedPromise; + video.pause(); + const scrubber = getElementWithinVideo(video, "scrubber"); + // Click the middle of the scrubber: + synthesizeMouseAtCenter(scrubber, {}); + // Expect that the progress updates, too: + + const progress = getElementWithinVideo(video, "progressBar"); + is( + // toFixed(2) takes care of rounding issues here. + (progress.value / progress.max).toFixed(2), + (scrubber.value / scrubber.max).toFixed(2), + "Should have updated progress bar." + ); +}); + +add_task(async function test_progress_and_scrubber_once_fullscreened() { + // loop to ensure we can always get 4 timeupdate events. + video.loop = true; + video.currentTime = video.duration / 2; + info("Setting max width"); + video.style.maxWidth = "200px"; + info( + "Current video progress = " + + (video.currentTime / video.duration).toFixed(2) + ); + // Wait for a flush so the scrubber has been hidden. + await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); + info("Hid progress and scrubber."); + // Then full screen. + await SpecialPowers.wrap(video).requestFullscreen(); + info("Gone into fullscreen."); + // Then wait for the video to play a bit (4 events is pretty arbitrary) + let updateCounter = 4; + let playABitPromise = new Promise(resolve => { + let handler = () => { + info("timeupdate event, counter left: " + updateCounter); + if (--updateCounter <= 0) { + video.removeEventListener("timeupdate", handler); + video.addEventListener("pause", resolve, { once: true }); + video.pause(); + } + }; + video.addEventListener("timeupdate", handler); + }); + video.play(); + await playABitPromise; + + const scrubber = getElementWithinVideo(video, "scrubber"); + let videoProgress = video.currentTime / video.duration; + let fuzzFactor = video.duration * 0.01; + info("Video progress: " + videoProgress.toFixed(3)); + let scrubberProgress = scrubber.value / scrubber.max; + info("Scrubber : " + scrubberProgress.toFixed(3)); + ok( + scrubberProgress <= videoProgress + fuzzFactor, + "Scrubber should match actual video point in time." + ); + ok( + scrubberProgress >= videoProgress - fuzzFactor, + "Scrubber should match actual video point in time." + ); + // Expect that the progress matches the scrubber: + const progress = getElementWithinVideo(video, "progressBar"); + let progressProgress = progress.value / progress.max; + info("Progress bar : " + progressProgress.toFixed(3)); + ok( + progressProgress <= videoProgress + fuzzFactor, + "Progress bar should match actual video point in time." + ); + ok( + progressProgress >= videoProgress - fuzzFactor, + "Progress bar should match actual video point in time." + ); + await SpecialPowers.wrap(document).exitFullscreen(); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_size.html b/toolkit/content/tests/widgets/test_videocontrols_size.html new file mode 100644 index 0000000000..559cc66e86 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_size.html @@ -0,0 +1,179 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - Size</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video controls preload="auto" width="480" height="320"></video> + <video controls preload="auto" width="320" height="320"></video> + <video controls preload="auto" width="280" height="320"></video> + <video controls preload="auto" width="240" height="320"></video> + <video controls preload="auto" width="180" height="320"></video> + <video controls preload="auto" width="120" height="320"></video> + <video controls preload="auto" width="60" height="320"></video> + <video controls preload="auto" width="48" height="320"></video> + <video controls preload="auto" width="20" height="320"></video> + + <video controls preload="auto" width="480" height="240"></video> + <video controls preload="auto" width="480" height="120"></video> + <video controls preload="auto" width="480" height="39"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const videoElems = [...document.getElementsByTagName("video")]; + const testCases = []; + + const isTouchControl = navigator.appVersion.includes("Android"); + + const buttonWidth = isTouchControl ? 40 : 30; + const minSrubberWidth = isTouchControl ? 64 : 48; + const minControlBarHeight = isTouchControl ? 52 : 40; + const minControlBarWidth = isTouchControl ? 58 : 48; + const minClickToPlaySize = isTouchControl ? 64 : 48; + + function getElementName(elem) { + return elem.getAttribute("anonid") || elem.getAttribute("class"); + } + + function testButton(btn) { + if (btn.hidden) return; + + const rect = btn.getBoundingClientRect(); + const name = getElementName(btn); + + is(rect.width, buttonWidth, `${name} should have correct width`); + is(rect.height, minControlBarHeight, `${name} should have correct height`); + } + + function testScrubber(scrubber) { + if (scrubber.hidden) return; + + const rect = scrubber.getBoundingClientRect(); + const name = getElementName(scrubber); + + ok(rect.width >= minSrubberWidth, `${name} should longer than ${minSrubberWidth}`); + } + + function testUI(video) { + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + + const videoRect = video.getBoundingClientRect(); + + const videoHeight = video.clientHeight; + const videoWidth = video.clientWidth; + + const videoSizeMsg = `size:${videoRect.width}x${videoRect.height} -`; + const controlBar = getElementWithinVideo(video, "controlBar"); + const playBtn = getElementWithinVideo(video, "playButton"); + const scrubber = getElementWithinVideo(video, "scrubberStack"); + const positionDurationBox = getElementWithinVideo(video, "positionDurationBox"); + const durationLabel = positionDurationBox.getElementsByTagName("span")[0]; + const muteBtn = getElementWithinVideo(video, "muteButton"); + const volumeStack = getElementWithinVideo(video, "volumeStack"); + const fullscreenBtn = getElementWithinVideo(video, "fullscreenButton"); + const clickToPlay = getElementWithinVideo(video, "clickToPlay"); + + + // Controls should show/hide according to the priority + const prioritizedControls = [ + playBtn, + muteBtn, + fullscreenBtn, + positionDurationBox, + scrubber, + durationLabel, + volumeStack, + ]; + + let stopAppend = false; + prioritizedControls.forEach(control => { + is(control.hidden, stopAppend = stopAppend || control.hidden, + `${videoSizeMsg} ${getElementName(control)} should ${stopAppend ? "hide" : "show"}`); + }); + + + // All controls should fit in control bar container + const controls = [ + playBtn, + scrubber, + positionDurationBox, + muteBtn, + volumeStack, + fullscreenBtn, + ]; + + let widthSum = 0; + controls.forEach(control => { + widthSum += control.clientWidth; + }); + ok(videoWidth >= widthSum, + `${videoSizeMsg} controlBar fit in video's width`); + + + // Control bar should show/hide according to video's dimensions + const shouldHideControlBar = videoHeight <= minControlBarHeight || + videoWidth < minControlBarWidth; + is(controlBar.hidden, shouldHideControlBar, `${videoSizeMsg} controlBar show/hide`); + + if (!shouldHideControlBar) { + is(controlBar.clientWidth, videoWidth, `control bar width should equal to video width`); + + // Check all controls' dimensions + testButton(playBtn); + testButton(muteBtn); + testButton(fullscreenBtn); + testScrubber(scrubber); + testScrubber(volumeStack); + } + + + // ClickToPlay button should show if min size can fit in + const shouldHideClickToPlay = videoWidth <= minClickToPlaySize || + (videoHeight - minClickToPlaySize) / 2 <= minControlBarHeight; + is(clickToPlay.hidden, shouldHideClickToPlay, `${videoSizeMsg} clickToPlay show/hide`); + } + + + testCases.push(() => Promise.all(videoElems.map(video => new Promise(resolve => { + video.addEventListener("loadedmetadata", resolve); + video.src = "seek_with_sound.ogg"; + })))); + + videoElems.forEach(video => { + testCases.push(() => new Promise(resolve => { + SimpleTest.executeSoon(() => { + testUI(video); + resolve(); + }); + })); + }); + + function executeTasks(tasks) { + return tasks.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function start() { + executeTasks(testCases).then(SimpleTest.finish); + } + + function loadevent() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + window.addEventListener("load", loadevent); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_standalone.html b/toolkit/content/tests/widgets/test_videocontrols_standalone.html new file mode 100644 index 0000000000..14208923dd --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_standalone.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/NativeKeyCodes.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + SimpleTest.expectAssertions(0, 1); + +const videoWidth = 320; +const videoHeight = 240; + +function getMediaElement(aWindow) { + return aWindow.document.getElementsByTagName("video")[0]; +} + +var popup = window.open("seek_with_sound.ogg"); +popup.addEventListener("load", function() { + var video = getMediaElement(popup); + + is(popup.document.activeElement, video, "Document should load with focus moved to the video element."); + + if (!video.paused) { + runTestVideo(video); + } else { + video.addEventListener("play", function() { + runTestVideo(video); + }, {once: true}); + } +}, {once: true}); + +function runTestVideo(aVideo) { + var condition = function() { + var boundingRect = aVideo.getBoundingClientRect(); + return boundingRect.width == videoWidth && + boundingRect.height == videoHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aVideo.getBoundingClientRect(); + is(boundingRect.width, videoWidth, "Width of the video should match expectation"); + is(boundingRect.height, videoHeight, "Height of video should match expectation"); + popup.close(); + runTestAudioPre(); + }, "The media element should eventually be resized to match the intrinsic size of the video."); +} + +function runTestAudioPre() { + popup = window.open("audio.ogg"); + popup.addEventListener("load", function() { + var audio = getMediaElement(popup); + + is(popup.document.activeElement, audio, "Document should load with focus moved to the video element."); + + if (!audio.paused) { + runTestAudio(audio); + } else { + audio.addEventListener("play", function() { + runTestAudio(audio); + }, {once: true}); + } + }, {once: true}); +} + +function runTestAudio(aAudio) { + info("User agent (help diagnose bug #943556): " + navigator.userAgent); + var isAndroid = navigator.userAgent.includes("Android"); + var expectedHeight = isAndroid ? 103 : 40; + var condition = function() { + var boundingRect = aAudio.getBoundingClientRect(); + return boundingRect.height == expectedHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aAudio.getBoundingClientRect(); + is(boundingRect.height, expectedHeight, + "Height of audio element should be " + expectedHeight + ", which is equal to the controls bar."); + ok(!aAudio.paused, "Should be playing"); + testPauseByKeyboard(aAudio); + }, "The media element should eventually be resized to match the height of the audio controls."); +} + +function testPauseByKeyboard(aAudio) { + aAudio.addEventListener("pause", function() { + afterKeyPause(aAudio); + }, {once: true}); + // Press spacebar, which means play/pause. + synthesizeKey(" ", {}, popup); +} + +function afterKeyPause(aAudio) { + ok(true, "successfully caused audio to pause"); + waitForCondition(function() { + return aAudio.paused; + }, + function() { + // Click outside of the controls area. (Hopefully this has no effect.) + synthesizeMouseAtPoint(5, 5, { type: 'mousedown' }, popup); + synthesizeMouseAtPoint(5, 5, { type: 'mouseup' }, popup); + setTimeout(function() { + testPlayByKeyboard(aAudio); + }, 0); + }); +} + +function testPlayByKeyboard(aAudio) { + aAudio.addEventListener("play", function() { + ok(true, "successfully caused audio to play"); + finishAudio(); + }, {once: true}); + // Press spacebar, which means play/pause. + synthesizeKey(" ", {}, popup); +} + +function finishAudio() { + popup.close(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_direction.html b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html new file mode 100644 index 0000000000..45cf7f6363 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-1a.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1b.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1c.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1d.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1e.html", ref: "videocontrols_direction-1-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html new file mode 100644 index 0000000000..bfc8018466 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + const video = document.getElementById("video"); + const muteButton = getElementWithinVideo(video, "muteButton"); + const volumeStack = getElementWithinVideo(video, "volumeStack"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + video.src = "video.ogg"; + video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); + }); + }); + + add_task(async function mute_button_icon() { + is(muteButton.getAttribute("noAudio"), "true"); + ok(muteButton.hasAttribute("disabled"), "Mute button should be disabled"); + + if (volumeStack) { + ok(volumeStack.hidden); + } + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_vtt.html b/toolkit/content/tests/widgets/test_videocontrols_vtt.html new file mode 100644 index 0000000000..2f8d70f35a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - VTT</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const video = document.getElementById("video"); + const ccBtn = getElementWithinVideo(video, "closedCaptionButton"); + const ttList = getElementWithinVideo(video, "textTrackList"); + const ttListContainer = getElementWithinVideo(video, "textTrackListContainer"); + + add_task(async function wait_for_media_ready() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve); + }); + }); + + add_task(async function check_inital_state() { + ok(ccBtn.hidden, "CC button should hide"); + }); + + add_task(async function check_unsupported_type_added() { + video.addTextTrack("descriptions", "English", "en"); + video.addTextTrack("chapters", "English", "en"); + video.addTextTrack("metadata", "English", "en"); + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hidden, "CC button should hide if no supported tracks provided"); + }); + + add_task(async function check_cc_button_present() { + const sub = video.addTextTrack("subtitles", "English", "en"); + sub.mode = "disabled"; + + await new Promise(SimpleTest.executeSoon); + ok(!ccBtn.hidden, "CC button should show"); + is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled"); + }); + + add_task(async function check_cc_button_be_enabled() { + const subtitle = video.addTextTrack("subtitles", "English", "en"); + subtitle.mode = "showing"; + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + subtitle.mode = "disabled"; + }); + + add_task(async function check_cpations_type() { + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + }); + + add_task(async function check_track_ui_state() { + synthesizeMouseAtCenter(ccBtn, {}); + + await new Promise(SimpleTest.executeSoon); + ok(!ttListContainer.hidden, "Texttrack menu should show up"); + ok(ttList.lastChild.getAttribute("aria-checked") === "true", "The last added item should be highlighted"); + }); + + add_task(async function check_select_texttrack() { + const tt = ttList.children[1]; + + ok(tt.getAttribute("aria-checked") === "false", "Item should be off before click"); + synthesizeMouseAtCenter(tt, {}); + + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(tt.getAttribute("aria-checked") === "true", "Selected item should be enabled"); + ok(ttListContainer.hidden, "Should hide texttrack menu once clicked on an item"); + }); + + add_task(async function check_change_texttrack_mode() { + const tts = [...video.textTracks]; + + tts.forEach(tt => tt.mode = "hidden"); + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(!ccBtn.hasAttribute("enabled"), "CC button should be disabled"); + + // enable the last text track. + tts[tts.length - 1].mode = "showing"; + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + ok(ttList.lastChild.getAttribute("aria-checked") === "true", "The last item should be highlighted"); + }); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/tree_shared.js b/toolkit/content/tests/widgets/tree_shared.js new file mode 100644 index 0000000000..ba52bf828e --- /dev/null +++ b/toolkit/content/tests/widgets/tree_shared.js @@ -0,0 +1,2184 @@ +// This file expects the following globals to be defined at various times. +/* globals getCustomTreeViewCellInfo */ + +// This files relies on these specific Chrome/XBL globals +/* globals TreeColumns, TreeColumn */ + +var columns_simpletree = [ + { name: "name", label: "Name", key: true, properties: "one two" }, + { name: "address", label: "Address" }, +]; + +var columns_hiertree = [ + { + name: "name", + label: "Name", + primary: true, + key: true, + properties: "one two", + }, + { name: "address", label: "Address" }, + { name: "planet", label: "Planet" }, + { name: "gender", label: "Gender", cycler: true }, +]; + +// XXXndeakin still to add some tests for: +// cycler columns, checkbox cells + +// this test function expects a tree to have 8 rows in it when it isn't +// expanded. The tree should only display four rows at a time. If editable, +// the cell at row 1 and column 0 must be editable, and the cell at row 2 and +// column 1 must not be editable. +async function testtag_tree( + treeid, + treerowinfoid, + seltype, + columnstype, + testid +) { + // Stop keystrokes that aren't handled by the tree from leaking out and + // scrolling the main Mochitests window! + function preventDefault(event) { + event.preventDefault(); + } + document.addEventListener("keypress", preventDefault); + + var multiple = seltype == "multiple"; + + var tree = document.getElementById(treeid); + var treerowinfo = document.getElementById(treerowinfoid); + var rowInfo; + if (testid == "tree view") { + rowInfo = getCustomTreeViewCellInfo(); + } else { + rowInfo = convertDOMtoTreeRowInfo(treerowinfo, 0, { value: -1 }); + } + var columnInfo = + columnstype == "simple" ? columns_simpletree : columns_hiertree; + + is(tree.selType, seltype == "multiple" ? "" : seltype, testid + " seltype"); + + // note: the functions below should be in this order due to changes made in later tests + + await testtag_tree_treecolpicker(tree, columnInfo, testid); + testtag_tree_columns(tree, columnInfo, testid); + testtag_tree_TreeSelection(tree, testid, multiple); + testtag_tree_TreeSelection_UI(tree, testid, multiple); + testtag_tree_TreeView(tree, testid, rowInfo); + + is(tree.editable, false, "tree should not be editable"); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow"); + is(tree.editingColumn, null, testid + " initial editingColumn"); + + testtag_tree_UI_editing(tree, testid, rowInfo); + + is( + tree.editable, + false, + "tree should not be editable after testtag_tree_UI_editing" + ); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow (continued)"); + is(tree.editingColumn, null, testid + " initial editingColumn (continued)"); + + var ecolumn = tree.columns[0]; + ok( + !tree.startEditing(1, ecolumn), + "non-editable trees shouldn't start editing" + ); + is( + tree.editingRow, + -1, + testid + " failed startEditing shouldn't set editingRow" + ); + is( + tree.editingColumn, + null, + testid + " failed startEditing shouldn't set editingColumn" + ); + + tree.editable = true; + + ok(tree.startEditing(1, ecolumn), "startEditing should have returned true"); + is(tree.editingRow, 1, testid + " startEditing editingRow"); + is(tree.editingColumn, ecolumn, testid + " startEditing editingColumn"); + is( + tree.getAttribute("editing"), + "true", + testid + " startEditing editing attribute" + ); + + tree.stopEditing(true); + is(tree.editingRow, -1, testid + " stopEditing editingRow"); + is(tree.editingColumn, null, testid + " stopEditing editingColumn"); + is( + tree.hasAttribute("editing"), + false, + testid + " stopEditing editing attribute" + ); + + tree.startEditing(-1, ecolumn); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing -1 editingRow" + ); + tree.startEditing(15, ecolumn); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing 15 editingRow" + ); + tree.startEditing(1, null); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing null column editingRow" + ); + tree.startEditing(2, tree.columns[1]); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing non editable cell editingRow" + ); + + tree.startEditing(1, ecolumn); + var inputField = tree.inputField; + is(inputField.localName, "input", testid + "inputField"); + inputField.value = "Changed Value"; + tree.stopEditing(true); + is( + tree.view.getCellText(1, ecolumn), + "Changed Value", + testid + "edit cell accept" + ); + + // this cell can be edited, but stopEditing(false) means don't accept the change. + tree.startEditing(1, ecolumn); + inputField.value = "Second Value"; + tree.stopEditing(false); + is( + tree.view.getCellText(1, ecolumn), + "Changed Value", + testid + "edit cell no accept" + ); + + tree.editable = false; + + // do the sorting tests last as it will cause the rows to rearrange + // skip them for the custom tree view + if (testid != "tree view") { + testtag_tree_TreeView_rows_sort(tree, testid, rowInfo); + } + + testtag_tree_wheel(tree); + + document.removeEventListener("keypress", preventDefault); + + SimpleTest.finish(); +} + +async function testtag_tree_treecolpicker(tree, expectedColumns, testid) { + testid += " "; + + async function showAndHideTreecolpicker() { + let treecolpicker = tree.querySelector("treecolpicker"); + let treecolpickerMenupopup = treecolpicker.querySelector("menupopup"); + await new Promise(resolve => { + treecolpickerMenupopup.addEventListener("popupshown", resolve, { + once: true, + }); + treecolpicker.querySelector("button").click(); + }); + let menuitems = treecolpicker.querySelectorAll("menuitem"); + // Ignore the last "Restore Column Order" menu in the count: + is( + menuitems.length - 1, + expectedColumns.length, + testid + "Same number of columns" + ); + for (var c = 0; c < expectedColumns.length; c++) { + is( + menuitems[c].textContent, + expectedColumns[c].label, + testid + "treecolpicker menu matches" + ); + ok( + !menuitems[c].querySelector("label").hidden, + testid + "label not hidden" + ); + } + await new Promise(resolve => { + treecolpickerMenupopup.addEventListener("popuphidden", resolve, { + once: true, + }); + treecolpickerMenupopup.hidePopup(); + }); + } + + // Regression test for Bug 1549931 (menuitem content being hidden upon second open) + await showAndHideTreecolpicker(); + await showAndHideTreecolpicker(); +} + +function testtag_tree_columns(tree, expectedColumns, testid) { + testid += " "; + + var columns = tree.columns; + + is( + TreeColumns.isInstance(columns), + true, + testid + "columns is a TreeColumns" + ); + is(columns.count, expectedColumns.length, testid + "TreeColumns count"); + is(columns.length, expectedColumns.length, testid + "TreeColumns length"); + + var treecols = tree.getElementsByTagName("treecols")[0]; + var treecol = treecols.getElementsByTagName("treecol"); + + var x = 0; + var primary = null, + sorted = null, + key = null; + for (var c = 0; c < expectedColumns.length; c++) { + var adjtestid = testid + " column " + c + " "; + var column = columns[c]; + var expectedColumn = expectedColumns[c]; + is(columns.getColumnAt(c), column, adjtestid + "getColumnAt"); + is( + columns.getNamedColumn(expectedColumn.name), + column, + adjtestid + "getNamedColumn" + ); + is(columns.getColumnFor(treecol[c]), column, adjtestid + "getColumnFor"); + if (expectedColumn.primary) { + primary = column; + } + if (expectedColumn.sorted) { + sorted = column; + } + if (expectedColumn.key) { + key = column; + } + + // XXXndeakin on Windows and Linux, some columns are one pixel to the + // left of where they should be. Could just be a rounding issue. + var adj = 1; + is( + column.x + adj >= x, + true, + adjtestid + + "position is after last column " + + column.x + + "," + + column.width + + "," + + x + ); + is(column.width > 0, true, adjtestid + "width is greater than 0"); + x = column.x + column.width; + + // now check the TreeColumn properties + is(TreeColumn.isInstance(column), true, adjtestid + "is a TreeColumn"); + is(column.element, treecol[c], adjtestid + "element is treecol"); + is(column.columns, columns, adjtestid + "columns is TreeColumns"); + is(column.id, expectedColumn.name, adjtestid + "name"); + is(column.index, c, adjtestid + "index"); + is(column.primary, primary == column, adjtestid + "column is primary"); + + is( + column.cycler, + "cycler" in expectedColumn && expectedColumn.cycler, + adjtestid + "column is cycler" + ); + is( + column.editable, + "editable" in expectedColumn && expectedColumn.editable, + adjtestid + "column is editable" + ); + + is( + column.type, + "type" in expectedColumn ? expectedColumn.type : 1, + adjtestid + "type" + ); + + is( + column.getPrevious(), + c > 0 ? columns[c - 1] : null, + adjtestid + "getPrevious" + ); + is( + column.getNext(), + c < columns.length - 1 ? columns[c + 1] : null, + adjtestid + "getNext" + ); + + // check the view's getColumnProperties method + var properties = tree.view.getColumnProperties(column); + var expectedProperties = expectedColumn.properties; + is( + properties, + expectedProperties ? expectedProperties : "", + adjtestid + "getColumnProperties" + ); + } + + is(columns.getFirstColumn(), columns[0], testid + "getFirstColumn"); + is( + columns.getLastColumn(), + columns[columns.length - 1], + testid + "getLastColumn" + ); + is(columns.getPrimaryColumn(), primary, testid + "getPrimaryColumn"); + is(columns.getSortedColumn(), sorted, testid + "getSortedColumn"); + is(columns.getKeyColumn(), key, testid + "getKeyColumn"); + + is(columns.getColumnAt(-1), null, testid + "getColumnAt under"); + is(columns.getColumnAt(columns.length), null, testid + "getColumnAt over"); + is(columns.getNamedColumn(""), null, testid + "getNamedColumn null"); + is( + columns.getNamedColumn("unknown"), + null, + testid + "getNamedColumn unknown" + ); + is(columns.getColumnFor(null), null, testid + "getColumnFor null"); + is(columns.getColumnFor(tree), null, testid + "getColumnFor other"); +} + +function testtag_tree_TreeSelection(tree, testid, multiple) { + testid += " selection "; + + var selection = tree.view.selection; + is( + selection instanceof Ci.nsITreeSelection, + true, + testid + "selection is a TreeSelection" + ); + is(selection.single, !multiple, testid + "single"); + + testtag_tree_TreeSelection_State(tree, testid + "initial", -1, []); + is(selection.shiftSelectPivot, -1, testid + "initial shiftSelectPivot"); + + selection.currentIndex = 2; + testtag_tree_TreeSelection_State(tree, testid + "set currentIndex", 2, []); + tree.currentIndex = 3; + testtag_tree_TreeSelection_State( + tree, + testid + "set tree.currentIndex", + 3, + [] + ); + + // test the select() method, which should deselect all rows and select + // a single row + selection.select(1); + testtag_tree_TreeSelection_State(tree, testid + "select 1", 1, [1]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select 2", 3, [3]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select same", 3, [3]); + + selection.currentIndex = 1; + testtag_tree_TreeSelection_State( + tree, + testid + "set currentIndex with single selection", + 1, + [3] + ); + + tree.currentIndex = 2; + testtag_tree_TreeSelection_State( + tree, + testid + "set tree.currentIndex with single selection", + 2, + [3] + ); + + // check the toggleSelect method. In single selection mode, it only toggles on when + // there isn't currently a selection. + selection.toggleSelect(2); + testtag_tree_TreeSelection_State( + tree, + testid + "toggleSelect 1", + 2, + multiple ? [2, 3] : [3] + ); + selection.toggleSelect(2); + selection.toggleSelect(3); + testtag_tree_TreeSelection_State(tree, testid + "toggleSelect 2", 3, []); + + // the current index doesn't change after a selectAll, so it should still be set to 1 + // selectAll has no effect on single selection trees + selection.currentIndex = 1; + selection.selectAll(); + testtag_tree_TreeSelection_State( + tree, + testid + "selectAll 1", + 1, + multiple ? [0, 1, 2, 3, 4, 5, 6, 7] : [] + ); + selection.toggleSelect(2); + testtag_tree_TreeSelection_State( + tree, + testid + "toggleSelect after selectAll", + 2, + multiple ? [0, 1, 3, 4, 5, 6, 7] : [2] + ); + selection.clearSelection(); + testtag_tree_TreeSelection_State(tree, testid + "clearSelection", 2, []); + selection.toggleSelect(3); + selection.toggleSelect(1); + if (multiple) { + selection.selectAll(); + testtag_tree_TreeSelection_State( + tree, + testid + "selectAll 2", + 1, + [0, 1, 2, 3, 4, 5, 6, 7] + ); + } + selection.currentIndex = 2; + selection.clearSelection(); + testtag_tree_TreeSelection_State( + tree, + testid + "clearSelection after selectAll", + 2, + [] + ); + + is(selection.shiftSelectPivot, -1, testid + "shiftSelectPivot set to -1"); + + // rangedSelect and clearRange set the currentIndex to the endIndex. The + // shiftSelectPivot property will be set to startIndex. + selection.rangedSelect(1, 3, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect no augment", + multiple ? 3 : 2, + multiple ? [1, 2, 3] : [] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "shiftSelectPivot after rangedSelect no augment" + ); + if (multiple) { + selection.select(1); + selection.rangedSelect(0, 2, true); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect augment", + 2, + [0, 1, 2] + ); + is( + selection.shiftSelectPivot, + 0, + testid + "shiftSelectPivot after rangedSelect augment" + ); + + selection.clearRange(1, 3); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 3, [ + 0, + ]); + + // check that rangedSelect can take a start value higher than end + selection.rangedSelect(3, 1, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect reverse", + 1, + [1, 2, 3] + ); + is( + selection.shiftSelectPivot, + 3, + testid + "shiftSelectPivot after rangedSelect reverse" + ); + + // check that setting the current index doesn't change the selection + selection.currentIndex = 0; + testtag_tree_TreeSelection_State( + tree, + testid + "currentIndex with range selection", + 0, + [1, 2, 3] + ); + } + + // both values of rangedSelect may be the same + selection.rangedSelect(2, 2, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect one row", 2, [ + 2, + ]); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot after selecting one row" + ); + + if (multiple) { + selection.rangedSelect(2, 3, true); + + // a start index of -1 means from the last point + selection.rangedSelect(-1, 0, true); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect -1 existing selection", + 0, + [0, 1, 2, 3] + ); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot after -1 existing selection" + ); + + selection.currentIndex = 2; + selection.rangedSelect(-1, 0, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect -1 from currentIndex", + 0, + [0, 1, 2] + ); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot -1 from currentIndex" + ); + } + + // XXXndeakin need to test out of range values but these don't work properly + /* + selection.select(-1); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment -1", -1, []); + + selection.select(8); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment 8", 3, [0]); +*/ +} + +function testtag_tree_TreeSelection_UI(tree, testid, multiple) { + testid += " selection UI "; + + var selection = tree.view.selection; + selection.clearSelection(); + selection.currentIndex = 0; + tree.focus(); + + var keydownFired = 0; + var keypressFired = 0; + function keydownListener(event) { + keydownFired++; + } + function keypressListener(event) { + keypressFired++; + } + + // check that cursor up and down keys navigate up and down + // select event fires after a delay so don't expect it. The reason it fires after a delay + // is so that cursor navigation allows quicking skimming over a set of items without + // actually firing events in-between, improving performance. The select event will only + // be fired on the row where the cursor stops. + window.addEventListener("keydown", keydownListener); + window.addEventListener("keypress", keypressListener); + + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down"); + testtag_tree_TreeSelection_State(tree, testid + "key down", 1, [1], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up"); + testtag_tree_TreeSelection_State(tree, testid + "key up", 0, [0], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up at start"); + testtag_tree_TreeSelection_State(tree, testid + "key up at start", 0, [0], 0); + + // pressing down while the last row is selected should not fire a select event, + // as the selection won't have changed. Also the view is not scrolled in this case. + selection.select(7); + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down at end"); + testtag_tree_TreeSelection_State(tree, testid + "key down at end", 7, [7], 0); + + // pressing keys while at the edge of the visible rows should scroll the list + tree.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up with scroll"); + is(tree.getFirstVisibleRow(), 3, testid + "key up with scroll"); + + tree.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent( + "VK_DOWN", + {}, + tree, + "!select", + "key down with scroll" + ); + is(tree.getFirstVisibleRow(), 1, testid + "key down with scroll"); + + // accel key and cursor movement adjust currentIndex but should not change + // the selection. In single selection mode, the selection will not change, + // but instead will just scroll up or down a line + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent( + "VK_DOWN", + { accelKey: true }, + tree, + "!select", + "key down with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key down with accel", + multiple ? 2 : 1, + [1] + ); + if (!multiple) { + is(tree.getFirstVisibleRow(), 1, testid + "key down with accel and scroll"); + } + + tree.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent( + "VK_UP", + { accelKey: true }, + tree, + "!select", + "key up with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key up with accel", + multiple ? 3 : 4, + [4] + ); + if (!multiple) { + is(tree.getFirstVisibleRow(), 3, testid + "key up with accel and scroll"); + } + + // do this three times, one for each state of pageUpOrDownMovesSelection, + // and then once with the accel key pressed + for (let t = 0; t < 3; t++) { + let testidmod = ""; + if (t == 2) { + testidmod = " with accel"; + } else if (t == 1) { + testidmod = " rev"; + } + var keymod = t == 2 ? { accelKey: true } : {}; + + var moveselection = tree.pageUpOrDownMovesSelection; + if (t == 2) { + moveselection = !moveselection; + } + + tree.scrollToRow(4); + selection.currentIndex = 6; + selection.select(6); + var expected = moveselection ? 4 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up" + testidmod, + expected, + [expected], + moveselection ? 4 : 0 + ); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up again" + testidmod, + expected, + [expected], + 0 + ); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up at start" + testidmod, + expected, + [expected], + 0 + ); + + tree.scrollToRow(0); + selection.currentIndex = 1; + selection.select(1); + expected = moveselection ? 3 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down" + testidmod, + expected, + [expected], + moveselection ? 0 : 4 + ); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down again" + testidmod, + expected, + [expected], + 4 + ); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down at start" + testidmod, + expected, + [expected], + 4 + ); + + if (t < 2) { + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + } + + tree.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent("VK_HOME", {}, tree, "!select", "key home"); + testtag_tree_TreeSelection_State(tree, testid + "key home", 0, [0], 0); + + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent("VK_END", {}, tree, "!select", "key end"); + testtag_tree_TreeSelection_State(tree, testid + "key end", 7, [7], 4); + + // in single selection mode, the selection doesn't change in this case + tree.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent( + "VK_HOME", + { accelKey: true }, + tree, + "!select", + "key home with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key home with accel", + multiple ? 0 : 6, + [6], + 0 + ); + + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent( + "VK_END", + { accelKey: true }, + tree, + "!select", + "key end with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key end with accel", + multiple ? 7 : 1, + [1], + 4 + ); + + // next, test cursor navigation with selection. Here the select event will be fired + selection.select(1); + var eventExpected = multiple ? "select" : "!select"; + synthesizeKeyExpectEvent( + "VK_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift down to select" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift down to select", + multiple ? 2 : 1, + multiple ? [1, 2] : [1] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "key shift down to select shiftSelectPivot" + ); + synthesizeKeyExpectEvent( + "VK_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift up to unselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift up to unselect", + 1, + [1] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "key shift up to unselect shiftSelectPivot" + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_UP", + { shiftKey: true }, + tree, + "select", + "key shift up to select" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift up to select", + 0, + [0, 1] + ); + is( + selection.shiftSelectPivot, + 1, + testid + "key shift up to select shiftSelectPivot" + ); + synthesizeKeyExpectEvent( + "VK_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift down to unselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift down to unselect", + 1, + [1] + ); + is( + selection.shiftSelectPivot, + 1, + testid + "key shift down to unselect shiftSelectPivot" + ); + } + + // do this twice, one for each state of pageUpOrDownMovesSelection, however + // when selecting with the shift key, pageUpOrDownMovesSelection is ignored + // and the selection always changes + var lastidx = tree.view.rowCount - 1; + for (let t = 0; t < 2; t++) { + let testidmod = t == 0 ? "" : " rev"; + + // If the top or bottom visible row is the current row, pressing shift and + // page down / page up selects one page up or one page down. Otherwise, the + // selection is made to the top or bottom of the visible area. + tree.scrollToRow(lastidx - 3); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift page up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up" + testidmod, + multiple ? 4 : 6, + multiple ? [4, 5, 6] : [6] + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "select", + "key shift page up again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up again" + testidmod, + 0, + [0, 1, 2, 3, 4, 5, 6] + ); + // no change in the selection, so no select event should be fired + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "!select", + "key shift page up at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up at start" + testidmod, + 0, + [0, 1, 2, 3, 4, 5, 6] + ); + // deselect by paging down again + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift page down deselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down deselect" + testidmod, + 3, + [3, 4, 5, 6] + ); + } + + tree.scrollToRow(1); + selection.currentIndex = 2; + selection.select(2); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift page down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down" + testidmod, + multiple ? 4 : 2, + multiple ? [2, 3, 4] : [2] + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift page down again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down again" + testidmod, + 7, + [2, 3, 4, 5, 6, 7] + ); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "!select", + "key shift page down at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down at start" + testidmod, + 7, + [2, 3, 4, 5, 6, 7] + ); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "select", + "key shift page up deselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up deselect" + testidmod, + 4, + [2, 3, 4] + ); + } + + // test when page down / page up is pressed when the view is scrolled such + // that the selection is not visible + if (multiple) { + tree.scrollToRow(3); + selection.currentIndex = 1; + selection.select(1); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift page down with view scrolled down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down with view scrolled down" + testidmod, + 6, + [1, 2, 3, 4, 5, 6], + 3 + ); + + tree.scrollToRow(2); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift page up with view scrolled up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up with view scrolled up" + testidmod, + 2, + [2, 3, 4, 5, 6], + 2 + ); + + tree.scrollToRow(2); + selection.currentIndex = 0; + selection.select(0); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "!select", + "key shift page up at start with view scrolled down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + + "key shift page up at start with view scrolled down" + + testidmod, + 0, + [0], + 0 + ); + + tree.scrollToRow(0); + selection.currentIndex = 7; + selection.select(7); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "!select", + "key shift page down at end with view scrolled up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down at end with view scrolled up" + testidmod, + 7, + [7], + 4 + ); + } + + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + + tree.scrollToRow(4); + selection.select(5); + synthesizeKeyExpectEvent( + "VK_HOME", + { shiftKey: true }, + tree, + eventExpected, + "key shift home" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift home", + multiple ? 0 : 5, + multiple ? [0, 1, 2, 3, 4, 5] : [5], + multiple ? 0 : 4 + ); + + tree.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent( + "VK_END", + { shiftKey: true }, + tree, + eventExpected, + "key shift end" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift end", + multiple ? 7 : 3, + multiple ? [3, 4, 5, 6, 7] : [3], + multiple ? 4 : 0 + ); + + // pressing space selects a row, pressing accel + space unselects a row + selection.select(2); + selection.currentIndex = 4; + synthesizeKeyExpectEvent(" ", {}, tree, "select", "key space on"); + // in single selection mode, space shouldn't do anything + testtag_tree_TreeSelection_State( + tree, + testid + "key space on", + 4, + multiple ? [2, 4] : [2] + ); + + if (multiple) { + synthesizeKeyExpectEvent( + " ", + { accelKey: true }, + tree, + "select", + "key space off" + ); + testtag_tree_TreeSelection_State(tree, testid + "key space off", 4, [2]); + } + + // check that clicking on a row selects it + tree.scrollToRow(0); + selection.select(2); + selection.currentIndex = 2; + if (0) { + // XXXndeakin disable these tests for now + mouseOnCell(tree, 1, tree.columns[1], "mouse on row"); + testtag_tree_TreeSelection_State( + tree, + testid + "mouse on row", + 1, + [1], + 0, + null + ); + } + + // restore the scroll position to the start of the page + sendKey("HOME"); + + window.removeEventListener("keydown", keydownListener); + window.removeEventListener("keypress", keypressListener); + is(keydownFired, multiple ? 63 : 40, "keydown event wasn't fired properly"); + is(keypressFired, multiple ? 2 : 1, "keypress event wasn't fired properly"); +} + +function testtag_tree_UI_editing(tree, testid, rowInfo) { + testid += " editing UI "; + + // check editing UI + var ecolumn = tree.columns[0]; + var rowIndex = 2; + + // temporary make the tree editable to test mouse double click + var wasEditable = tree.editable; + if (!wasEditable) { + tree.editable = true; + } + + // if this is a container save its current open status + var row = rowInfo.rows[rowIndex]; + var wasOpen = null; + if (tree.view.isContainer(row)) { + wasOpen = tree.view.isContainerOpen(row); + } + + mouseDblClickOnCell(tree, rowIndex, ecolumn, testid + "edit on double click"); + is(tree.editingColumn, ecolumn, testid + "editing column"); + is(tree.editingRow, rowIndex, testid + "editing row"); + + // ensure that we don't expand an expandable container on edit + if (wasOpen != null) { + is( + tree.view.isContainerOpen(row), + wasOpen, + testid + "opened container node on edit" + ); + } + + // ensure to restore editable attribute + if (!wasEditable) { + tree.editable = false; + } + + var ci = tree.currentIndex; + + // cursor navigation should not change the selection while editing + var testKey = function (key) { + synthesizeKeyExpectEvent( + key, + {}, + tree, + "!select", + "key " + key + " with editing" + ); + is( + tree.editingRow == rowIndex && + tree.editingColumn == ecolumn && + tree.currentIndex == ci, + true, + testid + "key " + key + " while editing" + ); + }; + + testKey("VK_DOWN"); + testKey("VK_UP"); + testKey("VK_PAGE_DOWN"); + testKey("VK_PAGE_UP"); + testKey("VK_HOME"); + testKey("VK_END"); + + // XXXndeakin figure out how to send characters to the textbox + // inputField.inputField.focus() + // synthesizeKeyExpectEvent(inputField.inputField, "b", null, ""); + // tree.stopEditing(true); + // is(tree.view.getCellText(0, ecolumn), "b", testid + "edit cell"); + + // Restore initial state. + tree.stopEditing(false); +} + +function testtag_tree_TreeView(tree, testid, rowInfo) { + testid += " view "; + + var columns = tree.columns; + var view = tree.view; + + is(view instanceof Ci.nsITreeView, true, testid + "view is a TreeView"); + is(view.rowCount, rowInfo.rows.length, testid + "rowCount"); + + testtag_tree_TreeView_rows(tree, testid, rowInfo, 0); + + // note that this will only work for content trees currently + view.setCellText(0, columns[1], "Changed Value"); + is(view.getCellText(0, columns[1]), "Changed Value", "setCellText"); + + view.setCellValue(1, columns[0], "Another Changed Value"); + is(view.getCellValue(1, columns[0]), "Another Changed Value", "setCellText"); +} + +function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { + var r; + var columns = tree.columns; + var view = tree.view; + var length = rowInfo.rows.length; + + // methods to test along with the functions which determine the expected value + var checkRowMethods = { + isContainer(row) { + return row.container; + }, + isContainerOpen(row) { + return false; + }, + isContainerEmpty(row) { + return row.children != null && !row.children.rows.length; + }, + isSeparator(row) { + return row.separator; + }, + getRowProperties(row) { + return row.properties; + }, + getLevel(row) { + return row.level; + }, + getParentIndex(row) { + return row.parent; + }, + hasNextSibling(row) { + return r < startRow + length - 1; + }, + }; + + var checkCellMethods = { + getCellText(row, cell) { + return cell.label; + }, + getCellValue(row, cell) { + return cell.value; + }, + getCellProperties(row, cell) { + return cell.properties; + }, + isEditable(row, cell) { + return cell.editable; + }, + getImageSrc(row, cell) { + return cell.image; + }, + }; + + var failedMethods = {}; + var checkMethod, actual, expected; + var toggleOpenStateOK = true; + + for (r = startRow; r < length; r++) { + var row = rowInfo.rows[r]; + for (var c = 0; c < row.cells.length; c++) { + var cell = row.cells[c]; + + for (checkMethod in checkCellMethods) { + expected = checkCellMethods[checkMethod](row, cell); + actual = view[checkMethod](r, columns[c]); + if (actual !== expected) { + failedMethods[checkMethod] = true; + is( + actual, + expected, + testid + + "row " + + r + + " column " + + c + + " " + + checkMethod + + " is incorrect" + ); + } + } + } + + // compare row properties + for (checkMethod in checkRowMethods) { + expected = checkRowMethods[checkMethod](row, r); + if (checkMethod == "hasNextSibling") { + actual = view[checkMethod](r, r); + } else { + actual = view[checkMethod](r); + } + if (actual !== expected) { + failedMethods[checkMethod] = true; + is( + actual, + expected, + testid + "row " + r + " " + checkMethod + " is incorrect" + ); + } + } + /* + // open and recurse into containers + if (row.container) { + view.toggleOpenState(r); + if (!view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), true, testid + "row " + r + " toggleOpenState open"); + } + testtag_tree_TreeView_rows(tree, testid + "container " + r + " ", row.children, r + 1); + view.toggleOpenState(r); + if (view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), false, testid + "row " + r + " toggleOpenState close"); + } + } +*/ + } + + for (var failedMethod in failedMethods) { + if (failedMethod in checkRowMethods) { + delete checkRowMethods[failedMethod]; + } + if (failedMethod in checkCellMethods) { + delete checkCellMethods[failedMethod]; + } + } + + for (checkMethod in checkRowMethods) { + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + } + for (checkMethod in checkCellMethods) { + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + } + if (toggleOpenStateOK) { + is("toggleOpenState ok", "toggleOpenState ok", testid + "toggleOpenState"); + } +} + +function testtag_tree_TreeView_rows_sort(tree, testid, rowInfo) { + // check if cycleHeader sorts the columns + var columnIndex = 0; + var view = tree.view; + var column = tree.columns[columnIndex]; + var columnElement = column.element; + var sortkey = columnElement.getAttribute("sort"); + if (sortkey) { + view.cycleHeader(column); + is(tree.getAttribute("sort"), sortkey, "cycleHeader sort"); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending" + ); + is( + columnElement.getAttribute("sortDirection"), + "ascending", + "cycleHeader column sortDirection" + ); + is( + columnElement.getAttribute("sortActive"), + "true", + "cycleHeader column sortActive" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "descending", + "cycleHeader sortDirection descending" + ); + is( + columnElement.getAttribute("sortDirection"), + "descending", + "cycleHeader column sortDirection descending" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "", + "cycleHeader sortDirection natural" + ); + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection natural" + ); + // XXXndeakin content view isSorted needs to be tested + } + + // Check that clicking on column header sorts the column. + var columns = getSortedColumnArray(tree); + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection" + ); + + // Click once on column header and check sorting has cycled once. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + is( + columnElement.getAttribute("sortDirection"), + "ascending", + "single click cycleHeader column sortDirection ascending" + ); + + // Now simulate a double click. + mouseClickOnColumnHeader(columns, columnIndex, 0, 2); + if (navigator.platform.indexOf("Win") == 0) { + // Windows cycles only once on double click. + is( + columnElement.getAttribute("sortDirection"), + "descending", + "double click cycleHeader column sortDirection descending" + ); + // 1 single clicks should restore natural sorting. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + } + + // Check we have gone back to natural sorting. + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection" + ); + + columnElement.setAttribute("sorthints", "twostate"); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending twostate" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "descending", + "cycleHeader sortDirection ascending twostate" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending twostate again" + ); + columnElement.removeAttribute("sorthints"); + view.cycleHeader(column); + view.cycleHeader(column); + + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection reset" + ); +} + +// checks if the current and selected rows are correct +// current is the index of the current row +// selected is an array of the indicies of the selected rows +// viewidx is the row that should be visible at the top of the tree +function testtag_tree_TreeSelection_State( + tree, + testid, + current, + selected, + viewidx +) { + var selection = tree.view.selection; + + is(selection.count, selected.length, testid + " count"); + is(tree.currentIndex, current, testid + " currentIndex"); + is(selection.currentIndex, current, testid + " TreeSelection currentIndex"); + if (viewidx !== null && viewidx !== undefined) { + is(tree.getFirstVisibleRow(), viewidx, testid + " first visible row"); + } + + var actualSelected = []; + var count = tree.view.rowCount; + for (var s = 0; s < count; s++) { + if (selection.isSelected(s)) { + actualSelected.push(s); + } + } + + is( + compareArrays(selected, actualSelected), + true, + testid + " selection [" + selected + "]" + ); + + actualSelected = []; + var rangecount = selection.getRangeCount(); + for (var r = 0; r < rangecount; r++) { + var start = {}, + end = {}; + selection.getRangeAt(r, start, end); + for (var rs = start.value; rs <= end.value; rs++) { + actualSelected.push(rs); + } + } + + is( + compareArrays(selected, actualSelected), + true, + testid + " range selection [" + selected + "]" + ); +} + +function testtag_tree_column_reorder() { + // Make sure the tree is scrolled into the view, otherwise the test will + // fail + var testframe = window.parent.document.getElementById("testframe"); + if (testframe) { + testframe.scrollIntoView(); + } + + var tree = document.getElementById("tree-column-reorder"); + var numColumns = tree.columns.count; + + var reference = []; + for (let i = 0; i < numColumns; i++) { + reference.push("col_" + i); + } + + // Drag the first column to each position + for (let i = 0; i < numColumns - 1; i++) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag first column right"); + } + + // And back + for (let i = numColumns - 1; i >= 1; i--) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag last column left"); + } + + // Drag each column one column left + for (let i = 1; i < numColumns; i++) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag each column left"); + } + + // And back + for (let i = numColumns - 2; i >= 0; i--) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag each column right"); + } + + // Drag each column 5 to the right + for (let i = 0; i < numColumns - 5; i++) { + synthesizeColumnDrag(tree, i, i + 5, true); + arrayMove(reference, i, i + 5, true); + checkColumns(tree, reference, "drag each column 5 to the right"); + } + + // And to the left + for (let i = numColumns - 6; i >= 5; i--) { + synthesizeColumnDrag(tree, i, i - 5, false); + arrayMove(reference, i, i - 5, false); + checkColumns(tree, reference, "drag each column 5 to the left"); + } + + // Test that moving a column after itself does not move anything + synthesizeColumnDrag(tree, 0, 0, true); + checkColumns(tree, reference, "drag to itself"); + is(document.treecolDragging, null, "drag to itself completed"); + + // XXX roc should this be here??? + SimpleTest.finish(); +} + +function testtag_tree_wheel(aTree) { + const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE, // 2 + ]; + function helper(aStart, aDelta, aIntDelta, aDeltaMode) { + aTree.scrollToRow(aStart); + var expected; + if (!aIntDelta) { + expected = aStart; + } else if (aDeltaMode != WheelEvent.DOM_DELTA_PAGE) { + expected = aStart + aIntDelta; + } else if (aIntDelta > 0) { + expected = aStart + aTree.getPageLength(); + } else { + expected = aStart - aTree.getPageLength(); + } + + if (expected < 0) { + expected = 0; + } + if (expected > aTree.view.rowCount - aTree.getPageLength()) { + expected = aTree.view.rowCount - aTree.getPageLength(); + } + synthesizeWheel(aTree.body, 1, 1, { + deltaMode: aDeltaMode, + deltaY: aDelta, + lineOrPageDeltaY: aIntDelta, + }); + is( + aTree.getFirstVisibleRow(), + expected, + "testtag_tree_wheel: vertical, starting " + + aStart + + " delta " + + aDelta + + " lineOrPageDelta " + + aIntDelta + + " aDeltaMode " + + aDeltaMode + ); + + aTree.scrollToRow(aStart); + // Check that horizontal scrolling has no effect + synthesizeWheel(aTree.body, 1, 1, { + deltaMode: aDeltaMode, + deltaX: aDelta, + lineOrPageDeltaX: aIntDelta, + }); + is( + aTree.getFirstVisibleRow(), + aStart, + "testtag_tree_wheel: horizontal, starting " + + aStart + + " delta " + + aDelta + + " lineOrPageDelta " + + aIntDelta + + " aDeltaMode " + + aDeltaMode + ); + } + + var defaultPrevented = 0; + + function wheelListener(event) { + defaultPrevented++; + } + window.addEventListener("wheel", wheelListener); + + deltaModes.forEach(function (aDeltaMode) { + var delta = aDeltaMode == WheelEvent.DOM_DELTA_PIXEL ? 5.0 : 0.3; + helper(2, -delta, 0, aDeltaMode); + helper(2, -delta, -1, aDeltaMode); + helper(2, delta, 0, aDeltaMode); + helper(2, delta, 1, aDeltaMode); + helper(2, -2 * delta, 0, aDeltaMode); + helper(2, -2 * delta, -1, aDeltaMode); + helper(2, 2 * delta, 0, aDeltaMode); + helper(2, 2 * delta, 1, aDeltaMode); + }); + + window.removeEventListener("wheel", wheelListener); + is(defaultPrevented, 48, "wheel event default prevented"); +} + +async function testtag_tree_scroll() { + const tree = document.querySelector("tree"); + + info("Scroll down with the content scrollbar at the top"); + await doScrollTest({ + tree, + initialTreeScrollRow: 0, + initialContainerScrollTop: 0, + scrollDelta: 10, + isTreeScrollExpected: true, + }); + + info("Scroll down with the content scrollbar at the middle"); + await doScrollTest({ + tree, + initialTreeScrollRow: 3, + initialContainerScrollTop: 0, + scrollDelta: 10, + isTreeScrollExpected: true, + }); + + info("Scroll down with the content scrollbar at the bottom"); + await doScrollTest({ + tree, + initialTreeScrollRow: 9, + initialContainerScrollTop: 0, + scrollDelta: 10, + isTreeScrollExpected: false, + }); + + info("Scroll up with the content scrollbar at the bottom"); + await doScrollTest({ + tree, + initialTreeScrollRow: 9, + initialContainerScrollTop: 50, + scrollDelta: -10, + isTreeScrollExpected: true, + }); + + info("Scroll up with the content scrollbar at the middle"); + await doScrollTest({ + tree, + initialTreeScrollRow: 5, + initialContainerScrollTop: 50, + scrollDelta: -10, + isTreeScrollExpected: true, + }); + + info("Scroll up with the content scrollbar at the top"); + await doScrollTest({ + tree, + initialTreeScrollRow: 0, + initialContainerScrollTop: 50, + scrollDelta: -10, + isTreeScrollExpected: false, + }); + + info("Check whether the tree is not scrolled when the parent is scrolling"); + await doScrollWhileScrollingParent(tree); + + info( + "Check whether the tree component consumes wheel events even if the scroll is located at edge as long as the events are handled as the same series" + ); + await doScrollInSameSeries({ + tree, + initialTreeScrollRow: 0, + initialContainerScrollTop: 0, + scrollDelta: 10, + }); + await doScrollInSameSeries({ + tree, + initialTreeScrollRow: 9, + initialContainerScrollTop: 50, + scrollDelta: -10, + }); + + SimpleTest.finish(); +} + +async function doScrollInSameSeries({ + tree, + initialTreeScrollRow, + initialContainerScrollTop, + scrollDelta, +}) { + // Set enough value to mousewheel.scroll_series_timeout pref to ensure the wheel + // event fired as the same series. + Services.prefs.setIntPref("mousewheel.scroll_series_timeout", 1000); + + const scrollbar = tree.shadowRoot.querySelector( + "scrollbar[orient='vertical']" + ); + const parent = tree.parentElement; + + tree.scrollToRow(initialTreeScrollRow); + parent.scrollTop = initialContainerScrollTop; + + // Scroll until the scrollbar was moved to the specified amount. + await SimpleTest.promiseWaitForCondition(async () => { + await nativeScroll(tree, 10, 10, scrollDelta); + const curpos = scrollbar.getAttribute("curpos"); + return ( + (scrollDelta < 0 && curpos == 0) || + (scrollDelta > 0 && curpos == scrollbar.getAttribute("maxpos")) + ); + }); + + // More scroll as the same series. + for (let i = 0; i < 10; i++) { + await nativeScroll(tree, 10, 10, scrollDelta); + } + + is( + parent.scrollTop, + initialContainerScrollTop, + "The wheel events are condumed in tree component" + ); + const utils = SpecialPowers.getDOMWindowUtils(window); + ok(!utils.getWheelScrollTarget(), "The parent should not handle the event"); + + Services.prefs.clearUserPref("mousewheel.scroll_series_timeout"); +} + +async function doScrollWhileScrollingParent(tree) { + // Set enough value to mousewheel.scroll_series_timeout pref to ensure the wheel + // event fired as the same series. + Services.prefs.setIntPref("mousewheel.scroll_series_timeout", 1000); + + const scrollbar = tree.shadowRoot.querySelector( + "scrollbar[orient='vertical']" + ); + const parent = tree.parentElement; + + // Set initial scroll amount. + tree.scrollToRow(0); + parent.scrollTop = 0; + + const scrollAmount = scrollbar.getAttribute("curpos"); + + // Scroll parent from top to bottom. + await SimpleTest.promiseWaitForCondition(async () => { + await nativeScroll(parent, 10, 10, 10); + return parent.scrollTop === parent.scrollTopMax; + }); + + is( + scrollAmount, + scrollbar.getAttribute("curpos"), + "The tree should not be scrolled" + ); + + const utils = SpecialPowers.getDOMWindowUtils(window); + await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget()); + Services.prefs.clearUserPref("mousewheel.scroll_series_timeout"); +} + +async function doScrollTest({ + tree, + initialTreeScrollRow, + initialContainerScrollTop, + scrollDelta, + isTreeScrollExpected, +}) { + const scrollbar = tree.shadowRoot.querySelector( + "scrollbar[orient='vertical']" + ); + const container = tree.parentElement; + + // Set initial scroll amount. + tree.scrollToRow(initialTreeScrollRow); + container.scrollTop = initialContainerScrollTop; + + const treeScrollAmount = scrollbar.getAttribute("curpos"); + const containerScrollAmount = container.scrollTop; + + // Wait until changing either scroll. + await SimpleTest.promiseWaitForCondition(async () => { + await nativeScroll(tree, 10, 10, scrollDelta); + return ( + treeScrollAmount !== scrollbar.getAttribute("curpos") || + containerScrollAmount !== container.scrollTop + ); + }); + + is( + treeScrollAmount !== scrollbar.getAttribute("curpos"), + isTreeScrollExpected, + "Scroll of tree is expected" + ); + is( + containerScrollAmount !== container.scrollTop, + !isTreeScrollExpected, + "Scroll of container is expected" + ); + + // Wait until finishing wheel scroll transaction. + const utils = SpecialPowers.getDOMWindowUtils(window); + await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget()); +} + +async function nativeScroll(component, offsetX, offsetY, scrollDelta) { + const utils = SpecialPowers.getDOMWindowUtils(window); + const x = component.screenX + offsetX; + const y = component.screenY + offsetY; + + // Mouse move event. + await new Promise(resolve => { + info("waiting for mousemove"); + window.addEventListener("mousemove", resolve, { once: true }); + utils.sendNativeMouseEvent( + x * window.devicePixelRatio, + y * window.devicePixelRatio, + utils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + {}, + component + ); + }); + + // Wheel event. + await new Promise(resolve => { + info("waiting for wheel"); + window.addEventListener("wheel", resolve, { once: true }); + utils.sendNativeMouseScrollEvent( + x * window.devicePixelRatio, + y * window.devicePixelRatio, + // nativeVerticalWheelEventMsg is defined in apz_test_native_event_utils.js + // eslint-disable-next-line no-undef + nativeVerticalWheelEventMsg(), + 0, + // nativeScrollUnits is defined in apz_test_native_event_utils.js + // eslint-disable-next-line no-undef + -nativeScrollUnits(component, scrollDelta), + 0, + 0, + 0, + component + ); + }); + + info("waiting for apz"); + // promiseApzFlushedRepaints is defined in apz_test_utils.js + // eslint-disable-next-line no-undef + await promiseApzFlushedRepaints(); +} + +function synthesizeColumnDrag( + aTree, + aMouseDownColumnNumber, + aMouseUpColumnNumber, + aAfter +) { + var columns = getSortedColumnArray(aTree); + + var down = columns[aMouseDownColumnNumber].element; + var up = columns[aMouseUpColumnNumber].element; + + // Target the initial mousedown in the middle of the column header so we + // avoid the extra hit test space given to the splitter + var columnWidth = down.getBoundingClientRect().width; + var splitterHitWidth = columnWidth / 2; + synthesizeMouse(down, splitterHitWidth, 3, { type: "mousedown" }); + + var offsetX = 0; + if (aAfter) { + offsetX = columnWidth; + } + + if (aMouseUpColumnNumber > aMouseDownColumnNumber) { + for (let i = aMouseDownColumnNumber; i <= aMouseUpColumnNumber; i++) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); + } + } else { + for (let i = aMouseDownColumnNumber; i >= aMouseUpColumnNumber; i--) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); + } + } + + synthesizeMouse(up, offsetX, 3, { type: "mouseup" }); +} + +function arrayMove(aArray, aFrom, aTo, aAfter) { + var o = aArray.splice(aFrom, 1)[0]; + if (aTo > aFrom) { + aTo--; + } + + if (aAfter) { + aTo++; + } + + aArray.splice(aTo, 0, o); +} + +function getSortedColumnArray(aTree) { + var columns = aTree.columns; + var array = []; + for (let i = 0; i < columns.length; i++) { + array.push(columns.getColumnAt(i)); + } + + array.sort(function (a, b) { + var o1 = parseInt(a.element.style.order); + var o2 = parseInt(b.element.style.order); + return o1 - o2; + }); + return array; +} + +function checkColumns(aTree, aReference, aMessage) { + var columns = getSortedColumnArray(aTree); + var ids = []; + columns.forEach(function (e) { + ids.push(e.element.id); + }); + is(compareArrays(ids, aReference), true, aMessage); +} + +function mouseOnCell(tree, row, column, testname) { + var rect = tree.getCoordsForCellItem(row, column, "text"); + + synthesizeMouseExpectEvent( + tree.body, + rect.x, + rect.y, + {}, + tree, + "select", + testname + ); +} + +function mouseClickOnColumnHeader( + aColumns, + aColumnIndex, + aButton, + aClickCount +) { + var columnHeader = aColumns[aColumnIndex].element; + var columnHeaderRect = columnHeader.getBoundingClientRect(); + var columnWidth = columnHeaderRect.right - columnHeaderRect.left; + // For multiple click we send separate click events, with increasing + // clickCount. This simulates the common behavior of multiple clicks. + for (let i = 1; i <= aClickCount; i++) { + // Target the middle of the column header. + synthesizeMouse(columnHeader, columnWidth / 2, 3, { + button: aButton, + clickCount: i, + }); + } +} + +function mouseDblClickOnCell(tree, row, column, testname) { + // select the row we will edit + var selection = tree.view.selection; + selection.select(row); + tree.ensureRowIsVisible(row); + + // get cell coordinates + var rect = tree.getCoordsForCellItem(row, column, "text"); + + synthesizeMouse(tree.body, rect.x, rect.y, { clickCount: 2 }); +} + +function compareArrays(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + + return true; +} + +function convertDOMtoTreeRowInfo(treechildren, level, rowidx) { + var obj = { rows: [] }; + + var parentidx = rowidx.value; + + treechildren = treechildren.childNodes; + for (var r = 0; r < treechildren.length; r++) { + rowidx.value++; + + var treeitem = treechildren[r]; + if (treeitem.hasChildNodes()) { + var treerow = treeitem.firstChild; + var cellInfo = []; + for (var c = 0; c < treerow.childNodes.length; c++) { + var cell = treerow.childNodes[c]; + cellInfo.push({ + label: "" + cell.getAttribute("label"), + value: cell.getAttribute("value"), + properties: cell.getAttribute("properties"), + editable: cell.getAttribute("editable") != "false", + selectable: cell.getAttribute("selectable") != "false", + image: cell.getAttribute("src"), + mode: cell.hasAttribute("mode") + ? parseInt(cell.getAttribute("mode")) + : 3, + }); + } + + var descendants = treeitem.lastChild; + var children = + treerow == descendants + ? null + : convertDOMtoTreeRowInfo(descendants, level + 1, rowidx); + obj.rows.push({ + cells: cellInfo, + properties: treerow.getAttribute("properties"), + container: treeitem.getAttribute("container") == "true", + separator: treeitem.localName == "treeseparator", + children, + level, + parent: parentidx, + }); + } + } + + return obj; +} diff --git a/toolkit/content/tests/widgets/video.ogg b/toolkit/content/tests/widgets/video.ogg Binary files differnew file mode 100644 index 0000000000..ac7ece3519 --- /dev/null +++ b/toolkit/content/tests/widgets/video.ogg diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html new file mode 100644 index 0000000000..1f7e76a7d0 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1a.html b/toolkit/content/tests/widgets/videocontrols_direction-1a.html new file mode 100644 index 0000000000..a4d3546294 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1b.html b/toolkit/content/tests/widgets/videocontrols_direction-1b.html new file mode 100644 index 0000000000..a14b11d5ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1c.html b/toolkit/content/tests/widgets/videocontrols_direction-1c.html new file mode 100644 index 0000000000..0885ebd893 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1d.html b/toolkit/content/tests/widgets/videocontrols_direction-1d.html new file mode 100644 index 0000000000..a39accec72 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" dir="rtl"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1e.html b/toolkit/content/tests/widgets/videocontrols_direction-1e.html new file mode 100644 index 0000000000..25e7c2c1f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html new file mode 100644 index 0000000000..630177883c --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2a.html b/toolkit/content/tests/widgets/videocontrols_direction-2a.html new file mode 100644 index 0000000000..2e40cdc1a7 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2b.html b/toolkit/content/tests/widgets/videocontrols_direction-2b.html new file mode 100644 index 0000000000..2e4dadb6ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2c.html b/toolkit/content/tests/widgets/videocontrols_direction-2c.html new file mode 100644 index 0000000000..a43b03e8f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2d.html b/toolkit/content/tests/widgets/videocontrols_direction-2d.html new file mode 100644 index 0000000000..52d56f1ccd --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" dir="rtl"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2e.html b/toolkit/content/tests/widgets/videocontrols_direction-2e.html new file mode 100644 index 0000000000..58bc30e2b3 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction_test.js b/toolkit/content/tests/widgets/videocontrols_direction_test.js new file mode 100644 index 0000000000..e937f06b3f --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction_test.js @@ -0,0 +1,119 @@ +// This file expects `tests` to have been declared in the global scope. +/* global tests */ + +var RemoteCanvas = function (url, id) { + this.url = url; + this.id = id; + this.snapshot = null; +}; + +RemoteCanvas.CANVAS_WIDTH = 200; +RemoteCanvas.CANVAS_HEIGHT = 200; + +RemoteCanvas.prototype.compare = function (otherCanvas, expected) { + return compareSnapshots(this.snapshot, otherCanvas.snapshot, expected)[0]; +}; + +RemoteCanvas.prototype.load = function (callback) { + var iframe = document.createElement("iframe"); + iframe.id = this.id + "-iframe"; + iframe.width = RemoteCanvas.CANVAS_WIDTH + "px"; + iframe.height = RemoteCanvas.CANVAS_HEIGHT + "px"; + iframe.src = this.url; + var me = this; + iframe.addEventListener("load", function () { + info("iframe loaded"); + var m = iframe.contentDocument.getElementById("av"); + m.addEventListener( + "suspend", + function (aEvent) { + setTimeout(function () { + let mediaElement = + iframe.contentDocument.querySelector("audio, video"); + const { widget } = SpecialPowers.wrap(iframe.contentWindow) + .windowGlobalChild.getActor("UAWidgets") + .widgets.get(mediaElement); + widget.impl.Utils.l10n.translateRoots().then(() => { + me.remotePageLoaded(callback); + }); + }, 0); + }, + { once: true } + ); + m.src = m.getAttribute("source"); + }); + window.document.body.appendChild(iframe); +}; + +RemoteCanvas.prototype.remotePageLoaded = function (callback) { + var ldrFrame = document.getElementById(this.id + "-iframe"); + this.snapshot = snapshotWindow(ldrFrame.contentWindow); + this.snapshot.id = this.id + "-canvas"; + window.document.body.appendChild(this.snapshot); + callback(this); +}; + +RemoteCanvas.prototype.cleanup = function () { + var iframe = document.getElementById(this.id + "-iframe"); + iframe.remove(); + var canvas = document.getElementById(this.id + "-canvas"); + canvas.remove(); +}; + +function runTest(index) { + var canvases = []; + function testCallback(canvas) { + canvases.push(canvas); + + if (canvases.length == 2) { + // when both canvases are loaded + var expectedEqual = currentTest.op == "=="; + var result = canvases[0].compare(canvases[1], expectedEqual); + ok( + result, + "Rendering of reftest " + + currentTest.test + + " should " + + (expectedEqual ? "not " : "") + + "be different to the reference" + ); + + if (result) { + canvases[0].cleanup(); + canvases[1].cleanup(); + } else { + info("Snapshot of canvas 1: " + canvases[0].snapshot.toDataURL()); + info("Snapshot of canvas 2: " + canvases[1].snapshot.toDataURL()); + } + + if (index < tests.length - 1) { + runTest(index + 1); + } else { + SimpleTest.finish(); + } + } + } + + var currentTest = tests[index]; + var testCanvas = new RemoteCanvas(currentTest.test, "test-" + index); + testCanvas.load(testCallback); + + var refCanvas = new RemoteCanvas(currentTest.ref, "ref-" + index); + refCanvas.load(testCallback); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +window.addEventListener( + "load", + function () { + SpecialPowers.pushPrefEnv( + { set: [["media.cache_size", 40000]] }, + function () { + runTest(0); + } + ); + }, + true +); diff --git a/toolkit/content/tests/widgets/videomask.css b/toolkit/content/tests/widgets/videomask.css new file mode 100644 index 0000000000..066d441388 --- /dev/null +++ b/toolkit/content/tests/widgets/videomask.css @@ -0,0 +1,23 @@ +html, body { + margin: 0; + padding: 0; +} + +audio, video { + width: 140px; + height: 100px; + background-color: black; +} + +/** + * Create a mask for the video direction tests which covers up the throbber. + */ +#mask { + position: absolute; + z-index: 3; + width: 140px; + height: 72px; + background-color: green; + top: 0; + right: 0; +} diff --git a/toolkit/content/tests/widgets/window_label_checkbox.xhtml b/toolkit/content/tests/widgets/window_label_checkbox.xhtml new file mode 100644 index 0000000000..e1a8258b92 --- /dev/null +++ b/toolkit/content/tests/widgets/window_label_checkbox.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Label Checkbox Tests" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<hbox> + <label control="checkbox" value="Label" id="label"/> + <checkbox id="checkbox"/> + <label control="radio2" value="Label" id="label2"/> + <radiogroup> + <radio/> + <radio id="radio2"/> + </radiogroup> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + + let SimpleTest = opener.SimpleTest; + SimpleTest.waitForFocus(() => { + let ok = SimpleTest.ok; + let label = document.getElementById("label"); + let checkbox = document.getElementById("checkbox"); + let label2 = document.getElementById("label2"); + let radio2 = document.getElementById("radio2"); + checkbox.checked = true; + radio2.selected = false; + ok(checkbox.checked, "sanity check"); + ok(!radio2.selected, "sanity check"); + setTimeout(() => { + synthesizeMouseAtCenter(label, {}); + ok(!checkbox.checked, "Checkbox should be unchecked"); + synthesizeMouseAtCenter(label2, {}); + ok(radio2.selected, "Radio2 should be selected"); + opener.postMessage("done", "*"); + window.close(); + }, 0); + }); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/widgets/window_menubar.xhtml b/toolkit/content/tests/widgets/window_menubar.xhtml new file mode 100644 index 0000000000..c4ced844ad --- /dev/null +++ b/toolkit/content/tests/widgets/window_menubar.xhtml @@ -0,0 +1,1015 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<!-- the condition in the focus event handler is because pressing Tab + unfocuses and refocuses the window on Windows --> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<hbox style="margin-left: 275px; margin-top: 275px;"> +<menubar id="menubar"> + <menu id="filemenu" label="File" accesskey="F"> + <menupopup id="filepopup"> + <menuitem id="item1" label="Open" accesskey="O"/> + <menuitem id="item2" label="Save" accesskey="S"/> + <menuitem id="item3" label="Close" accesskey="C"/> + </menupopup> + </menu> + <menu id="secretmenu" label="Secret Menu" accesskey="S" disabled="true"> + <menupopup> + <menuitem label="Secret Command" accesskey="S"/> + </menupopup> + </menu> + <menu id="editmenu" label="Edit" accesskey="E"> + <menupopup id="editpopup"> + <menuitem id="cut" label="Cut" accesskey="t" disabled="true"/> + <menuitem id="copy" label="Copy" accesskey="C"/> + <menuitem id="paste" label="Paste" accesskey="P"/> + </menupopup> + </menu> + <menu id="viewmenu" label="View" accesskey="V"> + <menupopup id="viewpopup"> + <menu id="toolbar" label="Toolbar" accesskey="T"> + <menupopup id="toolbarpopup"> + <menuitem id="navigation" label="Navigation" accesskey="N" disabled="true"/> + <menuitem label="Bookmarks" accesskey="B" disabled="true"/> + </menupopup> + </menu> + <menuitem label="Status Bar" accesskey="S"/> + <menu label="Sidebar" accesskey="d"> + <menupopup> + <menuitem label="Bookmarks" accesskey="B"/> + <menuitem label="History" accesskey="H"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="helpmenu" label="Help" accesskey="H"> + <menupopup id="helppopup" > + <label value="Unselectable"/> + <menuitem id="contents" label="Contents" accesskey="C"/> + <menuitem label="More Info" accesskey="I"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem label="Another Menu"/> + <menuitem id="one" label="One"/> + <menu id="only" label="Only Menu"> + <menupopup> + <menuitem label="Test Submenu"/> + </menupopup> + </menu> + <menuitem label="Second Menu"/> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="third" label="Third Menu"/> + <menuitem label="One Other Menu"/> + <label value="Unselectable"/> + <menuitem id="about" label="About" accesskey="A"/> + </menupopup> + </menu> +</menubar> +<hbox> + <description id="outside">Outside menubar</description> + <html:input id="input"/> +</hbox> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +async function moveMouseOver(id) { + // A single synthesized mouse move isn't always enough in some platforms. + let el = document.getElementById(id); + synthesizeMouse(el, 5, 5, { type: "mousemove" }); + await new Promise(r => setTimeout(r, 0)); + synthesizeMouse(el, 8, 8, { type: "mousemove" }); +} + +let gFilePopup; +window.opener.SimpleTest.waitForFocus(function () { + gFilePopup = document.getElementById("filepopup"); + var filemenu = document.getElementById("filemenu"); + filemenu.focus(); + is(filemenu.openedWithKey, false, "initial openedWithKey"); + startPopupTests(popupTests); +}, window); + +const kIsWindows = navigator.platform.indexOf("Win") == 0; +const kIsLinux = navigator.platform.includes("Linux"); + +// On Linux, the first menu opens when F10 is pressed, but on other platforms +// the menubar is focused but no menu is opened. This means that different events +// fire. +function pressF10Events() +{ + return kIsLinux ? + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "popupshowing filepopup", "popupshown filepopup"] : + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; +} + +function closeAfterF10Events() +{ + if (kIsLinux) { + return [ + "popuphiding filepopup", + "popuphidden filepopup", + "DOMMenuInactive filepopup", + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ]; + } + + return [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ]; +} + +var popupTests = [ +{ + testname: "press on menu", + events: [ "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", "popupshowing filepopup", "popupshown filepopup" ], + test() { synthesizeMouse(document.getElementById("filemenu"), 8, 8, { }); }, + result (testname) { + checkActive(gFilePopup, "", testname); + checkOpen("filemenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: [ "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item3" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { checkActive(gFilePopup, "item3", testname); } +}, +{ + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive item3", "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item2" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item2", testname); } +}, +{ + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: [ "DOMMenuItemInactive item2", "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, + +{ + // cursor right should skip the disabled menu and move to the edit menu + testname: "cursor right skip disabled", + events() { + var elist = [ + // the file menu gets deactivated, the file menu gets hidden, then + // the edit menu is activated + "DOMMenuItemInactive filemenu", "DOMMenuItemActive editmenu", + "popuphiding filepopup", "popuphidden filepopup", + // the popupshowing event gets fired when showing the edit menu. + "popupshowing editpopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + ]; + // finally, the first item is activated and popupshown is fired. + // On Windows, don't skip disabled items. + if (kIsWindows) { + elist.push("DOMMenuItemActive cut"); + } else { + elist.push("DOMMenuItemActive copy"); + } + elist.push("popupshown editpopup"); + return elist; + }, + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + var expected = kIsWindows ? "cut" : "copy"; + checkActive(document.getElementById("editpopup"), expected, testname); + checkClosed("filemenu", testname); + checkOpen("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // on Windows, a disabled item is selected, so pressing RETURN should close + // the menu but not fire a command event + testname: "enter on disabled", + events() { + if (kIsWindows) { + return [ + "popuphiding editpopup", + "popuphidden editpopup", + "DOMMenuItemInactive cut", + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + ]; + } + return [ + "DOMMenuItemInactive copy", + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + "command copy", + "popuphiding editpopup", "popuphidden editpopup", + ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkClosed("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // pressing Alt + a key should open the corresponding menu + testname: "open with accelerator", + events() { + return [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive viewmenu", + "popupshowing viewpopup", + "DOMMenuItemActive toolbar", + "popupshown viewpopup", + ]; + }, + test() { synthesizeKey("V", { altKey: true }); }, + result(testname) { + checkOpen("viewmenu", testname); + is(document.getElementById("viewmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events() { + // on Windows, the disabled 'navigation' item can still be highlighted + if (kIsWindows) { + return ["popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + } + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + } +}, +{ + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events() { + if (kIsWindows) { + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", + ]; + } + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + ]; + }, + test() { + synthesizeKey("KEY_ArrowLeft"); + }, + result(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + } +}, +{ + // open the submenu with the enter key + testname: "open submenu with enter", + events() { + if (kIsWindows) { + // on Windows, the disabled 'navigation' item can stll be highlighted + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + } + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + testname: "close submenu with escape", + events() { + if (kIsWindows) { + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", + ]; + } + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + ]; + }, + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + }, +}, +{ + testname: "open submenu with enter again", + condition() { return kIsWindows; }, + events() { + // on Windows, the disabled 'navigation' item can stll be highlighted + if (kIsWindows) { + return [ + "popupshowing toolbarpopup", + "DOMMenuItemActive navigation", + "popupshown toolbarpopup" + ]; + } + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + // while a submenu is open, switch to the next toplevel menu with the cursor right key + testname: "while a submenu is open, switch to the next menu with the cursor right", + condition() { return kIsWindows; }, + events: [ + "DOMMenuItemInactive viewmenu", + "DOMMenuItemActive helpmenu", + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "popuphiding viewpopup", + "popuphidden viewpopup", + "popupshowing helppopup", + "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", + "DOMMenuItemInactive toolbar", + "DOMMenuInactive viewpopup", + "DOMMenuItemActive contents", + "popupshown helppopup" + ], + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + checkOpen("helpmenu", testname); + checkClosed("toolbar", testname); + checkClosed("viewmenu", testname); + } +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition() { return kIsWindows; }, + events: [ + "popuphiding helppopup", + "popuphidden helppopup", + "DOMMenuItemInactive contents", + "DOMMenuInactive helppopup", + ], + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { + checkClosed("helpmenu", testname); + }, +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition() { return !kIsWindows; }, + events: [ + "popuphiding viewpopup", + "popuphidden viewpopup", + "DOMMenuItemInactive toolbar", + "DOMMenuInactive viewpopup", + ], + test() { + synthesizeKey("KEY_Escape"); + }, + result(testname) { + checkClosed("viewmenu", testname); + }, +}, +{ + // Deactivate menubar with the escape key. + testname: "deactivate menubar menu with escape", + events: [ + "DOMMenuItemInactive " + (kIsWindows ? "helpmenu" : "viewmenu"), + "DOMMenuBarInactive menubar", + ], + test() { + synthesizeKey("KEY_Escape"); + }, + result(testname) { + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keydown event is consumed. + testname: "alt shouldn't activate menubar if keydown event is consumed", + test() { + document.addEventListener("keydown", function (aEvent) { + aEvent.preventDefault(); + }, {once: true}); + synthesizeKey("KEY_Alt"); + }, + result(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keyup event is consumed. + testname: "alt shouldn't activate menubar if keyup event is consumed", + test() { + document.addEventListener("keyup", function (aEvent) { + aEvent.preventDefault(); + }, {once: true}); + synthesizeKey("KEY_Alt"); + }, + result(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it. + testname: "alt to activate menubar", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_Alt"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + checkClosed("filemenu", testname); + }, +}, +{ + // pressing cursor left should select the previous menu but not open it + testname: "cursor left on active menubar", + events: [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu" ], + test() { synthesizeKey("KEY_ArrowLeft"); }, + result(testname) { checkClosed("helpmenu", testname); }, +}, +{ + // pressing cursor right should select the previous menu but not open it + testname: "cursor right on active menubar", + events: [ "DOMMenuItemInactive helpmenu", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing a character should act as an accelerator and open the menu + testname: "accelerator on active menubar", + events: [ + "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + "popupshowing helppopup", + "DOMMenuItemActive contents", + "popupshown helppopup", + ], + test() { sendChar("h"); }, + result(testname) { + checkOpen("helpmenu", testname); + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + }, +}, +{ + // check that pressing cursor up skips non menuitems + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive about" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { } +}, +{ + // check that pressing cursor down skips non menuitems + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive about", "DOMMenuItemActive contents" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { } +}, +{ + // check that pressing a menuitem's accelerator selects it + testname: "menuitem accelerator", + events: [ + "DOMMenuItemInactive contents", + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuInactive helppopup", + "DOMMenuItemInactive helpmenu", + "DOMMenuBarInactive menubar", + "command amenu", + "popuphiding helppopup", + "popuphidden helppopup", + ], + test() { sendChar("m"); }, + result(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu. On Linux, the menu is opened. + testname: "F10 to activate menubar", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + if (kIsLinux) { + checkOpen("filemenu", testname); + } else { + checkClosed("filemenu", testname); + } + }, +}, +{ + // pressing cursor left then down should open a menu + testname: "cursor down on menu", + events: kIsLinux ? + [ + "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "popuphiding filepopup", + "popuphidden filepopup", + "popupshowing helppopup", + "DOMMenuInactive filepopup", + "popupshown helppopup", + "DOMMenuItemActive contents", + ] : [ + "popupshowing helppopup", + "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "DOMMenuItemActive contents", + "popupshown helppopup", + ], + async test() { + if (kIsLinux) { + // On linux we need to wait so that the hiding of the file popup happens + // (and the help popup opens) to send the key down. + let helpPopupShown = new Promise(r => { + document.getElementById("helppopup").addEventListener("popupshown", r, { once: true }); + }); + synthesizeKey("KEY_ArrowLeft"); + await helpPopupShown; + synthesizeKey("KEY_ArrowDown"); + } else { + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowDown"); + } + }, + result(testname) { + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // pressing a letter that doesn't correspond to an accelerator. The menu + // should not close because there is more than one item corresponding to + // that letter + testname: "menuitem with no accelerator", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive one" ], + test() { sendChar("o"); }, + result(testname) { checkOpen("helpmenu", testname); } +}, +{ + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with no accelerator again", + events: [ "DOMMenuItemInactive one", "DOMMenuItemActive only" ], + test() { sendChar("o"); }, + result(testname) { + // 'only' is a menu but it should not be open + checkOpen("helpmenu", testname); + checkClosed("only", testname); + } +}, +{ + // pressing the letter again when the next item is disabled should still + // select the disabled item + testname: "menuitem with no accelerator disabled", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive only", "DOMMenuItemActive other" ], + test() { sendChar("o"); }, + result(testname) { } +}, +{ + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with no accelerator single", + events() { + let elist = [ + "DOMMenuItemInactive other", + "DOMMenuItemActive third", + "DOMMenuItemInactive third", + "DOMMenuInactive helppopup", + "DOMMenuItemInactive helpmenu", + "DOMMenuBarInactive menubar", + "command third", + "popuphiding helppopup", + "popuphidden helppopup" + ]; + if (!kIsWindows) { + elist[0] = "DOMMenuItemInactive only"; + } + return elist; + }, + test() { sendChar("t"); }, + result(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu but not open it + testname: "F10 to activate menubar again", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_F10"); }, + result(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing an accelerator for a disabled item should deactivate the menubar + testname: "accelerator for disabled menu", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { sendChar("s"); }, + result(testname) { + checkClosed("secretmenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + }, +}, +{ + testname: "press on disabled menu", + test() { + synthesizeMouse(document.getElementById("secretmenu"), 8, 8, { }); + }, + result (testname) { + checkClosed("secretmenu", testname); + } +}, +{ + testname: "press on second menu with shift", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", + "popupshowing editpopup", + "popupshown editpopup", + ], + test() { + synthesizeMouse(document.getElementById("editmenu"), 8, 8, { shiftKey : true }); + }, + result (testname) { + checkOpen("editmenu", testname); + checkActive(document.getElementById("menubar"), "editmenu", testname); + } +}, +{ + testname: "press on disabled menuitem", + test() { + synthesizeMouse(document.getElementById("cut"), 8, 8, { }); + }, + result (testname) { + checkOpen("editmenu", testname); + } +}, +{ + testname: "press on menuitem", + events: [ + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + "command copy", + "popuphiding editpopup", + "popuphidden editpopup", + ], + test() { + synthesizeMouse(document.getElementById("copy"), 8, 8, { }); + }, + result (testname) { + checkClosed("editmenu", testname); + } +}, +{ + // this test ensures that the menu can still be opened by clicking after selecting + // a menuitem from the menu. See bug 399350. + testname: "press on menu after menuitem selected", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", + "popupshowing editpopup", + "popupshown editpopup", + ], + test() { synthesizeMouse(document.getElementById("editmenu"), 8, 8, { }); }, + result (testname) { + checkActive(document.getElementById("editpopup"), "", testname); + checkOpen("editmenu", testname); + } +}, +{ // try selecting a different command + testname: "press on menuitem again", + events: [ + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + "command paste", + "popuphiding editpopup", + "popuphidden editpopup", + ], + test() { + synthesizeMouse(document.getElementById("paste"), 8, 8, { }); + }, + result (testname) { + checkClosed("editmenu", testname); + } +}, +{ + testname: "F10 to activate menubar for tab deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with tab key", + events: closeAfterF10Events(), + test() { synthesizeKey("KEY_Tab"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for escape deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with escape key", + events: closeAfterF10Events(), + test() { + synthesizeKey("KEY_Escape"); + if (kIsLinux) { + // One to close the menu, one to deactivate the menubar. + synthesizeKey("KEY_Escape"); + } + }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for f10 deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with f10 key", + events: closeAfterF10Events(), + test() { synthesizeKey("KEY_F10"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for alt deactivation", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with alt key", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { synthesizeKey("KEY_Alt"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "Don't activate menubar with mousedown during alt key auto-repeat", + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mousedown", altKey: true }); + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mouseup", altKey: true }); + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("KEY_Alt", {type: "keyup"}); + }, + result (testname) { + checkActive(document.getElementById("menubar"), "", testname); + } +}, + +{ + testname: "Open menu and press alt key by itself - open menu", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", + "popupshowing filepopup", + "DOMMenuItemActive item1", + "popupshown filepopup", + ], + test() { synthesizeKey("F", { altKey: true }); }, + result (testname) { + checkOpen("filemenu", testname); + } +}, +{ + testname: "Open menu and press alt key by itself - close menu", + events: [ + "popuphiding filepopup", + "popuphidden filepopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ], + test() { + synthesizeKey("KEY_Alt"); + }, + result (testname) { + checkClosed("filemenu", testname); + } +}, + +// Following 4 tests are a test of bug 616797, don't insert any new tests +// between them. +{ + testname: "Open file menu by accelerator", + condition() { return kIsWindows; }, + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", + "popupshowing filepopup", + "DOMMenuItemActive item1", + "popupshown filepopup" + ], + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("f", {altKey: true}); + synthesizeKey("KEY_Alt", {type: "keyup"}); + } +}, +{ + testname: "Close file menu by click at outside of popup menu", + condition() { return kIsWindows; }, + events: [ + "popuphiding filepopup", + "popuphidden filepopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ], + test() { + document.getElementById("filepopup").hidePopup(); + } +}, +{ + testname: "Alt keydown set focus the menubar", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { + synthesizeKey("KEY_Alt"); + }, + result (testname) { + checkClosed("filemenu", testname); + } +}, +{ + testname: "unset focus the menubar", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { + synthesizeKey("KEY_Alt"); + } +}, + +// bug 1811466 +{ + testname: "Menubar activation / deactivation on mouse over", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + async test() { + await moveMouseOver("filemenu"); + await moveMouseOver("outside"); + }, +}, + +// bug 1811466 +{ + testname: "Menubar hover in and out after key activation (part 1)", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", + /* Shouldn't deactivate the menubar nor filemenu! */ + ], + async test() { + synthesizeKey("KEY_Alt"); + await moveMouseOver("filemenu"); + await moveMouseOver("outside"); + }, +}, +{ + testname: "Deactivate the menubar by mouse", + events: [ + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ], + test() { + synthesizeMouse(document.getElementById("outside"), 8, 8, {}); + }, +}, + + +// bug 1818241 +{ + testname: "Shortcut navigation on mouse-activated menubar", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + async test() { + let input = document.getElementById("input"); + input.value = ""; + input.focus(); + await moveMouseOver("filemenu"); + synthesizeKey("F"); + await moveMouseOver("outside"); + is(input.value, "F", "Key shouldn't be consumed by menubar"); + }, +}, + +// FIXME: This leaves the menubar in a state where IsActive() is false but +// IsActiveByKeyboard() is true! +{ + testname: "Trying to activate menubar without activatable items shouldn't crash", + events: [ "TestDone menubar" ], + test() { + const items = document.querySelectorAll("menubar > menu"); + let wasDisabled = {}; + for (let item of items) { + wasDisabled[item] = item.disabled; + item.disabled = true; + } + + synthesizeKey("KEY_F10"); + setTimeout(function() { + synthesizeKey("KEY_F10"); + + for (let item of items) { + item.disabled = wasDisabled[item]; + } + + document.getElementById("menubar").dispatchEvent(new CustomEvent("TestDone", { bubbles: true })); + }, 0); + } +}, + +// bug 625151 +{ + testname: "Alt key state before deactivating the window shouldn't prevent " + + "next Alt key handling", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("KEY_Tab", {type: "keydown"}); // cancels the Alt key + var thisWindow = window; + var newWindow = + window.open("javascript:'<html><body>dummy</body></html>';", "_blank", "width=100,height=100"); + newWindow.addEventListener("focus", function () { + thisWindow.addEventListener("focus", function () { + setTimeout(function () { + synthesizeKey("KEY_Alt", {}, thisWindow); + }, 0); + }, {once: true}); + newWindow.close(); + thisWindow.focus(); + }, {once: true}); + } +}, + +]; + +]]> +</script> + +</window> |