summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/tests/widgets')
-rw-r--r--toolkit/content/tests/widgets/audio.oggbin0 -> 14293 bytes
-rw-r--r--toolkit/content/tests/widgets/audio.wavbin0 -> 1422 bytes
-rw-r--r--toolkit/content/tests/widgets/chrome.toml68
-rw-r--r--toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html2
-rw-r--r--toolkit/content/tests/widgets/head.js67
-rw-r--r--toolkit/content/tests/widgets/image-zh.pngbin0 -> 5442 bytes
-rw-r--r--toolkit/content/tests/widgets/image.pngbin0 -> 7061 bytes
-rw-r--r--toolkit/content/tests/widgets/mochitest.toml110
-rw-r--r--toolkit/content/tests/widgets/popup_shared.js602
-rw-r--r--toolkit/content/tests/widgets/seek_with_sound.oggbin0 -> 299507 bytes
-rw-r--r--toolkit/content/tests/widgets/test-webvtt-1.vtt10
-rw-r--r--toolkit/content/tests/widgets/test-webvtt-2.vtt10
-rw-r--r--toolkit/content/tests/widgets/test_audiocontrols_dimensions.html66
-rw-r--r--toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html61
-rw-r--r--toolkit/content/tests/widgets/test_bug1654500.html33
-rw-r--r--toolkit/content/tests/widgets/test_bug898940.html31
-rw-r--r--toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml102
-rw-r--r--toolkit/content/tests/widgets/test_contextmenu_nested.xhtml132
-rw-r--r--toolkit/content/tests/widgets/test_editor_currentURI.xhtml37
-rw-r--r--toolkit/content/tests/widgets/test_image_recognition.html67
-rw-r--r--toolkit/content/tests/widgets/test_image_recognition_unsupported.html36
-rw-r--r--toolkit/content/tests/widgets/test_image_recognition_zh.html53
-rw-r--r--toolkit/content/tests/widgets/test_label_checkbox.xhtml40
-rw-r--r--toolkit/content/tests/widgets/test_menubar.xhtml30
-rw-r--r--toolkit/content/tests/widgets/test_mousecapture_area.html106
-rw-r--r--toolkit/content/tests/widgets/test_moz_button_group.html235
-rw-r--r--toolkit/content/tests/widgets/test_moz_card.html158
-rw-r--r--toolkit/content/tests/widgets/test_moz_five_star.html82
-rw-r--r--toolkit/content/tests/widgets/test_moz_label.html142
-rw-r--r--toolkit/content/tests/widgets/test_moz_message_bar.html92
-rw-r--r--toolkit/content/tests/widgets/test_moz_support_link.html151
-rw-r--r--toolkit/content/tests/widgets/test_moz_toggle.html85
-rw-r--r--toolkit/content/tests/widgets/test_nac_mutations.html65
-rw-r--r--toolkit/content/tests/widgets/test_panel_item_accesskey.html105
-rw-r--r--toolkit/content/tests/widgets/test_panel_list_accessibility.html79
-rw-r--r--toolkit/content/tests/widgets/test_panel_list_anchoring.html121
-rw-r--r--toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html87
-rw-r--r--toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html70
-rw-r--r--toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html96
-rw-r--r--toolkit/content/tests/widgets/test_popupanchor.xhtml387
-rw-r--r--toolkit/content/tests/widgets/test_popupreflows.xhtml96
-rw-r--r--toolkit/content/tests/widgets/test_tree_column_reorder.xhtml75
-rw-r--r--toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html22
-rw-r--r--toolkit/content/tests/widgets/test_ua_widget_sandbox.html101
-rw-r--r--toolkit/content/tests/widgets/test_ua_widget_unbind.html56
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols.html564
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_audio.html40
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_audio_direction.html31
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html56
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html144
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_error.html60
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_focus.html113
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html60
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html69
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_keyhandler.html150
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_onclickplay.html74
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html51
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html123
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_size.html179
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_standalone.html131
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_video_direction.html31
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html42
-rw-r--r--toolkit/content/tests/widgets/test_videocontrols_vtt.html112
-rw-r--r--toolkit/content/tests/widgets/tree_shared.js2184
-rw-r--r--toolkit/content/tests/widgets/video.oggbin0 -> 285310 bytes
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-1-ref.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-1a.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-1b.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-1c.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-1d.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-1e.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-2-ref.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-2a.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-2b.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-2c.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-2d.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction-2e.html10
-rw-r--r--toolkit/content/tests/widgets/videocontrols_direction_test.js119
-rw-r--r--toolkit/content/tests/widgets/videomask.css23
-rw-r--r--toolkit/content/tests/widgets/window_label_checkbox.xhtml46
-rw-r--r--toolkit/content/tests/widgets/window_menubar.xhtml1015
81 files changed, 9505 insertions, 0 deletions
diff --git a/toolkit/content/tests/widgets/audio.ogg b/toolkit/content/tests/widgets/audio.ogg
new file mode 100644
index 0000000000..a553c23e73
--- /dev/null
+++ b/toolkit/content/tests/widgets/audio.ogg
Binary files differ
diff --git a/toolkit/content/tests/widgets/audio.wav b/toolkit/content/tests/widgets/audio.wav
new file mode 100644
index 0000000000..c6fd5cb869
--- /dev/null
+++ b/toolkit/content/tests/widgets/audio.wav
Binary files differ
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
new file mode 100644
index 0000000000..944a12d39e
--- /dev/null
+++ b/toolkit/content/tests/widgets/image-zh.png
Binary files differ
diff --git a/toolkit/content/tests/widgets/image.png b/toolkit/content/tests/widgets/image.png
new file mode 100644
index 0000000000..3faa11b221
--- /dev/null
+++ b/toolkit/content/tests/widgets/image.png
Binary files differ
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
new file mode 100644
index 0000000000..c86d9946bd
--- /dev/null
+++ b/toolkit/content/tests/widgets/seek_with_sound.ogg
Binary files differ
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 --&gt; <span id="anchor-middle">v</span> &lt;--</p>
+</div>
+<div id="anchor-left-wrapper" style="text-align: left; display: none;">
+ <p><span id="anchor-left">v</span> &lt;-- The anchor;</p>
+</div>
+<div id="anchor-right-wrapper" style="text-align: right; display: none;">
+ <p>The anchor --&gt; <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 --&gt; <span id="anchor">v</span> &lt;--</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
new file mode 100644
index 0000000000..ac7ece3519
--- /dev/null
+++ b/toolkit/content/tests/widgets/video.ogg
Binary files differ
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>