summaryrefslogtreecommitdiffstats
path: root/dom/html/test/forms
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/html/test/forms
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/html/test/forms')
-rw-r--r--dom/html/test/forms/FAIL.html1
-rw-r--r--dom/html/test/forms/PASS.html1
-rw-r--r--dom/html/test/forms/chrome.toml6
-rw-r--r--dom/html/test/forms/file_double_submit.html11
-rw-r--r--dom/html/test/forms/file_login_fields.html16
-rw-r--r--dom/html/test/forms/mochitest.toml229
-rw-r--r--dom/html/test/forms/save_restore_custom_elements_sample.html43
-rw-r--r--dom/html/test/forms/save_restore_radio_groups.sjs48
-rw-r--r--dom/html/test/forms/submit_invalid_file.sjs13
-rw-r--r--dom/html/test/forms/test_MozEditableElement_setUserInput.html581
-rw-r--r--dom/html/test/forms/test_autocomplete.html164
-rw-r--r--dom/html/test/forms/test_autocompleteinfo.html206
-rw-r--r--dom/html/test/forms/test_bug1039548.html55
-rw-r--r--dom/html/test/forms/test_bug1283915.html67
-rw-r--r--dom/html/test/forms/test_bug1286509.html49
-rw-r--r--dom/html/test/forms/test_button_attributes_reflection.html144
-rw-r--r--dom/html/test/forms/test_change_event.html286
-rw-r--r--dom/html/test/forms/test_datalist_element.html118
-rw-r--r--dom/html/test/forms/test_double_submit.html33
-rw-r--r--dom/html/test/forms/test_form_attribute-1.html473
-rw-r--r--dom/html/test/forms/test_form_attribute-2.html53
-rw-r--r--dom/html/test/forms/test_form_attribute-3.html68
-rw-r--r--dom/html/test/forms/test_form_attribute-4.html48
-rw-r--r--dom/html/test/forms/test_form_attributes_reflection.html90
-rw-r--r--dom/html/test/forms/test_form_named_getter_dynamic.html54
-rw-r--r--dom/html/test/forms/test_formaction_attribute.html169
-rw-r--r--dom/html/test/forms/test_formnovalidate_attribute.html125
-rw-r--r--dom/html/test/forms/test_input_attributes_reflection.html271
-rw-r--r--dom/html/test/forms/test_input_color_input_change_events.html119
-rw-r--r--dom/html/test/forms/test_input_color_picker_datalist.html42
-rw-r--r--dom/html/test/forms/test_input_color_picker_initial.html78
-rw-r--r--dom/html/test/forms/test_input_color_picker_popup.html144
-rw-r--r--dom/html/test/forms/test_input_color_picker_update.html86
-rw-r--r--dom/html/test/forms/test_input_date_bad_input.html113
-rw-r--r--dom/html/test/forms/test_input_date_key_events.html270
-rw-r--r--dom/html/test/forms/test_input_datetime_calendar_button.html179
-rw-r--r--dom/html/test/forms/test_input_datetime_disabled_focus.html82
-rw-r--r--dom/html/test/forms/test_input_datetime_focus_blur.html64
-rw-r--r--dom/html/test/forms/test_input_datetime_focus_blur_events.html93
-rw-r--r--dom/html/test/forms/test_input_datetime_focus_state.html79
-rw-r--r--dom/html/test/forms/test_input_datetime_hidden.html32
-rw-r--r--dom/html/test/forms/test_input_datetime_input_change_events.html143
-rw-r--r--dom/html/test/forms/test_input_datetime_readonly.html20
-rw-r--r--dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html122
-rw-r--r--dom/html/test/forms/test_input_datetime_tabindex.html113
-rw-r--r--dom/html/test/forms/test_input_defaultValue.html81
-rw-r--r--dom/html/test/forms/test_input_email.html237
-rw-r--r--dom/html/test/forms/test_input_event.html409
-rw-r--r--dom/html/test/forms/test_input_file_picker.html280
-rw-r--r--dom/html/test/forms/test_input_hasBeenTypePassword.html67
-rw-r--r--dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html68
-rw-r--r--dom/html/test/forms/test_input_list_attribute.html253
-rw-r--r--dom/html/test/forms/test_input_number_data.js54
-rw-r--r--dom/html/test/forms/test_input_number_focus.html109
-rw-r--r--dom/html/test/forms/test_input_number_key_events.html238
-rw-r--r--dom/html/test/forms/test_input_number_l10n.html77
-rw-r--r--dom/html/test/forms/test_input_number_mouse_events.html272
-rw-r--r--dom/html/test/forms/test_input_number_placeholder_shown.html30
-rw-r--r--dom/html/test/forms/test_input_number_rounding.html120
-rw-r--r--dom/html/test/forms/test_input_number_validation.html139
-rw-r--r--dom/html/test/forms/test_input_password_click_show_password_button.html97
-rw-r--r--dom/html/test/forms/test_input_password_show_password_button.html81
-rw-r--r--dom/html/test/forms/test_input_radio_indeterminate.html109
-rw-r--r--dom/html/test/forms/test_input_radio_radiogroup.html75
-rw-r--r--dom/html/test/forms/test_input_radio_required.html31
-rw-r--r--dom/html/test/forms/test_input_range_attr_order.html48
-rw-r--r--dom/html/test/forms/test_input_range_key_events.html207
-rw-r--r--dom/html/test/forms/test_input_range_mouse_and_touch_events.html240
-rw-r--r--dom/html/test/forms/test_input_range_rounding.html103
-rw-r--r--dom/html/test/forms/test_input_sanitization.html585
-rw-r--r--dom/html/test/forms/test_input_setting_value.html619
-rw-r--r--dom/html/test/forms/test_input_textarea_set_value_no_scroll.html125
-rw-r--r--dom/html/test/forms/test_input_time_key_events.html221
-rw-r--r--dom/html/test/forms/test_input_time_sec_millisec_field.html134
-rw-r--r--dom/html/test/forms/test_input_types_pref.html77
-rw-r--r--dom/html/test/forms/test_input_typing_sanitization.html217
-rw-r--r--dom/html/test/forms/test_input_untrusted_key_events.html90
-rw-r--r--dom/html/test/forms/test_input_url.html91
-rw-r--r--dom/html/test/forms/test_interactive_content_in_label.html101
-rw-r--r--dom/html/test/forms/test_interactive_content_in_summary.html97
-rw-r--r--dom/html/test/forms/test_label_control_attribute.html100
-rw-r--r--dom/html/test/forms/test_label_input_controls.html84
-rw-r--r--dom/html/test/forms/test_max_attribute.html473
-rw-r--r--dom/html/test/forms/test_maxlength_attribute.html129
-rw-r--r--dom/html/test/forms/test_meter_element.html376
-rw-r--r--dom/html/test/forms/test_meter_pseudo-classes.html169
-rw-r--r--dom/html/test/forms/test_min_attribute.html473
-rw-r--r--dom/html/test/forms/test_minlength_attribute.html130
-rw-r--r--dom/html/test/forms/test_mozistextfield.html111
-rw-r--r--dom/html/test/forms/test_novalidate_attribute.html85
-rw-r--r--dom/html/test/forms/test_option_disabled.html123
-rw-r--r--dom/html/test/forms/test_option_index_attribute.html76
-rw-r--r--dom/html/test/forms/test_option_text.html57
-rw-r--r--dom/html/test/forms/test_output_element.html182
-rw-r--r--dom/html/test/forms/test_pattern_attribute.html324
-rw-r--r--dom/html/test/forms/test_preserving_metadata_between_reloads.html84
-rw-r--r--dom/html/test/forms/test_progress_element.html307
-rw-r--r--dom/html/test/forms/test_radio_in_label.html54
-rw-r--r--dom/html/test/forms/test_radio_radionodelist.html57
-rw-r--r--dom/html/test/forms/test_reportValidation_preventDefault.html89
-rw-r--r--dom/html/test/forms/test_required_attribute.html416
-rw-r--r--dom/html/test/forms/test_restore_form_elements.html174
-rw-r--r--dom/html/test/forms/test_save_restore_custom_elements.html90
-rw-r--r--dom/html/test/forms/test_save_restore_radio_groups.html70
-rw-r--r--dom/html/test/forms/test_select_change_event.html54
-rw-r--r--dom/html/test/forms/test_select_input_change_event.html122
-rw-r--r--dom/html/test/forms/test_select_selectedOptions.html119
-rw-r--r--dom/html/test/forms/test_select_validation.html39
-rw-r--r--dom/html/test/forms/test_set_range_text.html242
-rw-r--r--dom/html/test/forms/test_step_attribute.html1060
-rw-r--r--dom/html/test/forms/test_stepup_stepdown.html1137
-rw-r--r--dom/html/test/forms/test_submit_invalid_file.html55
-rw-r--r--dom/html/test/forms/test_textarea_attributes_reflection.html107
-rw-r--r--dom/html/test/forms/test_validation.html343
-rw-r--r--dom/html/test/forms/test_validation_not_in_doc.html19
-rw-r--r--dom/html/test/forms/test_valueasdate_attribute.html751
-rw-r--r--dom/html/test/forms/test_valueasnumber_attribute.html858
-rw-r--r--dom/html/test/forms/without_selectionchange/mochitest.toml5
-rw-r--r--dom/html/test/forms/without_selectionchange/test_select.html21
119 files changed, 20148 insertions, 0 deletions
diff --git a/dom/html/test/forms/FAIL.html b/dom/html/test/forms/FAIL.html
new file mode 100644
index 0000000000..94e1707e85
--- /dev/null
+++ b/dom/html/test/forms/FAIL.html
@@ -0,0 +1 @@
+FAIL
diff --git a/dom/html/test/forms/PASS.html b/dom/html/test/forms/PASS.html
new file mode 100644
index 0000000000..7ef22e9a43
--- /dev/null
+++ b/dom/html/test/forms/PASS.html
@@ -0,0 +1 @@
+PASS
diff --git a/dom/html/test/forms/chrome.toml b/dom/html/test/forms/chrome.toml
new file mode 100644
index 0000000000..0f49518b9b
--- /dev/null
+++ b/dom/html/test/forms/chrome.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files = ["submit_invalid_file.sjs"]
+
+["test_autocompleteinfo.html"]
+
+["test_submit_invalid_file.html"]
diff --git a/dom/html/test/forms/file_double_submit.html b/dom/html/test/forms/file_double_submit.html
new file mode 100644
index 0000000000..44889f86bc
--- /dev/null
+++ b/dom/html/test/forms/file_double_submit.html
@@ -0,0 +1,11 @@
+<form action="PASS.html" method="POST"><input name="foo"></form>
+<button>clicky</button>
+
+<script>
+document.querySelector("button")
+ .addEventListener("click", () => {
+ let f = document.querySelector("form");
+ f.dispatchEvent(new Event("submit"));
+ f.submit();
+ });
+</script>
diff --git a/dom/html/test/forms/file_login_fields.html b/dom/html/test/forms/file_login_fields.html
new file mode 100644
index 0000000000..f23ee0ad6a
--- /dev/null
+++ b/dom/html/test/forms/file_login_fields.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ // Add an unload listener to bypass bfcache.
+ window.addEventListner("unload", _ => _);
+ </script>
+ </head>
+ <body>
+ <input id="un" />
+ <input id="pw1" type="password" />
+ <input id="pw2" />
+ <a id="navigate" href="?navigated">Navigate</a>
+ <a id="back" href="javascript:history.back()">Back</a>
+ </body>
+</html>
diff --git a/dom/html/test/forms/mochitest.toml b/dom/html/test/forms/mochitest.toml
new file mode 100644
index 0000000000..80d6d3530f
--- /dev/null
+++ b/dom/html/test/forms/mochitest.toml
@@ -0,0 +1,229 @@
+[DEFAULT]
+support-files = [
+ "save_restore_radio_groups.sjs",
+ "test_input_number_data.js",
+ "!/dom/html/test/reflect.js",
+ "FAIL.html",
+ "PASS.html",
+]
+prefs = ["formhelper.autozoom.force-disable.test-only=true"]
+
+["test_MozEditableElement_setUserInput.html"]
+
+["test_autocomplete.html"]
+
+["test_bug1039548.html"]
+
+["test_bug1283915.html"]
+
+["test_bug1286509.html"]
+
+["test_button_attributes_reflection.html"]
+
+["test_change_event.html"]
+
+["test_datalist_element.html"]
+
+["test_double_submit.html"]
+support-files = ["file_double_submit.html"]
+
+["test_form_attribute-1.html"]
+
+["test_form_attribute-2.html"]
+
+["test_form_attribute-3.html"]
+
+["test_form_attribute-4.html"]
+
+["test_form_attributes_reflection.html"]
+
+["test_form_named_getter_dynamic.html"]
+
+["test_formaction_attribute.html"]
+
+["test_formnovalidate_attribute.html"]
+
+["test_input_attributes_reflection.html"]
+
+["test_input_color_input_change_events.html"]
+
+["test_input_color_picker_datalist.html"]
+
+["test_input_color_picker_initial.html"]
+
+["test_input_color_picker_popup.html"]
+
+["test_input_color_picker_update.html"]
+
+["test_input_date_bad_input.html"]
+
+["test_input_date_key_events.html"]
+
+["test_input_datetime_calendar_button.html"]
+
+["test_input_datetime_disabled_focus.html"]
+
+["test_input_datetime_focus_blur.html"]
+
+["test_input_datetime_focus_blur_events.html"]
+
+["test_input_datetime_focus_state.html"]
+
+["test_input_datetime_hidden.html"]
+
+["test_input_datetime_input_change_events.html"]
+
+["test_input_datetime_readonly.html"]
+
+["test_input_datetime_reset_default_value_input_change_event.html"]
+
+["test_input_datetime_tabindex.html"]
+
+["test_input_defaultValue.html"]
+
+["test_input_email.html"]
+
+["test_input_event.html"]
+
+["test_input_file_picker.html"]
+
+["test_input_hasBeenTypePassword.html"]
+
+["test_input_hasBeenTypePassword_navigation.html"]
+support-files = ["file_login_fields.html"]
+
+["test_input_list_attribute.html"]
+
+["test_input_number_focus.html"]
+
+["test_input_number_key_events.html"]
+
+["test_input_number_l10n.html"]
+
+["test_input_number_mouse_events.html"]
+# Not run on Firefox for Android where the spin buttons are hidden:
+skip-if = [
+ "os == 'android'",
+ "os == 'mac' && debug", # Bug 1484442
+]
+
+["test_input_number_placeholder_shown.html"]
+
+["test_input_number_rounding.html"]
+
+["test_input_number_validation.html"]
+
+["test_input_password_click_show_password_button.html"]
+
+["test_input_password_show_password_button.html"]
+
+["test_input_radio_indeterminate.html"]
+
+["test_input_radio_radiogroup.html"]
+
+["test_input_radio_required.html"]
+
+["test_input_range_attr_order.html"]
+
+["test_input_range_key_events.html"]
+
+["test_input_range_mouse_and_touch_events.html"]
+
+["test_input_range_rounding.html"]
+
+["test_input_sanitization.html"]
+
+["test_input_setting_value.html"]
+
+["test_input_textarea_set_value_no_scroll.html"]
+
+["test_input_time_key_events.html"]
+
+["test_input_time_sec_millisec_field.html"]
+
+["test_input_types_pref.html"]
+
+["test_input_typing_sanitization.html"]
+
+["test_input_untrusted_key_events.html"]
+
+["test_input_url.html"]
+
+["test_interactive_content_in_label.html"]
+
+["test_interactive_content_in_summary.html"]
+
+["test_label_control_attribute.html"]
+
+["test_label_input_controls.html"]
+
+["test_max_attribute.html"]
+
+["test_maxlength_attribute.html"]
+
+["test_meter_element.html"]
+
+["test_meter_pseudo-classes.html"]
+
+["test_min_attribute.html"]
+
+["test_minlength_attribute.html"]
+
+["test_mozistextfield.html"]
+
+["test_novalidate_attribute.html"]
+
+["test_option_disabled.html"]
+
+["test_option_index_attribute.html"]
+
+["test_option_text.html"]
+
+["test_output_element.html"]
+
+["test_pattern_attribute.html"]
+
+["test_preserving_metadata_between_reloads.html"]
+
+["test_progress_element.html"]
+
+["test_radio_in_label.html"]
+
+["test_radio_radionodelist.html"]
+
+["test_reportValidation_preventDefault.html"]
+
+["test_required_attribute.html"]
+
+["test_restore_form_elements.html"]
+
+["test_save_restore_custom_elements.html"]
+support-files = ["save_restore_custom_elements_sample.html"]
+
+["test_save_restore_radio_groups.html"]
+
+["test_select_change_event.html"]
+skip-if = ["os == 'mac'"]
+
+["test_select_input_change_event.html"]
+skip-if = ["os == 'mac'"]
+
+["test_select_selectedOptions.html"]
+
+["test_select_validation.html"]
+
+["test_set_range_text.html"]
+
+["test_step_attribute.html"]
+
+["test_stepup_stepdown.html"]
+
+["test_textarea_attributes_reflection.html"]
+
+["test_validation.html"]
+
+["test_validation_not_in_doc.html"]
+
+["test_valueasdate_attribute.html"]
+
+["test_valueasnumber_attribute.html"]
diff --git a/dom/html/test/forms/save_restore_custom_elements_sample.html b/dom/html/test/forms/save_restore_custom_elements_sample.html
new file mode 100644
index 0000000000..75dc4c388d
--- /dev/null
+++ b/dom/html/test/forms/save_restore_custom_elements_sample.html
@@ -0,0 +1,43 @@
+<script>
+ class CEBase extends HTMLElement {
+ static formAssociated = true;
+ constructor() {
+ super();
+ this.internals = this.attachInternals();
+ this.state_ = undefined;
+ }
+ formStateRestoreCallback(state, reason) {
+ if (reason == "restore") {
+ this.state_ = state;
+ }
+ }
+ set(state, value) {
+ this.state_ = state;
+ this.value_ = value;
+ this.internals.setFormValue(value, state);
+ }
+ get state() {
+ return this.state_;
+ }
+ get value() {
+ return this.value_;
+ }
+ }
+
+ customElements.define("c-e", class extends CEBase {});
+</script>
+<form>
+ <c-e id="custom0"></c-e>
+ <c-e id="custom1"></c-e>
+ <c-e id="custom2"></c-e>
+ <c-e id="custom3"></c-e>
+ <c-e id="custom4"></c-e>
+ <upgraded-ce id="upgraded0"></upgraded-ce>
+ <upgraded-ce id="upgraded1"></upgraded-ce>
+ <upgraded-ce id="upgraded2"></upgraded-ce>
+ <upgraded-ce id="upgraded3"></upgraded-ce>
+ <upgraded-ce id="upgraded4"></upgraded-ce>
+</form>
+<script>
+ customElements.define("upgraded-ce", class extends CEBase {});
+</script>
diff --git a/dom/html/test/forms/save_restore_radio_groups.sjs b/dom/html/test/forms/save_restore_radio_groups.sjs
new file mode 100644
index 0000000000..b4c9c4401a
--- /dev/null
+++ b/dom/html/test/forms/save_restore_radio_groups.sjs
@@ -0,0 +1,48 @@
+var pages = [
+ "<!DOCTYPE html>" +
+ "<html><body>" +
+ "<form>" +
+ "<input name='a' type='radio' checked><input name='a' type='radio'><input name='a' type='radio'>" +
+ "</form>" +
+ "</body></html>",
+ "<!DOCTYPE html>" +
+ "<html><body>" +
+ "<form>" +
+ "<input name='a' type='radio'><input name='a' type='radio' checked><input name='a' type='radio'>" +
+ "</form>" +
+ "</body></html>",
+];
+
+/**
+ * This SJS is going to send the same page the two first times it will be called
+ * and another page the two following times. After that, the response will have
+ * no content.
+ * The use case is to have two iframes using this SJS and both being reloaded
+ * once.
+ */
+
+function handleRequest(request, response) {
+ var counter = +getState("counter"); // convert to number; +"" === 0
+
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Cache-Control", "no-cache");
+
+ switch (counter) {
+ case 0:
+ case 1:
+ response.write(pages[0]);
+ break;
+ case 2:
+ case 3:
+ response.write(pages[1]);
+ break;
+ }
+
+ // When we finish the test case we need to reset the counter
+ if (counter == 3) {
+ setState("counter", "0");
+ } else {
+ setState("counter", "" + ++counter);
+ }
+}
diff --git a/dom/html/test/forms/submit_invalid_file.sjs b/dom/html/test/forms/submit_invalid_file.sjs
new file mode 100644
index 0000000000..3b4b576ec6
--- /dev/null
+++ b/dom/html/test/forms/submit_invalid_file.sjs
@@ -0,0 +1,13 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Cache-Control", "no-cache");
+
+ var result = {};
+ request.bodyInputStream.search("testfile", true, result, {});
+ if (result.value) {
+ response.write("SUCCESS");
+ } else {
+ response.write("FAIL");
+ }
+}
diff --git a/dom/html/test/forms/test_MozEditableElement_setUserInput.html b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
new file mode 100644
index 0000000000..06380776f6
--- /dev/null
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -0,0 +1,581 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for MozEditableElement.setUserInput()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content"></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+// eslint-disable-next-line complexity
+SimpleTest.waitForFocus(async () => {
+ const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
+
+ let content = document.getElementById("content");
+ /**
+ * Test structure:
+ * element: the tag name to create.
+ * type: the type attribute value for the element. If unnecessary omit it.
+ * input: the values calling setUserInput() with.
+ * before: used when calling setUserInput() before the element gets focus.
+ * after: used when calling setUserInput() after the element gets focus.
+ * result: the results of calling setUserInput().
+ * before: the element's expected value of calling setUserInput() before the element gets focus.
+ * after: the element's expected value of calling setUserInput() after the element gets focus.
+ * fireBeforeInputEvent: true if "beforeinput" event should be fired. Otherwise, false.
+ * fireInputEvent: true if "input" event should be fired. Otherwise, false.
+ */
+ for (let test of [{element: "input", type: "hidden",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "text",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "search",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "tel",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "url",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "email",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "password",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ // "date" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "date",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "month" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "month",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "week" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "week",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "time" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "time",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "datetime-local" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "datetime-local",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "number",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "range",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "color" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "color",
+ input: {before: "#5C5C5C", after: "#FFFFFF"},
+ result: {before: "#5c5c5c", after:"#ffffff", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "checkbox",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "radio",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "file" is not supported by setUserInput? But there is a path...
+ {element: "input", type: "file",
+ input: {before: "3", after: "6"},
+ result: {before: "", after:"", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "submit",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "image",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "reset",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "button",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "textarea",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}]) {
+ let tag =
+ test.type !== undefined ? `<${test.element} type="${test.type}">` :
+ `<${test.element}>`;
+ content.innerHTML =
+ test.element !== "input" ? tag : `${tag}</${test.element}>`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ let inputEvents = [], beforeInputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+
+ // Before setting focus, editor of the element may have not been created yet.
+ let previousValue = target.value;
+ SpecialPowers.wrap(target).setUserInput(test.input.before);
+ if (target.value == previousValue && test.result.before != previousValue) {
+ todo_is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
+ } else {
+ is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
+ }
+ if (target.value == previousValue) {
+ if (test.type === "date" || test.type === "time" || test.type === "datetime-local") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ } else {
+ if (!test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents.length, 0,
+ `No "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ if (!test.result.fireInputEvent) {
+ // HTML spec defines that "input" elements whose type are "hidden",
+ // "submit", "image", "reset" and "button" shouldn't fire input event
+ // when its value is changed.
+ // XXX Perhaps, we shouldn't support setUserInput() with such types.
+ if (test.type === "hidden" ||
+ test.type === "submit" ||
+ test.type === "image" ||
+ test.type === "reset" ||
+ test.type === "button") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ } else {
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ }
+ if (inputEvents.length) {
+ if (SpecialPowers.wrap(target).isInputEventTarget) {
+ if (test.type === "time") {
+ todo(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ if (beforeInputEvents.length && test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents[0].cancelable, kSetUserInputCancelable,
+ `"beforeinput" event for "insertReplacementText" should be cancelable when setUserInput("${test.input.before}") is called before ${tag} gets focus unless it's suppressed by the pref`);
+ is(beforeInputEvents[0].inputType, "insertReplacementText",
+ `inputType of "beforeinput"event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(beforeInputEvents[0].data, test.input.before,
+ `data of "beforeinput" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(beforeInputEvents[0].dataTransfer, null,
+ `dataTransfer of "beforeinput" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(beforeInputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "beforeinput" event should return empty array when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ ok(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].inputType, "insertReplacementText",
+ `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].data, test.input.before,
+ `data of "input" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].dataTransfer, null,
+ `dataTransfer of "input" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "input" event should return empty array when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ } else {
+ ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
+ `"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ is(inputEvents[0].cancelable, false,
+ `"input" event should be never cancelable (${tag}, before getting focus)`);
+ is(inputEvents[0].bubbles, true,
+ `"input" event should always bubble (${tag}, before getting focus)`);
+ }
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ target.focus();
+ previousValue = target.value;
+ SpecialPowers.wrap(target).setUserInput(test.input.after);
+ if (target.value == previousValue && test.result.after != previousValue) {
+ todo_is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
+ } else {
+ is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
+ }
+ if (target.value == previousValue) {
+ if (test.type === "date" || test.type === "time" || test.type === "datetime-local") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ } else {
+ if (!test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents.length, 0,
+ `No "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ if (!test.result.fireInputEvent) {
+ // HTML spec defines that "input" elements whose type are "hidden",
+ // "submit", "image", "reset" and "button" shouldn't fire input event
+ // when its value is changed.
+ // XXX Perhaps, we shouldn't support setUserInput() with such types.
+ if (test.type === "hidden" ||
+ test.type === "submit" ||
+ test.type === "image" ||
+ test.type === "reset" ||
+ test.type === "button") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ } else {
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ }
+ if (inputEvents.length) {
+ if (SpecialPowers.wrap(target).isInputEventTarget) {
+ if (test.type === "time") {
+ todo(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ if (beforeInputEvents.length && test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents[0].cancelable, kSetUserInputCancelable,
+ `"beforeinput" event should be cancelable when setUserInput("${test.input.after}") is called after ${tag} gets focus unless it's suppressed by the pref`);
+ is(beforeInputEvents[0].inputType, "insertReplacementText",
+ `inputType of "beforeinput" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(beforeInputEvents[0].data, test.input.after,
+ `data of "beforeinput" should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(beforeInputEvents[0].dataTransfer, null,
+ `dataTransfer of "beforeinput" should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(beforeInputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "beforeinput" should return empty array when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ ok(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].inputType, "insertReplacementText",
+ `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].data, test.input.after,
+ `data of "input" event should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].dataTransfer, null,
+ `dataTransfer of "input" event should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "input" event should return empty array when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ } else {
+ ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
+ `"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ is(inputEvents[0].cancelable, false,
+ `"input" event should be never cancelable (${tag}, after getting focus)`);
+ is(inputEvents[0].bubbles, true,
+ `"input" event should always bubble (${tag}, after getting focus)`);
+ }
+
+ target.removeEventListener("input", onInput);
+ }
+
+ function testValidationMessage(aType, aInvalidValue, aValidValue) {
+ let tag = `<input type="${aType}">`
+ content.innerHTML = tag;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ let inputEvents = [];
+ let validationMessage = "";
+
+ function reset() {
+ inputEvents = [];
+ validationMessage = "";
+ }
+
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ validationMessage = aEvent.target.validationMessage;
+ }
+ target.addEventListener("input", onInput);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+ is(validationMessage, "",
+ `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+ ok(!target.matches(":invalid"),
+ `The target shouldn't have invalid pseudo class when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+
+ target.value = "";
+ target.focus();
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+ is(validationMessage, "",
+ `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+ ok(!target.matches(":invalid"),
+ `The target shouldn't have invalid pseudo class when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+
+ target.removeEventListener("input", onInput);
+ }
+ testValidationMessage("email", "f", "foo@example.com");
+
+ function testValueMissing(aType, aValidValue) {
+ let tag = aType === "textarea" ? "<textarea required>" : `<input type="${aType}" required>`;
+ content.innerHTML = `${tag}${aType === "textarea" ? "</textarea>" : ""}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ let inputEvents = [], beforeInputEvents = [];
+ function reset() {
+ beforeInputEvents = [];
+ inputEvents = [];
+ }
+
+ function onBeforeInput(aEvent) {
+ aEvent.validity = aEvent.target.checkValidity();
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ aEvent.validity = aEvent.target.checkValidity();
+ inputEvents.push(aEvent);
+ }
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(beforeInputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "beforeinput" event (before gets focus)`);
+ if (beforeInputEvents.length) {
+ is(beforeInputEvents[0].validity, false,
+ `The ${tag} should be invalid at "beforeinput" event (before gets focus)`);
+ }
+ is(inputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "input" event (before gets focus)`);
+ if (inputEvents.length) {
+ is(inputEvents[0].validity, true,
+ `The ${tag} should be valid at "input" event (before gets focus)`);
+ }
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+
+ content.innerHTML = "";
+ content.scrollTop; // Flush pending layout.
+ content.innerHTML = `${tag}${aType === "textarea" ? "</textarea>" : ""}`;
+ content.scrollTop; // Flush pending layout.
+ target = content.firstChild;
+
+ target.focus();
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(beforeInputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "beforeinput" event (after gets focus)`);
+ if (beforeInputEvents.length) {
+ is(beforeInputEvents[0].validity, false,
+ `The ${tag} should be invalid at "beforeinput" event (after gets focus)`);
+ }
+ is(inputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "input" event (after gets focus)`);
+ if (inputEvents.length) {
+ is(inputEvents[0].validity, true,
+ `The ${tag} should be valid at "input" event (after gets focus)`);
+ }
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+ }
+ testValueMissing("text", "abc");
+ testValueMissing("password", "abc");
+ testValueMissing("textarea", "abc");
+ testValueMissing("email", "foo@example.com");
+ testValueMissing("url", "https://example.com/");
+
+ function testEditorValueAtEachEvent(aType) {
+ let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">`
+ let closeTag = aType === "textarea" ? "</textarea>" : "";
+ content.innerHTML = `${tag}${closeTag}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+ target.value = "Old Value";
+ let description = `Setting new value of ${tag} before setting focus: `;
+ let onBeforeInput = (aEvent) => {
+ is(target.value, "Old Value",
+ `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ };
+ let onInput = (aEvent) => {
+ is(target.value, "New Value",
+ `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
+ };
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ description = `Setting new value of ${tag} after setting focus: `;
+ target.value = "Old Value";
+ target.focus();
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+
+ // FYI: This is not realistic situation because we should do nothing
+ // while user composing IME.
+ // TODO: TextControlState should stop returning setting value as the value
+ // while committing composition.
+ description = `Setting new value of ${tag} during composition: `;
+ target.value = "";
+ target.focus();
+ synthesizeCompositionChange({
+ composition: {
+ string: "composition string",
+ clauses: [{length: 18, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ },
+ caret: {start: 18, length: 0},
+ });
+ let onCompositionUpdate = (aEvent) => {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "compositionupdate" event yet (data: "${aEvent.data}")`);
+ };
+ let onCompositionEnd = (aEvent) => {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "compositionupdate" event yet (data: "${aEvent.data}")`);
+ };
+ onBeforeInput = (aEvent) => {
+ if (aEvent.inputType === "insertCompositionText") {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ } else {
+ is(target.value, "composition string",
+ `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ }
+ };
+ onInput = (aEvent) => {
+ if (aEvent.inputType === "insertCompositionText") {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "input" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ } else {
+ is(target.value, "New Value",
+ `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
+ }
+ };
+ target.addEventListener("compositionupdate", onCompositionUpdate);
+ target.addEventListener("compositionend", onCompositionEnd);
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+ SpecialPowers.wrap(target).setUserInput("New Value");
+ target.removeEventListener("compositionupdate", onCompositionUpdate);
+ target.removeEventListener("compositionend", onCompositionEnd);
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+ }
+ testEditorValueAtEachEvent("text");
+ testEditorValueAtEachEvent("textarea");
+
+ async function testBeforeInputCancelable(aType) {
+ let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">`
+ let closeTag = aType === "textarea" ? "</textarea>" : "";
+ for (const kShouldBeCancelable of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.input_event.allow_to_cancel_set_user_input", kShouldBeCancelable]],
+ });
+
+ content.innerHTML = `${tag}${closeTag}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+ target.value = "Old Value";
+ let description = `Setting new value of ${tag} before setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `;
+ let onBeforeInput = (aEvent) => {
+ is(aEvent.cancelable, kShouldBeCancelable,
+ `${description}The "beforeinput" event should be ${kShouldBeCancelable ? "cancelable" : "not be cancelable due to suppressed by the pref"}`);
+ };
+ let onInput = (aEvent) => {
+ is(aEvent.cancelable, false,
+ `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
+ };
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ description = `Setting new value of ${tag} after setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `;
+ target.value = "Old Value";
+ target.focus();
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+ }
+
+ await SpecialPowers.clearUserPref({
+ clear: [["dom.input_event.allow_to_cancel_set_user_input"]],
+ });
+ }
+ await testBeforeInputCancelable("text");
+ await testBeforeInputCancelable("textarea");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_autocomplete.html b/dom/html/test/forms/test_autocomplete.html
new file mode 100644
index 0000000000..c98be94eea
--- /dev/null
+++ b/dom/html/test/forms/test_autocomplete.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test @autocomplete on <input>/<select>/<textarea>
+-->
+<head>
+ <title>Test for @autocomplete</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+var values = [
+ // @autocomplete content attribute, expected IDL attribute value
+
+ // Missing or empty attribute
+ [undefined, ""],
+ ["", ""],
+
+ // One token
+ ["on", "on"],
+ ["On", "on"],
+ ["off", "off"],
+ ["OFF", "off"],
+ ["name", "name"],
+ [" name ", "name"],
+ ["username", "username"],
+ [" username ", "username"],
+ ["cc-csc", ""],
+ ["one-time-code", ""],
+ ["language", ""],
+ [" language ", ""],
+ ["tel-extension", ""],
+ ["foobar", ""],
+ ["section-blue", ""],
+ [" WEBAUTHN ", "webauthn"],
+
+ // One token + WebAuthn credential type
+ ["on webauthn", ""],
+ ["off webauthn", ""],
+ ["webauthn webauthn", ""],
+ ["username WebAuthn", "username webauthn"],
+ ["current-PASSWORD webauthn", "current-password webauthn"],
+
+ // Two tokens
+ ["on off", ""],
+ ["off on", ""],
+ ["username tel", ""],
+ ["tel username ", ""],
+ [" username tel ", ""],
+ ["tel mobile", ""],
+ ["tel shipping", ""],
+ ["shipping tel", "shipping tel"],
+ ["shipPING tel", "shipping tel"],
+ ["mobile tel", "mobile tel"],
+ [" MoBiLe TeL ", "mobile tel"],
+ ["pager impp", ""],
+ ["fax tel-extension", ""],
+ ["XXX tel", ""],
+ ["XXX username", ""],
+ ["name section-blue", ""],
+ ["scetion-blue cc-name", ""],
+ ["pager language", ""],
+ ["fax url", ""],
+ ["section-blue name", "section-blue name"],
+ ["section-blue tel", "section-blue tel"],
+ ["webauthn username", ""],
+
+ // Two tokens + WebAuthn credential type
+ ["fax url webauthn", ""],
+ ["shipping tel webauthn", "shipping tel webauthn"],
+
+ // Three tokens
+ ["billing invalid tel", ""],
+ ["___ mobile tel", ""],
+ ["mobile foo tel", ""],
+ ["mobile tel foo", ""],
+ ["tel mobile billing", ""],
+ ["billing mobile tel", "billing mobile tel"],
+ [" BILLing MoBiLE tEl ", "billing mobile tel"],
+ ["billing home tel", "billing home tel"],
+ ["home section-blue tel", ""],
+ ["setion-blue work email", ""],
+ ["section-blue home address-level2", ""],
+ ["section-blue shipping name", "section-blue shipping name"],
+ ["section-blue mobile tel", "section-blue mobile tel"],
+ ["shipping webauthn tel", ""],
+
+ // Three tokens + WebAuthn credential type
+ ["invalid mobile tel webauthn", ""],
+ ["section-blue shipping name webauthn", "section-blue shipping name webauthn"],
+
+ // Four tokens
+ ["billing billing mobile tel", ""],
+ ["name section-blue shipping home", ""],
+ ["secti shipping work address-line1", ""],
+ ["section-blue shipping home name", ""],
+ ["section-blue shipping mobile tel", "section-blue shipping mobile tel"],
+ ["section-blue webauthn mobile tel", ""],
+
+ // Four tokens + WebAuthn credential type
+ ["section-blue shipping home name webauthn", ""],
+ ["section-blue shipping mobile tel webauthn", "section-blue shipping mobile tel webauthn"],
+
+ // Five tokens (invalid)
+ ["billing billing billing mobile tel", ""],
+ ["section-blue section-blue billing mobile tel", ""],
+ ["section-blue section-blue billing webauthn tel", ""],
+
+ // Five tokens + WebAuthn credential type (invalid)
+ ["billing billing billing mobile tel webauthn", ""],
+];
+
+var types = [undefined, "hidden", "text", "search"]; // Valid types for all non-multiline hints.
+
+function checkAutocompleteValues(field, type) {
+ for (var test of values) {
+ if (typeof(test[0]) === "undefined")
+ field.removeAttribute("autocomplete");
+ else
+ field.setAttribute("autocomplete", test[0]);
+ is(field.autocomplete, test[1], "Checking @autocomplete for @type=" + type + " of: " + test[0]);
+ is(field.autocomplete, test[1], "Checking cached @autocomplete for @type=" + type + " of: " + test[0]);
+ }
+}
+
+function start() {
+ var inputField = document.getElementById("input-field");
+ for (var type of types) {
+ // Switch the input type
+ if (typeof(type) === "undefined")
+ inputField.removeAttribute("type");
+ else
+ inputField.type = type;
+ checkAutocompleteValues(inputField, type || "");
+ }
+
+ var selectField = document.getElementById("select-field");
+ checkAutocompleteValues(selectField, "select");
+
+ var textarea = document.getElementById("textarea");
+ checkAutocompleteValues(textarea, "textarea");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", true]]}, start);
+</script>
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form>
+ <input id="input-field" />
+ <select id="select-field" />
+ <textarea id="textarea"></textarea>
+ </form>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_autocompleteinfo.html b/dom/html/test/forms/test_autocompleteinfo.html
new file mode 100644
index 0000000000..a3357ac8de
--- /dev/null
+++ b/dom/html/test/forms/test_autocompleteinfo.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test getAutocompleteInfo() on <input> and <select>
+-->
+<head>
+ <title>Test for getAutocompleteInfo()</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.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" style="display: none">
+ <form>
+ <input id="input"/>
+ <select id="select" />
+ </form>
+</div>
+<pre id="test">
+<script>
+"use strict";
+
+var values = [
+ // Missing or empty attribute
+ [undefined, {}, ""],
+ ["", {}, ""],
+
+ // One token
+ ["on", {fieldName: "on" }, "on"],
+ ["On", {fieldName: "on" }, "on"],
+ ["off", {fieldName: "off", canAutomaticallyPersist: false}, "off" ],
+ ["name", {fieldName: "name" }, "name"],
+ [" name ", {fieldName: "name" }, "name"],
+ ["username", {fieldName: "username"}, "username"],
+ [" username ", {fieldName: "username"}, "username"],
+ ["current-password", {fieldName: "current-password", canAutomaticallyPersist: false}, "current-password"],
+ ["new-password", {fieldName: "new-password", canAutomaticallyPersist: false}, "new-password"],
+ ["cc-number", {fieldName: "cc-number", canAutomaticallyPersist: false}, "cc-number"],
+ ["cc-csc", {fieldName: "cc-csc", canAutomaticallyPersist: false}, ""],
+ ["one-time-code", {fieldName: "one-time-code", canAutomaticallyPersist: false}, ""],
+ ["language", {fieldName: "language"}, ""],
+ [" language ", {fieldName: "language"}, ""],
+ ["tel-extension", {fieldName: "tel-extension"}, ""],
+ ["foobar", {}, ""],
+ ["section-blue", {}, ""],
+ [" WEBAUTHN ", {fieldName: "webauthn", credentialType: "webauthn"}, "webauthn"],
+
+ // One token + WebAuthn credential type
+ ["on webauthn", {}, ""],
+ ["off webauthn", {}, ""],
+ ["webauthn webauthn", {}, ""],
+ ["username WebAuthn", {fieldName: "username", credentialType: "webauthn"}, "username webauthn"],
+ ["current-PASSWORD webauthn", {fieldName: "current-password", credentialType: "webauthn", canAutomaticallyPersist: false}, "current-password webauthn"],
+
+ // Two tokens
+ ["on off", {}, ""],
+ ["off on", {}, ""],
+ ["username tel", {}, ""],
+ ["tel username ", {}, ""],
+ [" username tel ", {}, ""],
+ ["tel mobile", {}, ""],
+ ["tel shipping", {}, ""],
+ ["shipping tel", {addressType: "shipping", fieldName: "tel"}, "shipping tel"],
+ ["shipPING tel", {addressType: "shipping", fieldName: "tel"}, "shipping tel"],
+ ["mobile tel", {contactType: "mobile", fieldName: "tel"}, "mobile tel"],
+ [" MoBiLe TeL ", {contactType: "mobile", fieldName: "tel"}, "mobile tel"],
+ ["pager impp", {contactType: "pager", fieldName: "impp"}, ""],
+ ["fax tel-extension", {contactType: "fax", fieldName: "tel-extension"}, ""],
+ ["XXX tel", {}, ""],
+ ["XXX username", {}, ""],
+ ["name section-blue", {}, ""],
+ ["scetion-blue cc-name", {}, ""],
+ ["pager language", {}, ""],
+ ["fax url", {}, ""],
+ ["section-blue name", {section: "section-blue", fieldName: "name"}, "section-blue name"],
+ ["section-blue tel", {section: "section-blue", fieldName: "tel"}, "section-blue tel"],
+ ["webauthn username", {}, ""],
+
+ // Two tokens + WebAuthn credential type
+ ["fax url webauthn", {}, ""],
+ ["shipping tel webauthn", {addressType: "shipping", fieldName: "tel", credentialType: "webauthn"}, "shipping tel webauthn"],
+
+ // Three tokens
+ ["billing invalid tel", {}, ""],
+ ["___ mobile tel", {}, ""],
+ ["mobile foo tel", {}, ""],
+ ["mobile tel foo", {}, ""],
+ ["tel mobile billing", {}, ""],
+ ["billing mobile tel", {addressType: "billing", contactType: "mobile", fieldName: "tel"}, "billing mobile tel"],
+ [" BILLing MoBiLE tEl ", {addressType: "billing", contactType: "mobile", fieldName: "tel"}, "billing mobile tel"],
+ ["billing home tel", {addressType: "billing", contactType: "home", fieldName: "tel"}, "billing home tel"],
+ ["home section-blue tel", {}, ""],
+ ["setion-blue work email", {}, ""],
+ ["section-blue home address-level2", {}, ""],
+ ["section-blue shipping name", {section: "section-blue", addressType: "shipping", fieldName: "name"}, "section-blue shipping name"],
+ ["section-blue mobile tel", {section: "section-blue", contactType: "mobile", fieldName: "tel"}, "section-blue mobile tel"],
+ ["shipping webauthn tel", {}, ""],
+
+ // Three tokens + WebAuthn credential type
+ ["invalid mobile tel webauthn", {}, ""],
+ ["section-blue shipping name webauthn", {section: "section-blue", addressType: "shipping", fieldName: "name", credentialType: "webauthn"}, "section-blue shipping name webauthn"],
+
+ // Four tokens
+ ["billing billing mobile tel", {}, ""],
+ ["name section-blue shipping home", {}, ""],
+ ["secti shipping work address-line1", {}, ""],
+ ["section-blue shipping home name", {}, ""],
+ ["section-blue shipping mobile tel", {section: "section-blue", addressType: "shipping", contactType: "mobile", fieldName: "tel"}, "section-blue shipping mobile tel"],
+ ["section-blue webauthn mobile tel", {}, ""],
+
+ // Four tokens + WebAuthn credential type
+ ["section-blue shipping home name webauthn", {}, ""],
+ ["section-blue shipping mobile tel webauthn", {section: "section-blue", addressType: "shipping", contactType: "mobile", fieldName: "tel", credentialType: "webauthn"}, "section-blue shipping mobile tel webauthn"],
+
+ // Five tokens (invalid)
+ ["billing billing billing mobile tel", {}, ""],
+ ["section-blue section-blue billing mobile tel", {}, ""],
+ ["section-blue section-blue billing webauthn tel", {}, ""],
+
+ // Five tokens + WebAuthn credential type (invalid)
+ ["billing billing billing mobile tel webauthn", {}, ""],
+];
+
+var autocompleteInfoFieldIds = ["input", "select"];
+var autocompleteEnabledTypes = ["hidden", "text", "search", "url", "tel",
+ "email", "password", "date", "time", "number",
+ "range", "color"];
+var autocompleteDisabledTypes = ["reset", "submit", "image", "button", "radio",
+ "checkbox", "file"];
+
+function testInputTypes() {
+ let field = document.getElementById("input");
+
+ for (var type of autocompleteEnabledTypes) {
+ testAutocomplete(field, type, true);
+ }
+
+ for (var type of autocompleteDisabledTypes) {
+ testAutocomplete(field, type, false);
+ }
+
+ // Clear input type attribute.
+ field.removeAttribute("type");
+}
+
+function testAutocompleteInfoValue(aEnabled) {
+ for (var fieldId of autocompleteInfoFieldIds) {
+ let field = document.getElementById(fieldId);
+
+ for (var test of values) {
+ if (typeof(test[0]) === "undefined")
+ field.removeAttribute("autocomplete");
+ else
+ field.setAttribute("autocomplete", test[0]);
+
+ var info = field.getAutocompleteInfo();
+ if (aEnabled) {
+ // We need to consider if getAutocompleteInfo() is valid,
+ // but @autocomplete is invalid case, because @autocomplete
+ // has smaller set of values.
+ is(field.autocomplete, test[2], "Checking @autocomplete of: " + test[0]);
+ }
+
+ is(info.section, "section" in test[1] ? test[1].section : "",
+ "Checking autocompleteInfo.section for " + field + ": " + test[0]);
+ is(info.addressType, "addressType" in test[1] ? test[1].addressType : "",
+ "Checking autocompleteInfo.addressType for " + field + ": " + test[0]);
+ is(info.contactType, "contactType" in test[1] ? test[1].contactType : "",
+ "Checking autocompleteInfo.contactType for " + field + ": " + test[0]);
+ is(info.fieldName, "fieldName" in test[1] ? test[1].fieldName : "",
+ "Checking autocompleteInfo.fieldName for " + field + ": " + test[0]);
+ is(info.credentialType, "credentialType" in test[1] ? test[1].credentialType: "",
+ "Checking autocompleteInfo.credentialType for " + field + ": " + test[0]);
+ is(info.canAutomaticallyPersist, "canAutomaticallyPersist" in test[1] ? test[1].canAutomaticallyPersist : true,
+ "Checking autocompleteInfo.canAutomaticallyPersist for " + field + ": " + test[0]);
+ }
+ }
+}
+
+function testAutocomplete(aField, aType, aEnabled) {
+ aField.type = aType;
+ if (aEnabled) {
+ ok(aField.getAutocompleteInfo() !== null, "getAutocompleteInfo shouldn't return null");
+ } else {
+ is(aField.getAutocompleteInfo(), null, "getAutocompleteInfo should return null");
+ }
+}
+
+// getAutocompleteInfo() should be able to parse all tokens as defined
+// in the spec regardless of whether dom.forms.autocomplete.formautofill pref
+// is on or off.
+add_task(async function testAutocompletePreferenceEnabled() {
+ await SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", true]]}, testInputTypes);
+ testAutocompleteInfoValue(true);
+});
+
+add_task(async function testAutocompletePreferenceDisabled() {
+ await SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", false]]}, testInputTypes);
+ testAutocompleteInfoValue(false);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_bug1039548.html b/dom/html/test/forms/test_bug1039548.html
new file mode 100644
index 0000000000..cea3cd67ef
--- /dev/null
+++ b/dom/html/test/forms/test_bug1039548.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1039548
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1039548</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"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1039548 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(test);
+
+ var didTryToSubmit;
+ function test() {
+ var r = document.getElementById("radio");
+ r.focus();
+ didTryToSubmit = false;
+ sendKey("return");
+ ok(!didTryToSubmit, "Shouldn't have tried to submit!");
+
+ var t = document.getElementById("text");
+ t.focus();
+ didTryToSubmit = false;
+ sendKey("return");
+ ok(didTryToSubmit, "Should have tried to submit!");
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1039548">Mozilla Bug 1039548</a>
+<p id="display"></p>
+<div id="content">
+
+ <form onsubmit="didTryToSubmit = true; event.preventDefault();">
+ <input type="radio" id="radio">
+ </form>
+
+ <form onsubmit="didTryToSubmit = true; event.preventDefault();">
+ <input type="text" id="text">
+ </form>
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_bug1283915.html b/dom/html/test/forms/test_bug1283915.html
new file mode 100644
index 0000000000..90bffd4b20
--- /dev/null
+++ b/dom/html/test/forms/test_bug1283915.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1283915
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1283915</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"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1283915 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function isCursorAtEnd(field){
+ is(field.selectionStart, field.value.length);
+ is(field.selectionEnd, field.value.length);
+ }
+
+ function test() {
+ var tField = document.getElementById("textField");
+ tField.focus();
+
+ sendString("a");
+ is(tField.value, "a");
+ isCursorAtEnd(tField);
+ document.body.offsetWidth; // frame must be created after type change
+
+ sendString("b");
+ is(tField.value, "ab");
+ isCursorAtEnd(tField);
+
+ sendString("c");
+ is(tField.value, "abc");
+ isCursorAtEnd(tField);
+
+ var nField = document.getElementById("numField");
+ nField.focus();
+
+ sendString("1");
+ is(nField.value, "1");
+ document.body.offsetWidth;
+
+ sendString("2");
+ is(nField.value, "12");
+
+ sendString("3");
+ is(nField.value, "123");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(test);
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1283915">Mozilla Bug 1283915</a>
+<p id="display"></p>
+<input id="textField" type="text" oninput="if (this.type !='password') this.type = 'password';">
+<input id="numField" type="text" oninput="if (this.type !='number') this.type = 'number';">
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_bug1286509.html b/dom/html/test/forms/test_bug1286509.html
new file mode 100644
index 0000000000..638e7fe85c
--- /dev/null
+++ b/dom/html/test/forms/test_bug1286509.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1286509
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1286509</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286509">Mozilla Bug 1286509</a>
+<p id="display"></p>
+<div id="content">
+ <input type="range" id="test_input" min="0" max="10" value="5">
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ /** Test for Bug 1286509 **/
+ SimpleTest.waitForExplicitFinish();
+ var expectedEventSequence = ['keydown', 'change', 'keyup'];
+ var eventCounts = {};
+ var expectedEventIdx = 0;
+
+ function test() {
+ var range = document.getElementById("test_input");
+ range.focus();
+ expectedEventSequence.forEach((eventName) => {
+ eventCounts[eventName] = 0;
+ range.addEventListener(eventName, (e) => {
+ ++eventCounts[eventName];
+ is(expectedEventSequence[expectedEventIdx], e.type, "Events sequence should be keydown, change, keyup");
+ expectedEventIdx = (expectedEventIdx + 1) % 3;
+ });
+ });
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_ArrowRight");
+ is(eventCounts.change, 4, "Expect key up/down/left/right should trigger range input to fire change events");
+ SimpleTest.finish();
+ }
+ addLoadEvent(test);
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_button_attributes_reflection.html b/dom/html/test/forms/test_button_attributes_reflection.html
new file mode 100644
index 0000000000..de2097cb4c
--- /dev/null
+++ b/dom/html/test/forms/test_button_attributes_reflection.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLButtonElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLButtonElement attributes reflection **/
+
+// .autofocus
+reflectBoolean({
+ element: document.createElement("button"),
+ attribute: "autofocus",
+});
+
+// .disabled
+reflectBoolean({
+ element: document.createElement("button"),
+ attribute: "disabled",
+});
+
+// .formAction
+reflectURL({
+ element: document.createElement("button"),
+ attribute: "formAction",
+});
+
+// .formEnctype
+reflectLimitedEnumerated({
+ element: document.createElement("button"),
+ attribute: "formEnctype",
+ validValues: [
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ "text/plain",
+ ],
+ invalidValues: [ "text/html", "", "tulip" ],
+ defaultValue: {
+ invalid: "application/x-www-form-urlencoded",
+ missing: "",
+ }
+});
+
+// .formMethod
+add_task(async function() {
+ reflectLimitedEnumerated({
+ element: document.createElement("button"),
+ attribute: "formMethod",
+ validValues: [ "get", "post", "dialog"],
+ invalidValues: [ "put", "", "tulip" ],
+ defaultValue: {
+ invalid: "get",
+ missing: "",
+ }
+ });
+});
+
+// .formNoValidate
+reflectBoolean({
+ element: document.createElement("button"),
+ attribute: "formNoValidate",
+});
+
+// .formTarget
+reflectString({
+ element: document.createElement("button"),
+ attribute: "formTarget",
+ otherValues: [ "_blank", "_self", "_parent", "_top" ],
+});
+
+// .name
+reflectString({
+ element: document.createElement("button"),
+ attribute: "name",
+ otherValues: [ "isindex", "_charset_" ]
+});
+
+// .type
+reflectLimitedEnumerated({
+ element: document.createElement("button"),
+ attribute: "type",
+ validValues: [ "submit", "reset", "button" ],
+ invalidValues: [ "this-is-probably-a-wrong-type", "", "tulip" ],
+ unsupportedValues: [ "menu" ],
+ defaultValue: "submit",
+});
+
+// .value
+reflectString({
+ element: document.createElement("button"),
+ attribute: "value",
+});
+
+// .willValidate
+ok("willValidate" in document.createElement("button"),
+ "willValidate should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").willValidate), "boolean",
+ "button.willValidate should be a boolean");
+
+// .validity
+ok("validity" in document.createElement("button"),
+ "validity should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").validity), "object",
+ "button.validity should be an object");
+ok(document.createElement("button").validity instanceof ValidityState,
+ "button.validity sohuld be an instance of ValidityState");
+
+// .validationMessage
+ok("validationMessage" in document.createElement("button"),
+ "validationMessage should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").validationMessage), "string",
+ "button.validationMessage should be a string");
+
+// .checkValidity()
+ok("checkValidity" in document.createElement("button"),
+ "checkValidity() should be a method of the button element");
+is(typeof(document.createElement("button").checkValidity), "function",
+ "button.checkValidity should be a function");
+
+// .setCustomValidity()
+ok("setCustomValidity" in document.createElement("button"),
+ "setCustomValidity() should be a method of the button element");
+is(typeof(document.createElement("button").setCustomValidity), "function",
+ "button.setCustomValidity should be a function");
+
+// .labels
+ok("labels" in document.createElement("button"),
+ "button.labels should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").labels), "object",
+ "button.labels should be an object");
+ok(document.createElement("button").labels instanceof NodeList,
+ "button.labels sohuld be an instance of NodeList");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_change_event.html b/dom/html/test/forms/test_change_event.html
new file mode 100644
index 0000000000..8be4554c58
--- /dev/null
+++ b/dom/html/test/forms/test_change_event.html
@@ -0,0 +1,286 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=722599
+-->
+<head>
+<title>Test for Bug 722599</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=722599">Mozilla Bug 722599</a>
+<p id="display"></p>
+<div id="content">
+<input type="file" id="fileInput"></input>
+<textarea id="textarea" onchange="++textareaChange;"></textarea>
+<input type="text" id="input_text" onchange="++textInputChange[0];"></input>
+<input type="email" id="input_email" onchange="++textInputChange[1];"></input>
+<input type="search" id="input_search" onchange="++textInputChange[2];"></input>
+<input type="tel" id="input_tel" onchange="++textInputChange[3];"></input>
+<input type="url" id="input_url" onchange="++textInputChange[4];"></input>
+<input type="password" id="input_password" onchange="++textInputChange[5];"></input>
+
+<!-- "Non-text" inputs-->
+<input type="button" id="input_button" onchange="++NonTextInputChange[0];"></input>
+<input type="submit" id="input_submit" onchange="++NonTextInputChange[1];"></input>
+<input type="image" id="input_image" onchange="++NonTextInputChange[2];"></input>
+<input type="reset" id="input_reset" onchange="++NonTextInputChange[3];"></input>
+<input type="radio" id="input_radio" onchange="++NonTextInputChange[4];"></input>
+<input type="checkbox" id="input_checkbox" onchange="++NonTextInputChange[5];"></input>
+<input type="number" id="input_number" onchange="++numberChange;"></input>
+<input type="range" id="input_range" onchange="++rangeChange;"></input>
+
+<!-- Input text with default value and blurs on focus-->
+<input type="text" id="input_text_value" onchange="++textInputValueChange"
+ onfocus="this.blur();" value="foo"></input>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Test for Bug 722599 **/
+
+ const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+ var textareaChange = 0;
+ var fileInputChange = 0;
+ var textInputValueChange = 0;
+
+ var textInputTypes = ["text", "email", "search", "tel", "url", "password"];
+ var textInputChange = [0, 0, 0, 0, 0, 0];
+
+ var NonTextInputTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
+ var NonTextInputChange = [0, 0, 0, 0, 0, 0];
+
+ var numberChange = 0;
+ var rangeChange = 0;
+
+ var blurTestCalled = false; //Sentinel to prevent infinite loop.
+
+ SimpleTest.waitForExplicitFinish();
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ function fileInputBlurTest() {
+ var btn = document.getElementById('fileInput');
+ btn.focus()
+ btn.blur();
+ is(fileInputChange, 1, "change event shouldn't be dispatched on blur for file input element(1)");
+ }
+
+ function testUserInput() {
+ //Simulating an OK click and with a file name return.
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ var input = document.getElementById('fileInput');
+ input.focus();
+
+ input.addEventListener("change", function (aEvent) {
+ ++fileInputChange;
+ if (!blurTestCalled) {
+ is(fileInputChange, 1, "change event should have been dispatched on file input.");
+ blurTestCalled = true;
+ fileInputBlurTest();
+ }
+ else {
+ is(fileInputChange, 1, "change event shouldn't be dispatched on blur for file input element (2)");
+ }
+ });
+ input.click();
+ // blur the file input, we can't use blur() because of bug 760283
+ document.getElementById('input_text').focus();
+ setTimeout(testUserInput2, 0);
+ }
+
+ function testUserInput2() {
+ var input = document.getElementById('fileInput');
+ // remove it, otherwise cleanup() opens a native file picker!
+ input.remove();
+ MockFilePicker.cleanup();
+
+ //text, email, search, telephone, url & password input tests
+ for (var i = 0; i < textInputTypes.length; ++i) {
+ input = document.getElementById("input_" + textInputTypes[i]);
+ input.focus();
+ synthesizeKey("KEY_Enter");
+ is(textInputChange[i], 0, "Change event shouldn't be dispatched on " + textInputTypes[i] + " input element");
+
+ sendString("m");
+ synthesizeKey("KEY_Enter");
+ is(textInputChange[i], 1, textInputTypes[i] + " input element should have dispatched change event.");
+ }
+
+ //focus and blur text input
+ input = document.getElementById("input_text");
+ input.focus();
+ sendString("f");
+ input.blur();
+ is(textInputChange[0], 2, "text input element should have dispatched change event (2).");
+
+ // value being set while focused
+ input.focus();
+ input.value = 'foo';
+ input.blur();
+ is(textInputChange[0], 2, "text input element should not have dispatched change event (2).");
+
+ // value being set while focused after being modified manually
+ input.focus();
+ sendString("f");
+ input.value = 'bar';
+ input.blur();
+ is(textInputChange[0], 3, "text input element should have dispatched change event (3).");
+
+ //focus and blur textarea
+ var textarea = document.getElementById("textarea");
+ textarea.focus();
+ sendString("f");
+ textarea.blur();
+ is(textareaChange, 1, "Textarea element should have dispatched change event.");
+
+ // value being set while focused
+ textarea.focus();
+ textarea.value = 'foo';
+ textarea.blur();
+ is(textareaChange, 1, "textarea should not have dispatched change event (1).");
+
+ // value being set while focused after being modified manually
+ textarea.focus();
+ sendString("f");
+ textarea.value = 'bar';
+ textarea.blur();
+ is(textareaChange, 2, "textearea should have dispatched change event (2).");
+
+ //Non-text input tests:
+ for (var i = 0; i < NonTextInputTypes.length; ++i) {
+ //button, submit, image and reset input type tests.
+ if (i < 4) {
+ input = document.getElementById("input_" + NonTextInputTypes[i]);
+ input.focus();
+ input.click();
+ is(NonTextInputChange[i], 0, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element");
+ input.blur();
+ is(NonTextInputChange[i], 0, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element(2)");
+ }
+ //for radio and and checkboxes, we require that change event should ONLY be dispatched on setting the value.
+ else {
+ input = document.getElementById("input_" + NonTextInputTypes[i]);
+ input.focus();
+ input.click();
+ is(NonTextInputChange[i], 1, NonTextInputTypes[i] + " input element should have dispatched change event.");
+ input.blur();
+ is(NonTextInputChange[i], 1, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element");
+
+ // Test that change event is not dispatched if click event is cancelled.
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ input.addEventListener("click", preventDefault);
+ input.click();
+ is(NonTextInputChange[i], 1, "Change event shouldn't be dispatched if click event is cancelled");
+ input.removeEventListener("click", preventDefault);
+ }
+ }
+
+ // Special case type=number
+ var number = document.getElementById("input_number");
+ number.focus();
+ sendString("a");
+ number.blur();
+ is(numberChange, 0, "Change event shouldn't be dispatched on number input element for key changes that don't change its value");
+ number.value = "";
+ number.focus();
+ sendString("12");
+ is(numberChange, 0, "Change event shouldn't be dispatched on number input element for keyboard input until it loses focus");
+ number.blur();
+ is(numberChange, 1, "Change event should be dispatched on number input element on blur");
+ is(number.value, "12", "Sanity check that number keys were actually handled");
+ if (isDesktop) { // up/down arrow keys not supported on android/b2g
+ number.value = "";
+ number.focus();
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowDown");
+ is(numberChange, 4, "Change event should be dispatched on number input element for up/down arrow keys (a special case)");
+ is(number.value, "1", "Sanity check that number and arrow keys were actually handled");
+ }
+
+ // Special case type=range
+ var range = document.getElementById("input_range");
+ range.focus();
+ sendString("a");
+ range.blur();
+ is(rangeChange, 0, "Change event shouldn't be dispatched on range input element for key changes that don't change its value");
+ range.focus();
+ synthesizeKey("VK_HOME");
+ is(rangeChange, 1, "Change event should be dispatched on range input element for key changes");
+ range.blur();
+ is(rangeChange, 1, "Change event shouldn't be dispatched on range input element on blur");
+ range.focus();
+ var bcr = range.getBoundingClientRect();
+ var centerOfRangeX = bcr.width / 2;
+ var centerOfRangeY = bcr.height / 2;
+ synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" });
+ is(rangeChange, 1, "Change event shouldn't be dispatched on range input element for mousedown");
+ synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" });
+ is(rangeChange, 1, "Change event shouldn't be dispatched on range input element during drag of thumb");
+ synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" });
+ is(rangeChange, 2, "Change event should be dispatched on range input element at end of drag");
+ range.blur();
+ is(rangeChange, 2, "Change event shouldn't be dispatched on range input element when range loses focus after a drag");
+ synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, {});
+ is(rangeChange, 3, "Change event should be dispatched on range input element for a click that gives the range focus");
+
+ if (isDesktop) { // up/down arrow keys not supported on android/b2g
+ synthesizeKey("KEY_ArrowUp");
+ is(rangeChange, 4, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowUp)");
+ synthesizeKey("KEY_ArrowDown");
+ is(rangeChange, 5, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowDown)");
+ synthesizeKey("KEY_ArrowRight");
+ is(rangeChange, 6, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowRight)");
+ synthesizeKey("KEY_ArrowLeft");
+ is(rangeChange, 7, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowLeft)");
+ synthesizeKey("KEY_ArrowUp", {shiftKey: true});
+ is(rangeChange, 8, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowUp)");
+ synthesizeKey("KEY_ArrowDown", {shiftKey: true});
+ is(rangeChange, 9, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowDown)");
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ is(rangeChange, 10, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowRight)");
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ is(rangeChange, 11, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowLeft)");
+ synthesizeKey("KEY_PageUp");
+ is(rangeChange, 12, "Change event should be dispatched on range input element for key changes that change its value (KEY_PageUp)");
+ synthesizeKey("KEY_PageDown");
+ is(rangeChange, 13, "Change event should be dispatched on range input element for key changes that change its value (KEY_PageDown");
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ is(rangeChange, 14, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_PageUp)");
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ is(rangeChange, 15, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_PageDown)");
+ }
+ //Input type change test.
+ input = document.getElementById("input_checkbox");
+ input.type = "text";
+ input.focus();
+ input.click();
+ input.blur();
+ is(NonTextInputChange[5], 1, "Change event shouldn't be dispatched for checkbox ---> text input type change");
+
+ setTimeout(testInputWithDefaultValue, 0);
+ }
+
+ function testInputWithDefaultValue() {
+ // focus and blur an input text should not trigger change event if content hasn't changed.
+ var input = document.getElementById('input_text_value');
+ input.focus();
+ is(textInputValueChange, 0, "change event shouldn't be dispatched on input text with default value");
+
+ SimpleTest.finish();
+ }
+
+ addLoadEvent(testUserInput);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_datalist_element.html b/dom/html/test/forms/test_datalist_element.html
new file mode 100644
index 0000000000..5f05634018
--- /dev/null
+++ b/dom/html/test/forms/test_datalist_element.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for the datalist element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <datalist>
+ </datalist>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 555840 **/
+
+function checkClassesAndAttributes()
+{
+ var d = document.getElementsByTagName('datalist');
+ is(d.length, 1, "One datalist has been found");
+
+ d = d[0];
+ ok(d instanceof HTMLDataListElement,
+ "The datalist should be instance of HTMLDataListElement");
+
+ ok('options' in d, "datalist has an options IDL attribute");
+
+ ok(d.options, "options IDL attribute is not null");
+ ok(!d.getAttribute('options'), "datalist has no options content attribute");
+
+ ok(d.options instanceof HTMLCollection,
+ "options IDL attribute should be instance of HTMLCollection");
+}
+
+function checkOptions()
+{
+ var testData = [
+ /* [ Child list, Function modifying children, Recognized options ] */
+ [['option'], null, 1],
+ [['option', 'option', 'option', 'option'], null, 4],
+ /* Disabled options are not valid. */
+ [['option'], function(d) { d.childNodes[0].disabled = true; }, 0],
+ [['option', 'option'], function(d) { d.childNodes[0].disabled = true; }, 1],
+ /* Non-option elements are not recognized. */
+ [['input'], null, 0],
+ [['input', 'option'], null, 1],
+ [['input', 'textarea'], null, 0],
+ /* .value and .label are not needed to be valid options. */
+ [['option', 'option'], function(d) { d.childNodes[0].value = 'value'; }, 2],
+ [['option', 'option'], function(d) { d.childNodes[0].label = 'label'; }, 2],
+ [['option', 'option'], function(d) { d.childNodes[0].value = 'value'; d.childNodes[0].label = 'label'; }, 2],
+ [['select'],
+ function(d) {
+ var s = d.childNodes[0];
+ s.appendChild(new Option("foo"));
+ s.appendChild(new Option("bar"));
+ },
+ 2],
+ [['select'],
+ function(d) {
+ var s = d.childNodes[0];
+ s.appendChild(new Option("foo"));
+ s.appendChild(new Option("bar"));
+ var label = document.createElement("label");
+ d.appendChild(label);
+ label.appendChild(new Option("foobar"));
+ },
+ 3],
+ [['select'],
+ function(d) {
+ var s = d.childNodes[0];
+ s.appendChild(new Option("foo"));
+ s.appendChild(new Option("bar"));
+ var label = document.createElement("label");
+ d.appendChild(label);
+ label.appendChild(new Option("foobar"));
+ s.appendChild(new Option())
+ },
+ 4],
+ [[], function(d) { d.appendChild(document.createElementNS("foo", "option")); }, 0]
+ ];
+
+ var d = document.getElementsByTagName('datalist')[0];
+ var cachedOptions = d.options;
+
+ testData.forEach(function(data) {
+ data[0].forEach(function(e) {
+ d.appendChild(document.createElement(e));
+ })
+
+ /* Modify children. */
+ if (data[1]) {
+ data[1](d);
+ }
+
+ is(d.options, cachedOptions, "Should get the same object")
+ is(d.options.length, data[2],
+ "The number of recognized options should be " + data[2])
+
+ for (var i = 0; i < d.options.length; ++i) {
+ is(d.options[i].localName, "option",
+ "Should get an option for d.options[" + i + "]")
+ }
+
+ /* Cleaning-up. */
+ d.textContent = "";
+ })
+}
+
+checkClassesAndAttributes();
+checkOptions();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_double_submit.html b/dom/html/test/forms/test_double_submit.html
new file mode 100644
index 0000000000..d27fb290a4
--- /dev/null
+++ b/dom/html/test/forms/test_double_submit.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for multiple submissions in straightline code</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+
+add_task(async function double_submit() {
+ dump("test start\n");
+ let popup = window.open("file_double_submit.html");
+ await new Promise(resolve => {
+ popup.addEventListener("load", resolve, {once: true})
+ });
+
+ let numCalls = 0;
+ popup.addEventListener("beforeunload", () => {
+ numCalls++;
+ info("beforeunload called " + numCalls + " times");
+ });
+
+ info("clicking button");
+ popup.document.querySelector("button").click();
+
+ is(numCalls, 1, "beforeunload should only fire once");
+ popup.close();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-1.html b/dom/html/test/forms/test_form_attribute-1.html
new file mode 100644
index 0000000000..6735f514ae
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-1.html
@@ -0,0 +1,473 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 1</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for form attributes 1 **/
+
+/**
+ * All functions take an array of forms in first argument and an array of
+ * elements in second argument.
+ * Then, it returns an array containing an array of form and an array of array
+ * of elements. The array represent the form association with elements like this:
+ * [ [ form1, form2 ], [ [ elmt1ofForm1, elmt2ofForm2 ], [ elmtofForm2 ] ] ]
+ */
+
+/**
+ * test0a and test0b are testing the regular behavior of form ownership.
+ */
+function test0a(aForms, aElements)
+{
+ // <form><element></form>
+ // <form><element></form>
+ aForms[0].appendChild(aElements[0]);
+ aForms[1].appendChild(aElements[1]);
+
+ return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]];
+}
+
+function test0b(aForms, aElements)
+{
+ // <form><element><form><element></form></form>
+ aForms[0].appendChild(aElements[0]);
+ aForms[0].appendChild(aForms[1]);
+ aForms[1].appendChild(aElements[1]);
+
+ return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]];
+}
+
+/**
+ * This function test that, when an element is not a descendant of a form
+ * element and has @form set to a valid form id, it's form owner is the form
+ * which has the id.
+ */
+function test1(aForms, aElements)
+{
+ // <form id='f'></form><element id='f'>
+ aForms[0].id = 'f';
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0]], [[aElements[0]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), it's form
+ * owner is the form which has the id.
+ */
+function test2(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), then the
+ * form attribute is removed, it does not have a form owner.
+ */
+function test3(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[0].removeAttribute('form');
+
+ return [[aForms[0], aForms[1]], [[],[aElements[0]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), then the
+ * form's id attribute is removed, it does not have a form owner.
+ */
+function test4(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[0].removeAttribute('id');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to an invalid form id, then it does not have a form
+ * owner.
+ */
+function test5(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='foo'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'foo');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), then the
+ * form id attribute is changed to an invalid id, it does not have a form owner.
+ */
+function test6(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[0].setAttribute('form', 'foo');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to an invalid form id, then the form id attribute
+ * is changed to a valid form id, it's form owner is the form which has this id.
+ */
+function test7(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='foo'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'foo');
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a list of ids containing one valid form, then
+ * it does not have a form owner.
+ */
+function test8(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f foo'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f foo');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a form id which is valid in a case insensitive
+ * way, then it does not have a form owner.
+ */
+function test9(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='F'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'F');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a form id which is not a valid id, then it's
+ * form owner is it does not have a form owner.
+ */
+function test10(aForms, aElements)
+{
+ // <form id='F'></form><form><element form='f'></form>
+ aForms[0].id = 'F';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a form id which is not a valid id, then it's
+ * form owner is it does not have a form owner.
+ */
+function test11(aForms, aElements)
+{
+ // <form id='foo bar'></form><form><element form='foo bar'></form>
+ aForms[0].id = 'foo bar';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'foo bar');
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and the form id change, then
+ * it does not have a form owner.
+ */
+function test12(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[0].id = 'foo';
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to an invalid form id and the form id change to a
+ * valid one, then it's form owner is the form which has the id.
+ */
+function test13(aForms, aElements)
+{
+ // <form id='foo'></form><form><element form='f'></form>
+ aForms[0].id = 'foo';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[0].id = 'f';
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and a form with the same id is
+ * inserted before in the tree, then it's form owner is the form which has the
+ * id.
+ */
+function test14(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[2].id = 'f';
+
+ document.getElementById('content').insertBefore(aForms[2], aForms[0]);
+
+ return [[aForms[0], aForms[1], aForms[2]], [[],[],[aElements[0]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and an element with the same id is
+ * inserted before in the tree, then it does not have a form owner.
+ */
+function test15(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[1].id = 'f';
+
+ document.getElementById('content').insertBefore(aElements[1], aForms[0]);
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and the form is removed from
+ * the tree, then it does not have a form owner.
+ */
+function test16(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[1].id = 'f';
+
+ document.getElementById('content').removeChild(aForms[0]);
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to the empty string, it does not have a form owner.
+ */
+function test17(aForms, aElements)
+{
+ // <form><element form=''></form>
+ aForms[0].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', '');
+
+ return [[aForms[0]], [[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to the empty string, it does not have a form owner even if
+ * it's parent has its id equals to the empty string.
+ */
+function test18(aForms, aElements)
+{
+ // <form id=''><element form=''></form>
+ aForms[0].id = '';
+ aForms[0].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', '');
+
+ return [[aForms[0]], [[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to a valid form id and the element is being moving inside
+ * it's parent, it's form owner will remain the form with the id.
+ */
+function test19(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'><element></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aForms[1].appendChild(aElements[1]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[1].appendChild(aElements[0]);
+
+ return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to a valid form id and the element is being moving inside
+ * another form, it's form owner will remain the form with the id.
+ */
+function test20(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'><element></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aForms[1].appendChild(aElements[1]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[2].appendChild(aElements[0]);
+
+ return [[aForms[0],aForms[1],aForms[2]],[[aElements[0]],[aElements[1]],[]]];
+}
+
+/**
+ * This function test that when removing a form, the elements with a @form set
+ * will be correctly removed from there form owner.
+ */
+function test21(aForms, aElements)
+{
+ // <form id='f'><form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ document.getElementById('content').removeChild(aForms[1]);
+
+ return [[aForms[0]],[[]]];
+}
+
+var functions = [
+ test0a, test0b,
+ test1, test2, test3, test4, test5, test6, test7, test8, test9,
+ test10, test11, test12, test13, test14, test15, test16, test17, test18, test19,
+ test20, test21,
+];
+
+// Global variable to have an easy access to <div id='content'>.
+var content = document.getElementById('content');
+
+// Initializing the needed elements.
+var forms = [
+ document.createElement('form'),
+ document.createElement('form'),
+ document.createElement('form'),
+];
+
+var elementNames = [
+ 'button', 'fieldset', 'input', 'label', 'object', 'output', 'select',
+ 'textarea'
+];
+
+var todoElements = [
+ ['keygen', 'Keygen'],
+];
+
+for (var e of todoElements) {
+ var node = document.createElement(e[0]);
+ var nodeString = HTMLElement.prototype.toString.apply(node);
+ nodeString = nodeString.replace(/Element[\] ].*/, "Element");
+ todo_is(nodeString, "[object HTML" + e[1] + "Element",
+ e[0] + " should not be implemented");
+}
+
+for (var name of elementNames) {
+ var elements = [
+ document.createElement(name),
+ document.createElement(name),
+ ];
+
+ for (var func of functions) {
+ // Clean-up.
+ while (content.firstChild) {
+ content.firstChild.remove();
+ }
+ for (form of forms) {
+ content.appendChild(form);
+ form.removeAttribute('id');
+ }
+ for (e of elements) {
+ content.appendChild(e);
+ e.removeAttribute('form');
+ is(e.form, null, "The element should not have a form owner");
+ }
+
+ // Calling the test.
+ var results = func(forms, elements);
+
+ // Checking the results.
+ var formsList = results[0];
+ for (var i=0; i<formsList.length; ++i) {
+ var elementsList = results[1][i];
+ if (name != 'label' && name != 'meter' && name != 'progress') {
+ is(formsList[i].elements.length, elementsList.length,
+ "The form should contain " + elementsList.length + " elements");
+ }
+ for (var j=0; j<elementsList.length; ++j) {
+ if (name != 'label' && name != 'meter' && name != 'progress') {
+ is(formsList[i].elements[j], elementsList[j],
+ "The form should contain " + elementsList[j]);
+ }
+ if (name != 'label') {
+ is(elementsList[j].form, formsList[i],
+ "The form owner should be the form associated to the list");
+ }
+ }
+ }
+ }
+
+ // Cleaning-up.
+ for (e of elements) {
+ e.remove();
+ e = null;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-2.html b/dom/html/test/forms/test_form_attribute-2.html
new file mode 100644
index 0000000000..b7fe5daa87
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-2.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 2</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form id='a'>
+ <form id='b'>
+ <input id='i' form='b'>
+ <script>
+ is(document.getElementById('i').form, document.getElementById('b'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+ </form>
+ <form id='c'>
+ <form id='d'>
+ <input id='i2' form='c'>
+ <script>
+ is(document.getElementById('i2').form, document.getElementById('c'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+ </form>
+ <!-- Let's tests without @form -->
+ <form id='e'>
+ <form id='f'>
+ <input id='i3'>
+ <script>
+ // bug 589073
+ todo_is(document.getElementById('i3').form, document.getElementById('f'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+ </form>
+ <form id='g'>
+ <input id='i4'>
+ <script>
+ is(document.getElementById('i4').form, document.getElementById('g'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-3.html b/dom/html/test/forms/test_form_attribute-3.html
new file mode 100644
index 0000000000..9ceed86716
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-3.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 3</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content">
+ <form id='f'>
+ <input name='e1'>
+ </form>
+ <form id='f2'>
+ <input name='e2'>
+ <input id='i3' form='f'
+ onfocus="var catched=false;
+ try { e1; } catch(e) { catched=true; }
+ ok(!catched, 'e1 should be in the scope of i3');
+ catched = false;
+ try { e2; } catch(e) { catched=true; }
+ ok(catched, 'e2 should not be in the scope of i3');
+ document.getElementById('i4').focus();"
+ >
+ <input id='i4' form='f2'
+ onfocus="var catched=false;
+ try { e2; } catch(e) { catched=true; }
+ ok(!catched, 'e2 should be in the scope of i4');
+ document.getElementById('i5').focus();"
+ >
+ <input id='i5'
+ onfocus="var catched=false;
+ try { e2; } catch(e) { catched=true; }
+ ok(!catched, 'e2 should be in the scope of i5');
+ document.getElementById('i6').focus();"
+ >
+ </form>
+ <input id='i6' form='f'
+ onfocus="var catched=false;
+ try { e1; } catch(e) { catched=true; }
+ ok(!catched, 'e1 should be in the scope of i6');
+ document.getElementById('i7').focus();"
+ >
+ <input id='i7' form='f2'
+ onfocus="var catched=false;
+ try { e2; } catch(e) { catched=true; }
+ ok(!catched, 'e2 should be in the scope of i7');
+ this.blur();
+ SimpleTest.finish();"
+ >
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for form attributes 3 **/
+
+SimpleTest.waitForExplicitFinish();
+
+document.getElementById('i3').focus();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-4.html b/dom/html/test/forms/test_form_attribute-4.html
new file mode 100644
index 0000000000..f2228cec45
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-4.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 4</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content" style='display:none;'>
+ <form id='f'>
+ </form>
+ <table id='t'>
+ <form id='f2'>
+ <tr><td><input id='i1'></td></tr>
+ <tr><td><input id='i2' form='f'></td></tr>
+ </form>
+ </table>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for form attributes 4 **/
+
+var table = document.getElementById('t');
+var i1 = document.getElementById('i1');
+var i2 = document.getElementById('i2');
+
+is(i1.form, document.getElementById('f2'),
+ "i1 form should be it's parent");
+is(i2.form, document.getElementById('f'),
+ "i1 form should be the form with the id in @form");
+
+table.removeChild(document.getElementById('f2'));
+is(i1, document.getElementById('i1'),
+ "i1 should still be in the document");
+is(i1.form, null, "i1 should not have any form owner");
+is(i2.form, document.getElementById('f'),
+ "i1 form should be the form with the id in @form");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attributes_reflection.html b/dom/html/test/forms/test_form_attributes_reflection.html
new file mode 100644
index 0000000000..0d0ef6b870
--- /dev/null
+++ b/dom/html/test/forms/test_form_attributes_reflection.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLFormElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLFormElement attributes reflection **/
+
+// .acceptCharset
+reflectString({
+ element: document.createElement("form"),
+ attribute: { idl: "acceptCharset", content: "accept-charset" },
+ otherValues: [ "ISO-8859-1", "UTF-8" ],
+});
+
+reflectURL({
+ element: document.createElement("form"),
+ attribute: "action",
+});
+
+// .autocomplete
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: "autocomplete",
+ validValues: [ "on", "off" ],
+ invalidValues: [ "", "foo", "tulip", "default" ],
+ defaultValue: "on",
+});
+
+// .enctype
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: "enctype",
+ validValues: [ "application/x-www-form-urlencoded", "multipart/form-data",
+ "text/plain" ],
+ invalidValues: [ "", "foo", "tulip", "multipart/foo" ],
+ defaultValue: "application/x-www-form-urlencoded"
+});
+
+// .encoding
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: { idl: "encoding", content: "enctype" },
+ validValues: [ "application/x-www-form-urlencoded", "multipart/form-data",
+ "text/plain" ],
+ invalidValues: [ "", "foo", "tulip", "multipart/foo" ],
+ defaultValue: "application/x-www-form-urlencoded"
+});
+
+// .method
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: "method",
+ validValues: [ "get", "post" ],
+ invalidValues: [ "", "foo", "tulip" ],
+ defaultValue: "get"
+});
+
+// .name
+reflectString({
+ element: document.createElement("form"),
+ attribute: "name",
+});
+
+// .noValidate
+reflectBoolean({
+ element: document.createElement("form"),
+ attribute: "noValidate",
+});
+
+// .target
+reflectString({
+ element: document.createElement("form"),
+ attribute: "target",
+ otherValues: [ "_blank", "_self", "_parent", "_top" ],
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_named_getter_dynamic.html b/dom/html/test/forms/test_form_named_getter_dynamic.html
new file mode 100644
index 0000000000..4a19768453
--- /dev/null
+++ b/dom/html/test/forms/test_form_named_getter_dynamic.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=377413
+-->
+<head>
+ <title>Test for Bug 377413</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <link rel='stylesheet' href='/resources/testharness.css'>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377413">Mozilla Bug 377413</a>
+<p id="log"></p>
+<div id="content">
+ <form>
+ <table>
+ <tbody>
+ </tbody>
+ </table>
+ </form>
+</div>
+
+<script type="text/javascript">
+
+/** Tests for Bug 377413 **/
+var tb = document.getElementsByTagName('tbody')[0];
+
+test(function(){
+ tb.innerHTML = '<tr><td><input name="fooboo"></td></tr>';
+ document.forms[0].fooboo.value = 'testme';
+ document.getElementsByTagName('table')[0].deleteRow(0);
+ assert_equals(document.forms[0].fooboo, undefined);
+}, "no element reference after deleting it with deleteRow()");
+
+test(function(){
+ var b = tb.appendChild(document.createElement('tr')).appendChild(document.createElement('td')).appendChild(document.createElement('button'));
+ b.name = b.value = 'boofoo';
+ assert_equals(document.forms[0].elements[0].value, 'boofoo');
+}, 'element value set correctly');
+
+test(function(){
+ assert_true('boofoo' in document.forms[0]);
+}, 'element name has created property on form');
+
+test(function(){
+ tb.innerHTML = '';
+ assert_false('boofoo' in document.forms[0]);
+}, "no element reference after deleting it by setting innerHTML");
+
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_formaction_attribute.html b/dom/html/test/forms/test_formaction_attribute.html
new file mode 100644
index 0000000000..0dee2f172d
--- /dev/null
+++ b/dom/html/test/forms/test_formaction_attribute.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=566160
+-->
+<head>
+ <title>Test for Bug 566160</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a>
+<p id="display"></p>
+<style>
+ iframe { width: 130px; height: 100px;}
+</style>
+<iframe name='frame1' id='frame1'></iframe>
+<iframe name='frame2' id='frame2'></iframe>
+<iframe name='frame3' id='frame3'></iframe>
+<iframe name='frame3bis' id='frame3bis'></iframe>
+<iframe name='frame4' id='frame4'></iframe>
+<iframe name='frame5' id='frame5'></iframe>
+<iframe name='frame6' id='frame6'></iframe>
+<iframe name='frame7' id='frame7'></iframe>
+<div id="content">
+ <!-- submit controls with formaction that are validated with a CLICK -->
+ <form target="frame1" action="FAIL.html" method="GET">
+ <input name='foo' value='foo'>
+ <input type='submit' id='is' formaction="PASS.html">
+ </form>
+ <form target="frame2" action="FAIL.html" method="GET">
+ <input name='bar' value='bar'>
+ <input type='image' id='ii' formaction="PASS.html">
+ </form>
+ <form target="frame3" action="FAIL.html" method="GET">
+ <input name='tulip' value='tulip'>
+ <button type='submit' id='bs' formaction="PASS.html">submit</button>
+ </form>
+ <form target="frame3bis" action="FAIL.html" method="GET">
+ <input name='tulipbis' value='tulipbis'>
+ <button type='submit' id='bsbis' formaction="PASS.html">submit</button>
+ </form>
+
+ <!-- submit controls with formaction that are validated with ENTER -->
+ <form target="frame4" action="FAIL.html" method="GET">
+ <input name='footulip' value='footulip'>
+ <input type='submit' id='is2' formaction="PASS.html">
+ </form>
+ <form target="frame5" action="FAIL.html" method="GET">
+ <input name='foobar' value='foobar'>
+ <input type='image' id='ii2' formaction="PASS.html">
+ </form>
+ <form target="frame6" action="FAIL.html" method="GET">
+ <input name='tulip2' value='tulip2'>
+ <button type='submit' id='bs2' formaction="PASS.html">submit</button>
+ </form>
+
+ <!-- check that when submitting a from from an element
+ which is not a submit control, @formaction isn't used -->
+ <form target='frame7' action="PASS.html" method="GET">
+ <input id='enter' name='input' value='enter' formaction="FAIL.html">
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 566160 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const BASE_URI = `${location.origin}/tests/dom/html/test/forms/PASS.html`;
+var gTestResults = {
+ frame1: BASE_URI + "?foo=foo",
+ frame2: BASE_URI + "?bar=bar&x=0&y=0",
+ frame3: BASE_URI + "?tulip=tulip",
+ frame3bis: BASE_URI + "?tulipbis=tulipbis",
+ frame4: BASE_URI + "?footulip=footulip",
+ frame5: BASE_URI + "?foobar=foobar&x=0&y=0",
+ frame6: BASE_URI + "?tulip2=tulip2",
+ frame7: BASE_URI + "?input=enter",
+};
+
+var gPendingLoad = 0; // Has to be set after depending on the frames number.
+
+function runTests()
+{
+ // We add a load event for the frames which will be called when the forms
+ // will be submitted.
+ var frames = [ document.getElementById('frame1'),
+ document.getElementById('frame2'),
+ document.getElementById('frame3'),
+ document.getElementById('frame3bis'),
+ document.getElementById('frame4'),
+ document.getElementById('frame5'),
+ document.getElementById('frame6'),
+ document.getElementById('frame7'),
+ ];
+ gPendingLoad = frames.length;
+
+ for (var i=0; i<frames.length; i++) {
+ frames[i].setAttribute('onload', "frameLoaded(this);");
+ }
+
+ /**
+ * We are going to focus each element before interacting with either for
+ * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or
+ * using .click(). This because it may be needed (ENTER) and because we want
+ * to have the element visible in the iframe.
+ *
+ * Focusing the first element (id='is') is launching the tests.
+ */
+ document.getElementById('is').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('is'), 5, 5, {});
+ document.getElementById('ii').focus();
+ }, {once: true});
+
+ document.getElementById('ii').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('ii'), 5, 5, {});
+ document.getElementById('bs').focus();
+ }, {once: true});
+
+ document.getElementById('bs').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('bs'), 5, 5, {});
+ document.getElementById('bsbis').focus();
+ }, {once: true});
+
+ document.getElementById('bsbis').addEventListener('focus', function(aEvent) {
+ document.getElementById('bsbis').click();
+ document.getElementById('is2').focus();
+ }, {once: true});
+
+ document.getElementById('is2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('ii2').focus();
+ }, {once: true});
+
+ document.getElementById('ii2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('bs2').focus();
+ }, {once: true});
+
+ document.getElementById('bs2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('enter').focus();
+ }, {once: true});
+
+ document.getElementById('enter').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ }, {once: true});
+
+ document.getElementById('is').focus();
+}
+
+function frameLoaded(aFrame) {
+ // Check if formaction/action has the correct behavior.
+ is(aFrame.contentWindow.location.href, gTestResults[aFrame.name],
+ "the action attribute doesn't have the correct behavior");
+
+ if (--gPendingLoad == 0) {
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_formnovalidate_attribute.html b/dom/html/test/forms/test_formnovalidate_attribute.html
new file mode 100644
index 0000000000..2e3714d2fe
--- /dev/null
+++ b/dom/html/test/forms/test_formnovalidate_attribute.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=589696
+-->
+<head>
+ <title>Test for Bug 589696</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=589696">Mozilla Bug 589696</a>
+<p id="display"></p>
+<iframe style='width:50px; height: 50px;' name='t'></iframe>
+<div id="content">
+ <!-- Next forms should not submit because formnovalidate isn't set on the
+ element used for the submission. -->
+ <form target='t' action='data:text/html,'>
+ <input id='av' required>
+ <input type='submit' formnovalidate>
+ <input id='a' type='submit'>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input id='bv' type='checkbox' required>
+ <button type='submit' formnovalidate></button>
+ <button id='b' type='submit'></button>
+ </form>
+ <!-- Next form should not submit because formnovalidate only applies for
+ submit controls. -->
+ <form target='t' action='data:text/html,'>
+ <input id='c' required formnovalidate>
+ </form>
+ <!--- Next forms should submit without any validation check. -->
+ <form target='t' action='data:text/html,'>
+ <input id='dv' required>
+ <input id='d' type='submit' formnovalidate>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input id='ev' type='checkbox' required>
+ <button id='e' type='submit' formnovalidate></button>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 589696 **/
+
+document.getElementById('av').addEventListener("invalid", function(aEvent) {
+ aEvent.target.removeAttribute("invalid", arguments.callee, false);
+ ok(true, "formnovalidate should not apply on if not set on the submit " +
+ "control used for the submission");
+ document.getElementById('b').click();
+});
+
+document.getElementById('bv').addEventListener("invalid", function(aEvent) {
+ aEvent.target.removeAttribute("invalid", arguments.callee, false);
+ ok(true, "formnovalidate should not apply on if not set on the submit " +
+ "control used for the submission");
+ var c = document.getElementById('c');
+ c.focus();
+ synthesizeKey("KEY_Enter");
+});
+
+document.getElementById('c').addEventListener("invalid", function(aEvent) {
+ aEvent.target.removeAttribute("invalid", arguments.callee, false);
+ ok(true, "formnovalidate should only apply on submit controls");
+ document.getElementById('d').click();
+});
+
+document.forms[3].addEventListener("submit", function(aEvent) {
+ aEvent.target.removeAttribute("submit", arguments.callee, false);
+ ok(true, "formnovalidate applies if set on the submit control used for the submission");
+ document.getElementById('e').click();
+});
+
+document.forms[4].addEventListener("submit", function(aEvent) {
+ aEvent.target.removeAttribute("submit", arguments.callee, false);
+ ok(true, "formnovalidate applies if set on the submit control used for the submission");
+ SimpleTest.executeSoon(SimpleTest.finish);
+});
+
+/**
+ * We have to be sure invalid events behave as expected.
+ * They should be sent before the submit event so we can just create a test
+ * failure if we got one when unexpected. All of them should be caught if
+ * sent.
+ * At worst, we got random green which isn't harmful.
+ * If expected, they will be part of the chain reaction.
+ */
+function unexpectedInvalid(aEvent)
+{
+ aEvent.target.removeAttribute("invalid", unexpectedInvalid, false);
+ ok(false, "invalid event should not be sent");
+}
+
+document.getElementById('dv').addEventListener("invalid", unexpectedInvalid);
+document.getElementById('ev').addEventListener("invalid", unexpectedInvalid);
+
+/**
+ * Some submission have to be canceled. In that case, the submit events should
+ * not be sent.
+ * Same behavior as unexpected invalid events.
+ */
+function unexpectedSubmit(aEvent)
+{
+ aEvent.target.removeAttribute("submit", unexpectedSubmit, false);
+ ok(false, "submit event should not be sent");
+}
+
+document.forms[0].addEventListener("submit", unexpectedSubmit);
+document.forms[1].addEventListener("submit", unexpectedSubmit);
+document.forms[2].addEventListener("submit", unexpectedSubmit);
+
+SimpleTest.waitForExplicitFinish();
+
+// This is going to call all the tests (with a chain reaction).
+SimpleTest.waitForFocus(function() {
+ document.getElementById('a').click();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_attributes_reflection.html b/dom/html/test/forms/test_input_attributes_reflection.html
new file mode 100644
index 0000000000..348ea0f80d
--- /dev/null
+++ b/dom/html/test/forms/test_input_attributes_reflection.html
@@ -0,0 +1,271 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLInputElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.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 type="application/javascript">
+
+/** Test for HTMLInputElement attributes reflection **/
+
+// TODO: maybe make those reflections be tested against all input types.
+
+function testWidthHeight(attr) {
+ var element = document.createElement('input');
+ is(element[attr], 0, attr + ' always returns 0 if not type=image');
+ element.setAttribute(attr, '42');
+ is(element[attr], 0, attr + ' always returns 0 if not type=image');
+ is(element.getAttribute(attr), '42');
+ element[attr] = 0;
+ is(element.getAttribute(attr), '0', 'setting ' + attr + ' changes the content attribute');
+ element[attr] = 12;
+ is(element.getAttribute(attr), '12', 'setting ' + attr + ' changes the content attribute');
+
+ element.removeAttribute(attr);
+ is(element.getAttribute(attr), null);
+
+ element = document.createElement('input');
+ element.type = 'image';
+ element.style.display = "inline";
+ document.getElementById('content').appendChild(element);
+ isnot(element[attr], 0, attr + ' represents the dimension of the element if type=image');
+
+ element.setAttribute(attr, '42');
+ isnot(element[attr], 0, attr + ' represents the dimension of the element if type=image');
+ isnot(element[attr], 42, attr + ' represents the dimension of the element if type=image');
+ is(element.getAttribute(attr), '42');
+ element[attr] = 0;
+ is(element.getAttribute(attr), '0', 'setting ' + attr + ' changes the content attribute');
+ element[attr] = 12;
+ is(element.getAttribute(attr), '12', 'setting ' + attr + ' changes the content attribute');
+
+ element.removeAttribute(attr);
+ is(element.getAttribute(attr), null);
+}
+
+// .accept
+reflectString({
+ element: document.createElement("input"),
+ attribute: "accept",
+ otherValues: [ "audio/*", "video/*", "image/*", "image/png",
+ "application/msword", "appplication/pdf" ],
+});
+
+// .alt
+reflectString({
+ element: document.createElement("input"),
+ attribute: "alt",
+});
+
+// .autocomplete
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "autocomplete",
+ validValues: [ "on", "off" ],
+ invalidValues: [ "", "default", "foo", "tulip" ],
+});
+
+// .autofocus
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "autofocus",
+});
+
+// .defaultChecked
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: { idl: "defaultChecked", content: "checked" },
+});
+
+// .checked doesn't reflect a content attribute.
+
+// .dirName
+reflectString({
+ element: document.createElement("input"),
+ attribute: "dirName"
+});
+
+// .disabled
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "disabled",
+});
+
+// TODO: form (HTMLFormElement)
+// TODO: files (FileList)
+
+// .formAction
+reflectURL({
+ element: document.createElement("button"),
+ attribute: "formAction",
+});
+
+// .formEnctype
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "formEnctype",
+ validValues: [ "application/x-www-form-urlencoded", "multipart/form-data",
+ "text/plain" ],
+ invalidValues: [ "", "foo", "tulip", "multipart/foo" ],
+ defaultValue: { invalid: "application/x-www-form-urlencoded", missing: "" }
+});
+
+// .formMethod
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "formMethod",
+ validValues: [ "get", "post" ],
+ invalidValues: [ "", "foo", "tulip" ],
+ defaultValue: { invalid: "get", missing: "" }
+});
+
+// .formNoValidate
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "formNoValidate",
+});
+
+// .formTarget
+reflectString({
+ element: document.createElement("input"),
+ attribute: "formTarget",
+ otherValues: [ "_blank", "_self", "_parent", "_top" ],
+});
+
+// .height
+testWidthHeight('height');
+
+// .indeterminate doesn't reflect a content attribute.
+
+// TODO: list (HTMLElement)
+
+// .max
+reflectString({
+ element: document.createElement('input'),
+ attribute: 'max',
+});
+
+// .maxLength
+reflectInt({
+ element: document.createElement("input"),
+ attribute: "maxLength",
+ nonNegative: true,
+});
+
+// .min
+reflectString({
+ element: document.createElement('input'),
+ attribute: 'min',
+});
+
+// .multiple
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "multiple",
+});
+
+// .name
+reflectString({
+ element: document.createElement("input"),
+ attribute: "name",
+ otherValues: [ "isindex", "_charset_" ],
+});
+
+// .pattern
+reflectString({
+ element: document.createElement("input"),
+ attribute: "pattern",
+ otherValues: [ "[0-9][A-Z]{3}" ],
+});
+
+// .placeholder
+reflectString({
+ element: document.createElement("input"),
+ attribute: "placeholder",
+ otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ],
+});
+
+// .readOnly
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "readOnly",
+});
+
+// .required
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "required",
+});
+
+// .size
+reflectUnsignedInt({
+ element: document.createElement("input"),
+ attribute: "size",
+ nonZero: true,
+ defaultValue: 20,
+});
+
+// .src (URL)
+reflectURL({
+ element: document.createElement('input'),
+ attribute: 'src',
+});
+
+// .step
+reflectString({
+ element: document.createElement('input'),
+ attribute: 'step',
+});
+
+// .type
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "type",
+ validValues: [ "hidden", "text", "search", "tel", "url", "email", "password",
+ "checkbox", "radio", "file", "submit", "image", "reset",
+ "button", "date", "time", "number", "range", "color", "month",
+ "week", "datetime-local" ],
+ invalidValues: [ "this-is-probably-a-wrong-type", "", "tulip" ],
+ defaultValue: "text"
+});
+
+// .defaultValue
+reflectString({
+ element: document.createElement("input"),
+ attribute: { idl: "defaultValue", content: "value" },
+ otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ],
+});
+
+// .value doesn't reflect a content attribute.
+
+// .valueAsDate
+is("valueAsDate" in document.createElement("input"), true,
+ "valueAsDate should be available");
+
+// Deeper check will be done with bug 763305.
+is('valueAsNumber' in document.createElement("input"), true,
+ "valueAsNumber should be available");
+
+// .selectedOption
+todo("selectedOption" in document.createElement("input"),
+ "selectedOption isn't implemented yet");
+
+// .width
+testWidthHeight('width');
+
+// .willValidate doesn't reflect a content attribute.
+// .validity doesn't reflect a content attribute.
+// .validationMessage doesn't reflect a content attribute.
+// .labels doesn't reflect a content attribute.
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_input_change_events.html b/dom/html/test/forms/test_input_color_input_change_events.html
new file mode 100644
index 0000000000..f97d54f66e
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_input_change_events.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</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"/>
+ <script type="application/javascript">
+
+ /** Test that update() modifies the element value such as done() when it is
+ * not called as a concellation.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker, update) {
+ is(picker.initialColor, element.value);
+
+ var inputEvent = false;
+ var changeEvent = false;
+ element.oninput = function() {
+ inputEvent = true;
+ };
+ element.onchange = function() {
+ changeEvent = true;
+ };
+
+ if (element.dataset.type == 'update') {
+ update('#f00ba4');
+
+ is(inputEvent, true, 'input event should have been received');
+ is(changeEvent, false, 'change event should not have been received');
+
+ inputEvent = changeEvent = false;
+
+ is(element.value, '#f00ba4');
+
+ MockColorPicker.returnColor = '#f00ba7';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'cancel') {
+ MockColorPicker.returnColor = '#bababa';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'done') {
+ MockColorPicker.returnColor = '#098766';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'noop-done') {
+ MockColorPicker.returnColor = element.value;
+ is(element.value, MockColorPicker.returnColor);
+ }
+
+ SimpleTest.executeSoon(function() {
+ if (element.dataset.type == 'cancel') {
+ isnot(element.value, MockColorPicker.returnColor);
+ is(inputEvent, false, 'no input event should have been sent');
+ is(changeEvent, false, 'no change event should have been sent');
+ } else if (element.dataset.type == 'noop-done') {
+ is(element.value, MockColorPicker.returnColor);
+ is(inputEvent, false, 'no input event should have been sent');
+ is(changeEvent, false, 'no change event should have been sent');
+ } else {
+ is(element.value, MockColorPicker.returnColor);
+ is(inputEvent, true, 'input event should have been sent');
+ is(changeEvent, true, 'change event should have been sent');
+ }
+
+ changeEvent = false;
+ element.blur();
+
+ setTimeout(function() {
+ is(changeEvent, false, "change event should not be fired on blur");
+ test.next();
+ });
+ });
+
+ return element.dataset.type == 'cancel' ? "" : MockColorPicker.returnColor;
+ };
+
+ for (var i = 0; i < document.getElementsByTagName('input').length; ++i) {
+ element = document.getElementsByTagName('input')[i];
+ element.focus();
+ synthesizeMouseAtCenter(element, {});
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <input type='color' data-type='update'>
+ <input type='color' data-type='cancel'>
+ <input type='color' data-type='done'>
+ <input type='color' data-type='noop-done'>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_datalist.html b/dom/html/test/forms/test_input_color_picker_datalist.html
new file mode 100644
index 0000000000..1a268c0701
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_datalist.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<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"/>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ let MockColorPicker = SpecialPowers.MockColorPicker;
+
+ MockColorPicker.init(window);
+
+ MockColorPicker.showCallback = (picker) => {
+ is(picker.defaultColors.length, 2);
+ is(picker.defaultColors[0], "#112233");
+ is(picker.defaultColors[1], "#00ffaa");
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ let input = document.querySelector("input");
+ synthesizeMouseAtCenter(input, {});
+}
+
+SimpleTest.waitForFocus(runTest);
+</script>
+</head>
+<body>
+<input type="color" list="color-list">
+<datalist id="color-list">
+ <option value="#112233"></option>
+ <option value="black"></option> <!-- invalid -->
+ <option value="#000000" disabled></option>
+ <option value="#00FFAA"></option>
+ <option></option>
+</datalist>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_initial.html b/dom/html/test/forms/test_input_color_picker_initial.html
new file mode 100644
index 0000000000..c7467c7520
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_initial.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</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"/>
+ <script type="application/javascript">
+
+ /** Test that the initial value of the nsIColorPicker is the current value of
+ the <input type='color'> element. **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker) {
+ is(picker.initialColor, element.value);
+ SimpleTest.executeSoon(function() {
+ test.next();
+ });
+ return "";
+ };
+
+ for (var i = 0; i < document.getElementsByTagName('input').length; ++i) {
+ element = document.getElementsByTagName('input')[i];
+ if (element.parentElement.id === 'dynamic-values') {
+ element.value = '#deadbe';
+ }
+ synthesizeMouseAtCenter(element, {});
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <div id='valid-values'>
+ <input type='color' value='#ff00ff'>
+ <input type='color' value='#ab3275'>
+ <input type='color' value='#abcdef'>
+ <input type='color' value='#ABCDEF'>
+ </div>
+ <div id='invalid-values'>
+ <input type='color' value='ffffff'>
+ <input type='color' value='#abcdez'>
+ <input type='color' value='#0123456'>
+ </div>
+ <div id='dynamic-values'>
+ <input type='color' value='#ab4594'>
+ <input type='color' value='#984534'>
+ <input type='color' value='#f8b9a0'>
+ </div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_popup.html b/dom/html/test/forms/test_input_color_picker_popup.html
new file mode 100644
index 0000000000..9fbebf15bc
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_popup.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</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"/>
+ <style> body { font-family: serif } </style>
+ <script type="application/javascript">
+
+ /** Test the behaviour of the <input type='color'> when clicking on it from
+ different ways. **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+ var testData = [
+ { id: 'normal', result: true },
+ { id: 'hidden', result: false },
+ { id: 'normal', type: 'untrusted', result: true },
+ { id: 'normal', type: 'prevent-default-1', result: false },
+ { id: 'normal', type: 'prevent-default-2', result: false },
+ { id: 'normal', type: 'click-method', result: true },
+ { id: 'normal', type: 'show-picker', result: true },
+ { id: 'normal', type: 'right-click', result: false },
+ { id: 'normal', type: 'middle-click', result: false },
+ { id: 'label-1', result: true },
+ { id: 'label-2', result: true },
+ { id: 'label-3', result: true },
+ { id: 'label-4', result: true },
+ { id: 'button-click', result: true },
+ { id: 'button-down', result: true },
+ { id: 'button-up', result: true },
+ { id: 'div-click', result: true },
+ { id: 'div-click-on-demand', result: true },
+ ];
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ let currentTest = null;
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker) {
+ ok(currentTest.result);
+ SimpleTest.executeSoon(function() {
+ test.next();
+ });
+ return "";
+ };
+
+ while (testData.length) {
+ currentTest = testData.shift();
+ element = document.getElementById(currentTest.id);
+
+ // To make sure we can actually click on the element.
+ element.focus();
+
+ switch (currentTest.type) {
+ case 'untrusted':
+ var e = document.createEvent('MouseEvents');
+ e.initEvent('click', true, false);
+ document.getElementById(element.dispatchEvent(e));
+ break;
+ case 'prevent-default-1':
+ element.onclick = function() {
+ return false;
+ };
+ element.click();
+ element.onclick = function() {};
+ break;
+ case 'prevent-default-2':
+ element.onclick = function(event) {
+ event.preventDefault();
+ };
+ element.click();
+ element.onclick = function() {};
+ break;
+ case 'click-method':
+ element.click();
+ break;
+ case 'show-picker':
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ element.showPicker();
+ break;
+ case 'right-click':
+ synthesizeMouseAtCenter(element, { button: 2 });
+ break;
+ case 'middle-click':
+ synthesizeMouseAtCenter(element, { button: 1 });
+ break;
+ default:
+ synthesizeMouseAtCenter(element, {});
+ }
+
+ if (!currentTest.result) {
+ setTimeout(function() {
+ setTimeout(function() {
+ ok(true);
+ SimpleTest.executeSoon(function() {
+ test.next();
+ });
+ });
+ });
+ }
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <input type='color' id='normal'>
+ <input type='color' id='hidden' hidden>
+ <label id='label-1'>foo<input type='color'></label>
+ <label id='label-2' for='labeled-2'>foo</label><input id='labeled-2' type='color'></label>
+ <label id='label-3'>foo<input type='color'></label>
+ <label id='label-4' for='labeled-4'>foo</label><input id='labeled-4' type='color'></label>
+ <input id='by-button' type='color'>
+ <button id='button-click' onclick="document.getElementById('by-button').click();">click</button>
+ <button id='button-down' onclick="document.getElementById('by-button').click();">click</button>
+ <button id='button-up' onclick="document.getElementById('by-button').click();">click</button>
+ <div id='div-click' onclick="document.getElementById('by-button').click();">click</div>
+ <div id='div-click-on-demand' onclick="var i=document.createElement('input'); i.type='color'; i.click();">click</div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_update.html b/dom/html/test/forms/test_input_color_picker_update.html
new file mode 100644
index 0000000000..5c22b667e1
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_update.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</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"/>
+ <style> body { font-family: serif } </style>
+ <script type="application/javascript">
+
+ /** Test that update() modifies the element value such as done() when it is
+ * not called as a concellation.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker, update) {
+ is(picker.initialColor, element.value);
+
+ if (element.dataset.type == 'update') {
+ update('#f00ba4');
+ is(element.value, '#f00ba4');
+
+ MockColorPicker.returnColor = '#f00ba7';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'cancel') {
+ MockColorPicker.returnColor = '#bababa';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'done') {
+ MockColorPicker.returnColor = '#098766';
+ isnot(element.value, MockColorPicker.returnColor);
+ }
+
+ SimpleTest.executeSoon(function() {
+ if (element.dataset.type == 'cancel') {
+ isnot(element.value, MockColorPicker.returnColor);
+ } else {
+ is(element.value, MockColorPicker.returnColor);
+ }
+
+ test.next();
+ });
+
+ return element.dataset.type == 'cancel' ? "" : MockColorPicker.returnColor;
+ };
+
+ for (var i = 0; i < document.getElementsByTagName('input').length; ++i) {
+ element = document.getElementsByTagName('input')[i];
+ synthesizeMouseAtCenter(element, {});
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <input type='color' data-type='update'>
+ <input type='color' data-type='cancel'>
+ <input type='color' data-type='done'>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_date_bad_input.html b/dom/html/test/forms/test_input_date_bad_input.html
new file mode 100644
index 0000000000..516d48263f
--- /dev/null
+++ b/dom/html/test/forms/test_input_date_bad_input.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1372369
+-->
+<head>
+ <title>Test for &lt;input type='date'&gt; bad input validity state</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"/>
+ <style>
+ input { background-color: rgb(0,0,0) !important; }
+ :valid { background-color: rgb(0,255,0) !important; }
+ :invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1372369">Mozilla Bug 1372369</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <input type="date" id="input">
+ <form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for <input type='date'> bad input validity state **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+const DATE_BAD_INPUT_MSG = "Please enter a valid date.";
+const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+function checkValidity(aElement, aIsBadInput) {
+ is(aElement.validity.valid, !aIsBadInput,
+ "validity.valid should be " + (aIsBadInput ? "false" : "true"));
+ is(aElement.validity.badInput, !!aIsBadInput,
+ "validity.badInput should be " + (aIsBadInput ? "true" : "false"));
+ is(aElement.validationMessage, aIsBadInput ? DATE_BAD_INPUT_MSG : "",
+ "validationMessage should be: " + (aIsBadInput ? DATE_BAD_INPUT_MSG : ""));
+
+ is(window.getComputedStyle(aElement).getPropertyValue('background-color'),
+ aIsBadInput ? "rgb(255, 0, 0)" : "rgb(0, 255, 0)",
+ (aIsBadInput ? ":invalid" : "valid") + " pseudo-class should apply");
+}
+
+function sendKeys(aKey) {
+ if (aKey.startsWith("KEY_")) {
+ synthesizeKey(aKey);
+ } else {
+ sendString(aKey);
+ }
+}
+
+function test() {
+ var elem = document.getElementById("input");
+
+ elem.focus();
+ sendKeys("02312017");
+ elem.blur();
+ checkValidity(elem, true);
+
+ elem.focus();
+ sendKeys("02292016");
+ elem.blur();
+ checkValidity(elem, false);
+
+ elem.focus();
+ sendKeys("06312000");
+ elem.blur();
+ checkValidity(elem, true);
+
+ // Removing some of the fields keeps the input as invalid.
+ elem.focus();
+ sendKeys("KEY_Backspace");
+ elem.blur();
+ checkValidity(elem, true);
+
+ // Removing all of the fields manually makes the input valid (but empty) again.
+ elem.focus();
+ sendKeys("KEY_ArrowRight");
+ sendKeys("KEY_Backspace");
+ sendKeys("KEY_ArrowRight");
+ sendKeys("KEY_Delete");
+ elem.blur();
+ checkValidity(elem, false);
+
+ elem.focus();
+ sendKeys("02292017");
+ elem.blur();
+ checkValidity(elem, true);
+
+ // Clearing all fields should clear bad input validity state as well.
+ elem.focus();
+ synthesizeKey("KEY_Backspace", { accelKey: true });
+ checkValidity(elem, false);
+
+ sendKeys("22334444");
+ elem.blur();
+ elem.focus();
+ synthesizeKey("KEY_Delete", { accelKey: true });
+ checkValidity(elem, false);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html
new file mode 100644
index 0000000000..387cb37af7
--- /dev/null
+++ b/dom/html/test/forms/test_input_date_key_events.html
@@ -0,0 +1,270 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1286182
+-->
+<head>
+ <title>Test key events for date control</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286182">Mozilla Bug 1286182</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1804669">Mozilla Bug 1804669</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="date">
+ <div id="host"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+var testData = [
+ /**
+ * keys: keys to send to the input element.
+ * initialVal: initial value set to the input element.
+ * expectedVal: expected value of the input element after sending the keys.
+ */
+ {
+ // Type 11222016, default order is month, day, year.
+ keys: ["11222016"],
+ initialVal: "",
+ expectedVal: "2016-11-22"
+ },
+ {
+ // Type 3 in the month field will automatically advance to the day field,
+ // then type 5 in the day field will automatically advance to the year
+ // field.
+ keys: ["352016"],
+ initialVal: "",
+ expectedVal: "2016-03-05"
+ },
+ {
+ // Type 13 in the month field will set it to the maximum month, which is
+ // 12.
+ keys: ["13012016"],
+ initialVal: "",
+ expectedVal: "2016-12-01"
+ },
+ {
+ // Type 00 in the month field will set it to the minimum month, which is 1.
+ keys: ["00012016"],
+ initialVal: "",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // Type 33 in the day field will set it to the maximum day, which is 31.
+ keys: ["12332016"],
+ initialVal: "",
+ expectedVal: "2016-12-31"
+ },
+ {
+ // Type 00 in the day field will set it to the minimum day, which is 1.
+ keys: ["12002016"],
+ initialVal: "",
+ expectedVal: "2016-12-01"
+ },
+ {
+ // Type 275769 in the year field will set it to 0069, because the
+ // 5th digit will erase the previous 4 digits.
+ keys: ["0101275769"],
+ initialVal: "",
+ expectedVal: "0069-01-01"
+ },
+ {
+ // Type 0000 in the year field will set it to the minimum year, which is
+ // 0001.
+ keys: ["01010000"],
+ initialVal: "",
+ expectedVal: "0001-01-01"
+ },
+ {
+ // Advance to year field and decrement.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "2016-11-25",
+ expectedVal: "2015-11-25"
+ },
+ {
+ // Right key should do the same thing as TAB key.
+ keys: ["KEY_ArrowRight", "KEY_ArrowRight", "KEY_ArrowDown"],
+ initialVal: "2016-11-25",
+ expectedVal: "2015-11-25"
+ },
+ {
+ // Advance to day field then back to month field and decrement.
+ keys: ["KEY_ArrowRight", "KEY_ArrowLeft", "KEY_ArrowDown"],
+ initialVal: "2000-05-01",
+ expectedVal: "2000-04-01"
+ },
+ {
+ // Focus starts on the first field, month in this case, and increment.
+ keys: ["KEY_ArrowUp"],
+ initialVal: "2000-03-01",
+ expectedVal: "2000-04-01"
+ },
+ {
+ // Advance to day field and decrement.
+ keys: ["KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "1234-01-01",
+ expectedVal: "1234-01-31"
+ },
+ {
+ // Advance to day field and increment.
+ keys: ["KEY_Tab", "KEY_ArrowUp"],
+ initialVal: "1234-01-01",
+ expectedVal: "1234-01-02"
+ },
+ {
+ // PageUp on month field increments month by 3.
+ keys: ["KEY_PageUp"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-04-01"
+ },
+ {
+ // PageDown on month field decrements month by 3.
+ keys: ["KEY_PageDown"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-10-01"
+ },
+ {
+ // PageUp on day field increments day by 7.
+ keys: ["KEY_Tab", "KEY_PageUp"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-01-08"
+ },
+ {
+ // PageDown on day field decrements day by 7.
+ keys: ["KEY_Tab", "KEY_PageDown"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-01-25"
+ },
+ {
+ // PageUp on year field increments year by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageUp"],
+ initialVal: "1999-01-01",
+ expectedVal: "2009-01-01"
+ },
+ {
+ // PageDown on year field decrements year by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageDown"],
+ initialVal: "1999-01-01",
+ expectedVal: "1989-01-01"
+ },
+ {
+ // Home key on month field sets it to the minimum month, which is 01.
+ keys: ["KEY_Home"],
+ initialVal: "2016-06-01",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // End key on month field sets it to the maximum month, which is 12.
+ keys: ["KEY_End"],
+ initialVal: "2016-06-01",
+ expectedVal: "2016-12-01"
+ },
+ {
+ // Home key on day field sets it to the minimum day, which is 01.
+ keys: ["KEY_Tab", "KEY_Home"],
+ initialVal: "2016-01-10",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // End key on day field sets it to the maximum day, which is 31.
+ keys: ["KEY_Tab", "KEY_End"],
+ initialVal: "2016-01-10",
+ expectedVal: "2016-01-31"
+ },
+ {
+ // Home key should have no effect on year field.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Home"],
+ initialVal: "2016-01-01",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // End key should have no effect on year field.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_End"],
+ initialVal: "2016-01-01",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // Incomplete value maps to empty .value.
+ keys: ["1111"],
+ initialVal: "",
+ expectedVal: ""
+ },
+ {
+ // Backspace key should clean a month field and map to empty .value.
+ keys: ["KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: ""
+ },
+ {
+ // Backspace key should clean a day field and map to empty .value.
+ keys: ["KEY_Tab", "KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: ""
+ },
+ {
+ // Backspace key should clean a year field and map to empty .value.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: ""
+ },
+ {
+ // Backspace key on Calendar button should not change a value.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Tab", "KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: "2016-01-01"
+ },
+];
+
+function sendKeys(aKeys) {
+ for (let i = 0; i < aKeys.length; i++) {
+ let key = aKeys[i];
+ if (key.startsWith("KEY_")) {
+ synthesizeKey(key);
+ } else {
+ sendString(key);
+ }
+ }
+}
+
+function test() {
+ document.querySelector("#host").attachShadow({ mode: "open" }).innerHTML = `
+ <input type="date">
+ `;
+
+ function chromeListener(e) {
+ ok(false, "Picker should not be opened when dispatching untrusted click.");
+ }
+
+ for (const elem of [document.getElementById("input"), document.getElementById("host").shadowRoot.querySelector("input")]) {
+ for (let { keys, initialVal, expectedVal } of testData) {
+ elem.focus();
+ elem.value = initialVal;
+ sendKeys(keys);
+ is(elem.value, expectedVal,
+ "Test with " + keys + ", result should be " + expectedVal);
+ elem.value = "";
+ elem.blur();
+ }
+ SpecialPowers.addChromeEventListener("MozOpenDateTimePicker",
+ chromeListener);
+ elem.click();
+ SpecialPowers.removeChromeEventListener("MozOpenDateTimePicker",
+ chromeListener);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_calendar_button.html b/dom/html/test/forms/test_input_datetime_calendar_button.html
new file mode 100644
index 0000000000..970eee9027
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_calendar_button.html
@@ -0,0 +1,179 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1479708
+-->
+<head>
+<title>Test required date/datetime-local input's Calendar button</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Created for <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1479708">Mozilla Bug 1479708</a> and updated by <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1676068">Mozilla Bug 1676068</a> and <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1865885">Mozilla Bug 1865885</a>
+<p id="display"></p>
+<div id="content">
+<input type="date" id="id_date" value="2017-06-08">
+<input type="time" id="id_time" value="10:30">
+<input type="datetime-local" id="id_datetime-local" value="2017-06-08T10:30">
+<input type="date" id="id_date_required" value="2017-06-08" required>
+<input type="time" id="id_time_required" value="10:30" required>
+<input type="datetime-local" id="id_datetime-local_required" value="2017-06-08T10:30" required>
+<input type="date" id="id_date_readonly" value="2017-06-08" readonly>
+<input type="time" id="id_time_readonly" value="10:30" readonly>
+<input type="datetime-local" id="id_datetime-local_readonly" value="2017-06-08T10:30" readonly>
+<input type="date" id="id_date_disabled" value="2017-06-08" disabled>
+<input type="time" id="id_time_disabled" value="10:30" disabled>
+<input type="datetime-local" id="id_datetime-local_disabled" value="2017-06-08T10:30" disabled>
+</div>
+<pre id="test">
+<script class="testbody">
+
+const kTypes = ["date", "time", "datetime-local"];
+
+function id_for_type(type, kind) {
+ return "id_" + type + (kind ? "_" + kind : "");
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // Initial load.
+ assert_calendar_visible_all("");
+ assert_calendar_visible_all("required");
+ assert_calendar_hidden_all("readonly");
+ assert_calendar_hidden_all("disabled");
+
+ // Dynamic toggling.
+ test_make_readonly("");
+ test_make_editable("readonly");
+ test_disabled_field_disabled();
+
+ // Now toggle the inputs to the initial state, but while being
+ // display: none. This tests for bug 1567191.
+ for (const input of document.querySelectorAll("input")) {
+ input.style.display = "none";
+ is(input.getBoundingClientRect().width, 0, "Should be undisplayed");
+ }
+
+ test_make_readonly("readonly");
+ test_make_editable("");
+
+ // And test other toggling as well.
+ test_readonly_field_disabled();
+ test_disabled_field_disabled();
+
+ SimpleTest.finish();
+});
+
+function test_disabled_field_disabled() {
+ for (let type of kTypes) {
+ const id = id_for_type(type, "disabled");
+ const input = document.getElementById(id);
+
+ ok(input.disabled, `#${id} Should be disabled`);
+ ok(
+ is_calendar_button_hidden(id),
+ `disabled's Calendar button is hidden (${id})`
+ );
+
+ input.disabled = false;
+ ok(!input.disabled, `#${id} Should not be disabled anymore`);
+ if (type === "time") {
+ assert_calendar_hidden(id);
+ } else {
+ ok(
+ !is_calendar_button_hidden(id),
+ `enabled field's Calendar button is not hidden (${id})`
+ );
+ }
+
+ input.disabled = true; // reset to the original state.
+ }
+}
+
+function test_readonly_field_disabled() {
+ for (let type of kTypes) {
+ const id = id_for_type(type, "readonly");
+ const input = document.getElementById(id);
+
+ ok(input.readOnly, `#${id} Should be read-only`);
+ ok(is_calendar_button_hidden(id), `readonly field's Calendar button is hidden (${id})`);
+
+ input.readOnly = false;
+ ok(!input.readOnly, `#${id} Should not be read-only anymore`);
+ if (type === "time") {
+ assert_calendar_hidden(id);
+ } else {
+ ok(
+ !is_calendar_button_hidden(id),
+ `non-readonly field's Calendar button is not hidden (${id})`
+ );
+ }
+
+ input.readOnly = true; // reset to the original state.
+ }
+}
+
+function test_make_readonly(kind) {
+ for (let type of kTypes) {
+ const id = id_for_type(type, kind);
+ const input = document.getElementById(id);
+ is(input.readOnly, false, `Precondition: input #${id} is editable`);
+
+ input.readOnly = true;
+ assert_calendar_hidden(id);
+ }
+}
+
+function test_make_editable(kind) {
+ for (let type of kTypes) {
+ const id = id_for_type(type, kind);
+ const input = document.getElementById(id);
+ is(input.readOnly, true, `Precondition: input #${id} is read-only`);
+
+ input.readOnly = false;
+ if (type === "time") {
+ assert_calendar_hidden(id);
+ } else {
+ assert_calendar_visible(id);
+ }
+ }
+}
+
+function assert_calendar_visible_all(kind) {
+ for (let type of kTypes) {
+ if (type === "time") {
+ assert_calendar_hidden(id_for_type(type, kind));
+ } else {
+ assert_calendar_visible(id_for_type(type, kind));
+ }
+ }
+}
+function assert_calendar_visible(id) {
+ const isCalendarButtonHidden = is_calendar_button_hidden(id);
+ ok(!isCalendarButtonHidden, `Calendar button is not hidden on #${id}`);
+}
+
+function assert_calendar_hidden_all(kind) {
+ for (let type of kTypes) {
+ assert_calendar_hidden(id_for_type(type, kind));
+ }
+}
+
+function assert_calendar_hidden(id) {
+ const isCalendarButtonHidden = is_calendar_button_hidden(id);
+ ok(isCalendarButtonHidden, `Calendar button is hidden on #${id}`);
+}
+
+function is_calendar_button_hidden(id) {
+ const input = document.getElementById(id);
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarButton = shadowRoot.getElementById("calendar-button");
+ const calendarButtonDisplay = SpecialPowers.wrap(window).getComputedStyle(calendarButton).display;
+ return calendarButtonDisplay === "none";
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_disabled_focus.html b/dom/html/test/forms/test_input_datetime_disabled_focus.html
new file mode 100644
index 0000000000..68a89b1780
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_disabled_focus.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<title>Test for bugs 1772841 and 1865885</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1772841">Mozilla Bug 1772841</a> and <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1865885">Mozilla Bug 1865885</a>
+<div id="content">
+ <!-- Disabled -->
+ <input type="date" id="date" disabled>
+ <input type="time" id="time" disabled>
+ <input type="datetime-local" id="datetime-local" disabled>
+ <fieldset id="fieldset" disabled>
+ <input type="date" id="fieldset-date">
+ <input type="time" id="fieldset-time">
+ <input type="datetime-local" id="fieldset-datetime-local">
+ </fieldset>
+
+ <!-- Dynamically disabled -->
+ <input type="date" id="date1">
+ <input type="time" id="time1">
+ <input type="datetime-local" id="datetime-local1">
+ <fieldset id="fieldset1">
+ <input type="date" id="fieldset-date1">
+ <input type="time" id="fieldset-time1">
+ <input type="datetime-local" id="fieldset-datetime-local1">
+ </fieldset>
+
+ <!-- Dynamically enabled -->
+ <input type="date" id="date2" disabled>
+ <input type="time" id="time2" disabled>
+ <input type="datetime-local" id="datetime-local2" disabled>
+ <fieldset id="fieldset2" disabled>
+ <input type="date" id="fieldset-date2">
+ <input type="time" id="fieldset-time2">
+ <input type="datetime-local" id="fieldset-datetime-local2">
+ </fieldset>
+</div>
+<script>
+ /*
+ * Test for bugs 1772841 and 1865885
+ * This test checks that when a datetime input element is disabled by itself
+ * or from its containing fieldset, it should not be focusable by click.
+ **/
+
+ add_task(async function() {
+ await SimpleTest.promiseFocus(window);
+ for (let inputId of ["time", "date", "datetime-local", "fieldset-time", "fieldset-date", "fieldset-datetime-local"]) {
+ testFocusState(inputId, /* isDisabled = */ true);
+ testDynamicChange(inputId, "1", /* isDisabling = */ true);
+ testDynamicChange(inputId, "2", /* isDisabling = */ false);
+ }
+ })
+ function testFocusState(inputId, isDisabled) {
+ let input = document.getElementById(inputId);
+
+ document.getElementById("content").click();
+ input.click();
+ if (isDisabled) {
+ isnot(document.activeElement, input, `This disabled ${inputId} input should not be focusable by click`);
+ } else {
+ // The click method won't set the focus on clicked input, thus we
+ // only check that the state is changed to enabled here
+ ok(!input.disabled, `This ${inputId} input is not disabled`);
+ }
+
+ document.getElementById("content").click();
+ synthesizeMouseAtCenter(input, {});
+ if (isDisabled) {
+ isnot(document.activeElement, input, `This disabled ${inputId} input should not be focusable by click`);
+ } else {
+ is(document.activeElement, input, `This enabled ${inputId} input should be focusable by click`);
+ }
+ }
+ function testDynamicChange(inputId, index, isDisabling) {
+ if (inputId.split("-")[0] === "fieldset") {
+ document.getElementById("fieldset" + index).disabled = isDisabling;
+ } else {
+ document.getElementById(inputId + index).disabled = isDisabling;
+ }
+ testFocusState(inputId + index, /* isDisabled = */ isDisabling);
+ }
+</script>
diff --git a/dom/html/test/forms/test_input_datetime_focus_blur.html b/dom/html/test/forms/test_input_datetime_focus_blur.html
new file mode 100644
index 0000000000..bff7b2ceb8
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_blur.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+ <title>Test focus/blur behaviour for date/time input types</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input_time" type="time">
+ <input id="input_date" type="date">
+ <input id="input_datetime-local" type="datetime-local">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1288591.
+ * This test checks whether date/time input types' .focus()/.blur() works
+ * correctly. This test also checks when focusing on an date/time input element,
+ * the focus is redirected to the anonymous text control, but the
+ * document.activeElement still returns date/time input element.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function testFocusBlur(type) {
+ let input = document.getElementById("input_" + type);
+ input.focus();
+
+ // The active element returns the date/time input element.
+ let activeElement = document.activeElement;
+ is(activeElement, input, "activeElement should be the date/time input element");
+ is(activeElement.localName, "input", "activeElement should be an input element");
+ is(activeElement.type, type, "activeElement should be of type " + type);
+
+ // Use FocusManager to check that the actual focus is on the anonymous
+ // text control.
+ let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
+ .getService(SpecialPowers.Ci.nsIFocusManager);
+ let focusedElement = fm.focusedElement;
+ is(focusedElement.localName, "span", "focusedElement should be an span element");
+
+ input.blur();
+ isnot(document.activeElement, input, "activeElement should no longer be the datetime input element");
+}
+
+function test() {
+ for (let inputType of ["time", "date", "datetime-local"]) {
+ testFocusBlur(inputType);
+ }
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_focus_blur_events.html b/dom/html/test/forms/test_input_datetime_focus_blur_events.html
new file mode 100644
index 0000000000..2e4e918119
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_blur_events.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1301306
+-->
+<head>
+<title>Test for Bug 1301306</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301306">Mozilla Bug 722599</a>
+<p id="display"></p>
+<div id="content">
+<input type="time" id="input_time" onfocus="++focusEvents[0]"
+ onblur="++blurEvents[0]" onfocusin="++focusInEvents[0]"
+ onfocusout="++focusOutEvents[0]">
+<input type="date" id="input_date" onfocus="++focusEvents[1]"
+ onblur="++blurEvents[1]" onfocusin="++focusInEvents[1]"
+ onfocusout="++focusOutEvents[1]">
+<input type="datetime-local" id="input_datetime-local" onfocus="++focusEvents[2]"
+ onblur="++blurEvents[2]" onfocusin="++focusInEvents[2]"
+ onfocusout="++focusOutEvents[2]">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/**
+ * Test for Bug 1301306.
+ * This test checks that when moving inside the time input element, e.g. jumping
+ * through the inner text boxes, does not fire extra focus/blur events.
+ **/
+
+var inputTypes = ["time", "date", "datetime-local"];
+var focusEvents = [0, 0, 0];
+var focusInEvents = [0, 0, 0];
+var focusOutEvents = [0, 0, 0];
+var blurEvents = [0, 0, 0];
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ for (var i = 0; i < inputTypes.length; i++) {
+ var input = document.getElementById("input_" + inputTypes[i]);
+
+ input.focus();
+ is(focusEvents[i], 1, inputTypes[i] + " input element should have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ // Move around inside the input element's input box.
+ synthesizeKey("KEY_Tab");
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " time input element should not have dispatched blur event.");
+
+ synthesizeKey("KEY_ArrowRight");
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ synthesizeKey("KEY_ArrowLeft");
+ is(focusEvents[i], 1,inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ synthesizeKey("KEY_ArrowRight");
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ input.blur();
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 1, inputTypes[i] + " input element should have dispatched focusout event.");
+ is(blurEvents[i], 1, inputTypes[i] + " input element should have dispatched blur event.");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_focus_state.html b/dom/html/test/forms/test_input_datetime_focus_state.html
new file mode 100644
index 0000000000..3b771f2394
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_state.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1346085
+-->
+<head>
+ <title>Test moving focus in onfocus/onblur handler</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1346085">Mozilla Bug 1346085</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input_time" type="time">
+ <input id="input_date" type="date">
+ <input id="input_dummy" type="text">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1346085.
+ * This test checks whether date/time input types' focus state are set
+ * correctly, event when moving focus in onfocus/onblur handler.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function testFocusState(type) {
+ let input = document.getElementById("input_" + type);
+
+ input.focus();
+ let focus = document.querySelector(":focus");
+ let focusRing = document.querySelector(":-moz-focusring");
+ is(focus, input, "input should have :focus state after focus");
+ is(focusRing, input, "input should have :-moz-focusring state after focus");
+
+ input.blur();
+ focus = document.querySelector(":focus");
+ focusRing = document.querySelector(":-moz-focusring");
+ isnot(focus, input, "input should not have :focus state after blur");
+ isnot(focusRing, input, "input should not have :-moz-focusring state after blur");
+
+ input.addEventListener("focus", function() {
+ document.getElementById("input_dummy").focus();
+ }, { once: true });
+
+ input.focus();
+ focus = document.querySelector(":focus");
+ focusRing = document.querySelector(":-moz-focusring");
+ isnot(focus, input, "input should not have :focus state when moving focus in onfocus handler");
+ isnot(focusRing, input, "input should not have :-moz-focusring state when moving focus in onfocus handler");
+
+ input.addEventListener("blur", function() {
+ document.getElementById("input_dummy").focus();
+ }, { once: true });
+
+ input.blur();
+ focus = document.querySelector(":focus");
+ focusRing = document.querySelector(":-moz-focusring");
+ isnot(focus, input, "input should not have :focus state when moving focus in onblur handler");
+ isnot(focusRing, input, "input should not have :-moz-focusring state when moving focus in onblur handler");
+}
+
+function test() {
+ let inputTypes = ["time", "date"];
+
+ for (let i = 0; i < inputTypes.length; i++) {
+ testFocusState(inputTypes[i]);
+ }
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_hidden.html b/dom/html/test/forms/test_input_datetime_hidden.html
new file mode 100644
index 0000000000..7d8a6766a9
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_hidden.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1514040
+-->
+<head>
+ <title>Test construction of hidden date input type</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1514040">Mozilla Bug 1514040</a>
+<p id="display"></p>
+<div id="content">
+ <input id="date" type="date" hidden value="1947-02-28">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+let el = document.getElementById("date");
+ok(el.hidden, "element is hidden");
+is(el.value, "1947-02-28", ".value is set correctly");
+let fieldElements = Array.from(SpecialPowers.wrap(el).openOrClosedShadowRoot.querySelectorAll(".datetime-edit-field"));
+is(fieldElements[0].textContent, "02", "month is set");
+is(fieldElements[1].textContent, "28", "day is set");
+is(fieldElements[2].textContent, "1947", "year is set");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html
new file mode 100644
index 0000000000..63c8012252
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_input_change_events.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1370858
+-->
+<head>
+<title>Test for Bugs 1370858 and 1804881</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370858">Mozilla Bug 1370858</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1804881">Mozilla Bug 1804881</a>
+<p id="display"></p>
+<div id="content">
+<input type="time" id="input_time" onchange="++changeEvents[0]"
+ oninput="++inputEvents[0]">
+<input type="date" id="input_date" onchange="++changeEvents[1]"
+ oninput="++inputEvents[1]">
+<input type="datetime-local" id="input_datetime-local" onchange="++changeEvents[2]"
+ oninput="++inputEvents[2]">
+</div>
+<pre id="test">
+<script class="testbody">
+
+/**
+ * Test for Bug 1370858.
+ * Test that change and input events are (not) fired for date/time inputs.
+ **/
+
+const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+var inputTypes = ["time", "date", "datetime-local"];
+var changeEvents = [0, 0, 0];
+var inputEvents = [0, 0, 0];
+var values = ["10:30", "2017-06-08", "2017-06-08T10:30"];
+var expectedValues = [
+ ["09:30", "01:30", "01:25", "", "01:59", "13:59", ""],
+ ["2017-05-08", "2017-01-08", "2017-01-25", "", "2017-01-31", "2017-01-31", ""],
+ ["2017-05-08T10:30", "2017-01-08T10:30", "2017-01-25T10:30", "", "2017-01-31T10:30", "2017-01-31T10:30", ""]
+];
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ for (var i = 0; i < inputTypes.length; i++) {
+ var input = document.getElementById("input_" + inputTypes[i]);
+
+ is(changeEvents[i], 0, "Number of change events should be 0 at start.");
+ is(inputEvents[i], 0, "Number of input events should be 0 at start.");
+
+ // Test that change and input events are not dispatched setting .value by
+ // script.
+ input.value = values[i];
+ is(input.value, values[i], "Check that value was set correctly (0).");
+ is(changeEvents[i], 0, "Change event should not have dispatched (0).");
+ is(inputEvents[i], 0, "Input event should not have dispatched (0).");
+
+ // Test that change and input events are fired when changing the value using
+ // up/down keys.
+ input.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(input.value, expectedValues[i][0], "Check that value was set correctly (1).");
+ is(changeEvents[i], 1, "Change event should be dispatched (1).");
+ is(inputEvents[i], 1, "Input event should be dispatched (1).");
+
+ // Test that change and input events are fired when changing the value with
+ // the keyboard.
+ sendString("01");
+ // We get event per character.
+ is(input.value, expectedValues[i][1], "Check that value was set correctly (2).");
+ is(changeEvents[i], 3, "Change event should be dispatched (2).");
+ is(inputEvents[i], 3, "Input event should be dispatched (2).");
+
+ // Test that change and input events are fired when changing the value with
+ // both the numeric keyboard and digit keys.
+ synthesizeKey("2", { code: "Numpad2" });
+ synthesizeKey("5");
+ // We get event per character.
+ is(input.value, expectedValues[i][2], "Check that value was set correctly (3).");
+ is(changeEvents[i], 5, "Change event should be dispatched (3).");
+ is(inputEvents[i], 5, "Input event should be dispatched (3).");
+
+ // Test that change and input events are not fired when navigating with Tab.
+ // Return to the previously focused field (minutes, day, day).
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ is(input.value, expectedValues[i][2], "Check that value was not changed (4).");
+ is(changeEvents[i], 5, "Change event should not be dispatched (4).");
+ is(inputEvents[i], 5, "Input event should not be dispatched (4).");
+
+ // Test that change and input events are fired when using Backspace.
+ synthesizeKey("KEY_Backspace");
+ // We get event per character.
+ is(input.value, expectedValues[i][3], "Check that value was set correctly (5).");
+ is(changeEvents[i], 6, "Change event should be dispatched (5).");
+ is(inputEvents[i], 6, "Input event should be dispatched (5).");
+
+ // Test that change and input events are fired when using Home key.
+ synthesizeKey("KEY_End");
+ // We get event per character.
+ is(input.value, expectedValues[i][4], "Check that value was set correctly (6).");
+ is(changeEvents[i], 7, "Change event should be dispatched (6).");
+ is(inputEvents[i], 7, "Input event should be dispatched (6).");
+
+ // Test that change and input events are fired for time and not fired
+ // for others when changing the value with a letter key.
+ // Navigate to the next field (time of the day, year, year).
+ synthesizeKey("KEY_Tab");
+ synthesizeKey("P");
+ // We get event per character.
+ is(input.value, expectedValues[i][5], "Check that value was set correctly (7).");
+ if (i === 0) {
+ // For the time input, the time of the day should be focused and it,
+ // as an AM/PM toggle, should change to "PM" when the "p" key is pressed
+ is(changeEvents[i], 8, "Change event should be dispatched (7).");
+ is(inputEvents[i], 8, "Input event should be dispatched (7).");
+ } else {
+ // For the date and datetime inputs, the year should be focused and it,
+ // as a numeric value, should not change when the "p" key is pressed
+ is(changeEvents[i], 7, "Change event should not be dispatched (7).");
+ is(inputEvents[i], 7, "Input event should not be dispatched (7).");
+ }
+
+ // Test that change and input events are fired when clearing the value
+ // using a Ctrl/Cmd+Delete/Backspace key combination
+ let events = (i === 0) ? 9 : 8;
+ synthesizeKey("KEY_Backspace", { accelKey: true });
+ // We get one event
+ is(input.value, expectedValues[i][6], "Check that value was cleared out correctly (8).");
+ is(changeEvents[i], events, "Change event should be dispatched (8).");
+ is(inputEvents[i], events, "Input event should be dispatched (8).");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_readonly.html b/dom/html/test/forms/test_input_datetime_readonly.html
new file mode 100644
index 0000000000..aa7b40753b
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_readonly.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Test for bug 1461509</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"/>
+<input id="i" type="date" value="1995-11-20" readonly required>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let input = document.getElementById("i");
+ let value = input.value;
+
+ isnot(value, "", "should have a value");
+
+ input.focus();
+ synthesizeKey("KEY_Backspace");
+ is(input.value, value, "Value shouldn't change");
+ SimpleTest.finish();
+});
+</script>
diff --git a/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
new file mode 100644
index 0000000000..393de9fdee
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1446722
+-->
+<head>
+<title>Test for bug 1446722</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script type="application/javascript" src="utils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1446722">Mozilla bug 1446722</a>
+<p id="display"></p>
+<div id="content">
+<form>
+<input type="time" id="input_time" value="10:30" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="date" id="input_date" value="2012-05-06" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="time" id="input_time2" value="11:30" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="date" id="input_date2" value="2014-07-08"
+ onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="time" id="input_time3" value="12:30" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="date" id="input_date3" value="2014-08-09"
+ onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="reset" id="input_reset">
+</form>
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+/**
+ * Test for bug 1446722.
+ *
+ * Test change and input events are fired for date and time inputs when the
+ * default value is reset from the date UI and the time UI.
+ * Test they are not fired when the value is changed via a script.
+ * Test clicking the reset button of a form does not fire these events.
+ **/
+
+const INPUT_FIELD_ID_PREFIX = "input_";
+
+var numberChangeEvents = 0;
+var numberInputEvents = 0;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test_reset_in_script_does_not_trigger_change_and_input_event(
+ "time2", numberChangeEvents, numberInputEvents);
+ test_reset_in_script_does_not_trigger_change_and_input_event(
+ "date2", numberChangeEvents, numberInputEvents);
+
+ test_reset_form_does_not_trigger_change_and_input_events("time3", "14:00",
+ numberChangeEvents, numberInputEvents);
+ test_reset_form_does_not_trigger_change_and_input_events("date3", "2016-01-01",
+ numberChangeEvents, numberInputEvents);
+
+ SimpleTest.finish();
+});
+
+function test_reset_in_script_does_not_trigger_change_and_input_event(
+ inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) {
+ const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+ var input = document.getElementById(inputFieldName);
+
+ is(input.value, input.defaultValue,
+ "Check " + inputFieldName + "'s default value is initialized correctly.");
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+ ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Check numberInputEvents is initialized correctly for " + inputFieldName +
+ ".");
+
+ input.value = "";
+
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Change event should not be dispatched for " + inputFieldName + ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Input event should not be dispatched for " + inputFieldName + ".");
+}
+
+function test_reset_form_does_not_trigger_change_and_input_events(
+ inputFieldIdSuffix, newValue, oldNumberChangeEvents, oldNumberInputEvents) {
+ const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+ const inputFieldResetButtonName = "input_reset";
+ var input = document.getElementById(inputFieldName);
+
+ is(input.value, input.defaultValue,
+ "Check " + inputFieldName + "'s default value is initialized correctly.");
+ isnot(input.defaultValue, newValue, "Check default value differs from newValue for " +
+ inputFieldName + ".");
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+ ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Check numberInputEvents is initialized correctly for " + inputFieldName +
+ ".");
+
+ input.value = newValue;
+
+ var resetButton = document.getElementById(inputFieldResetButtonName);
+ synthesizeMouseAtCenter(resetButton, {});
+
+ is(input.value, input.defaultValue, "Check value is reset to default for " +
+ inputFieldName + ".");
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Change event should not be dispatched for " + inputFieldResetButtonName + ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Input event should not be dispatched for " + inputFieldResetButtonName + ".");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_tabindex.html b/dom/html/test/forms/test_input_datetime_tabindex.html
new file mode 100644
index 0000000000..207a7a8a8e
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_tabindex.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+ <title>Test tabindex attribute for date/time input types</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+ <input id="time1" type="time" tabindex="0">
+ <input id="time2" type="time" tabindex="-1">
+ <input id="time3" type="time" tabindex="0">
+ <input id="time4" type="time" disabled>
+ <input id="date1" type="date" tabindex="0">
+ <input id="date2" type="date" tabindex="-1">
+ <input id="date3" type="date" tabindex="0">
+ <input id="date4" type="date" disabled>
+ <input id="datetime-local1" type="datetime-local" tabindex="0">
+ <input id="datetime-local2" type="datetime-local" tabindex="-1">
+ <input id="datetime-local3" type="datetime-local" tabindex="0">
+ <input id="datetime-local4" type="datetime-local" disabled>
+</div>
+<pre id="test">
+<script>
+/**
+ * Test for Bug 1288591.
+ * This test checks whether date/time input types tabindex attribute works
+ * correctly.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function checkInnerTextboxTabindex(input, tabindex) {
+ let fields = SpecialPowers.wrap(input).openOrClosedShadowRoot.querySelectorAll(".datetime-edit-field");
+
+ for (let field of fields) {
+ is(field.tabIndex, tabindex, "tabIndex in the inner textbox should be correct");
+ }
+
+}
+
+function testTabindex(type) {
+ let input1 = document.getElementById(type + "1");
+ let input2 = document.getElementById(type + "2");
+ let input3 = document.getElementById(type + "3");
+ let input4 = document.getElementById(type + "4");
+
+ input1.focus();
+ is(document.activeElement, input1,
+ "input element with tabindex=0 is focusable");
+
+ // Time input does not include a Calendar button
+ let fieldCount;
+ if (type == "datetime-local") {
+ fieldCount = 7;
+ } else if (type == "date") {
+ fieldCount = 4;
+ } else {
+ fieldCount = 3;
+ };
+
+ // Advance through inner fields.
+ for (let i = 0; i < fieldCount - 1; ++i) {
+ synthesizeKey("KEY_Tab");
+ is(document.activeElement, input1,
+ "input element with tabindex=0 is tabbable");
+ }
+
+ // Advance to next element
+ synthesizeKey("KEY_Tab");
+ is(document.activeElement, input3,
+ "input element with tabindex=-1 is not tabbable");
+
+ input2.focus();
+ is(document.activeElement, input2,
+ "input element with tabindex=-1 is still focusable");
+
+ checkInnerTextboxTabindex(input1, 0);
+ checkInnerTextboxTabindex(input2, -1);
+ checkInnerTextboxTabindex(input3, 0);
+
+ // Changing the tabindex attribute dynamically.
+ input3.setAttribute("tabindex", "-1");
+
+ synthesizeKey("KEY_Tab"); // need only one TAB since input2 is not tabbable
+
+ isnot(document.activeElement, input3,
+ "element with tabindex changed to -1 should not be tabbable");
+ isnot(document.activeElement, input4,
+ "disabled element should not be tabbable");
+
+ checkInnerTextboxTabindex(input3, -1);
+}
+
+function test() {
+ for (let inputType of ["time", "date", "datetime-local"]) {
+ testTabindex(inputType);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_defaultValue.html b/dom/html/test/forms/test_input_defaultValue.html
new file mode 100644
index 0000000000..03849d7f54
--- /dev/null
+++ b/dom/html/test/forms/test_input_defaultValue.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=977029
+-->
+<head>
+ <title>Test for Bug 977029</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<div id="content">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=977029">Bug 977029</a>
+ <p>
+ Goal of this test is to check that modifying defaultValue and value attribute
+ of input types is working as expected.
+ </p>
+ <form>
+ <input id='a' type="color" value="#00ff00">
+ <input id='b' type="text" value="foo">
+ <input id='c' type="email" value="foo">
+ <input id='d' type="date" value="2010-09-20">
+ <input id='e' type="search" value="foo">
+ <input id='f' type="tel" value="foo">
+ <input id='g' type="url" value="foo">
+ <input id='h' type="number" value="42">
+ <input id='i' type="range" value="42" min="0" max="100">
+ <input id='j' type="time" value="17:00:25.54">
+ </form>
+</div>
+<script type="application/javascript">
+
+// [ element id | original defaultValue | another value | another default value]
+// Preferably use only valid values: the goal of this test isn't to test the
+// value sanitization algorithm (for input types which have one) as this is
+// already part of another test)
+var testData = [["a", "#00ff00", "#00aaaa", "#00ccaa"],
+ ["b", "foo", "bar", "tulip"],
+ ["c", "foo", "foo@bar.org", "tulip"],
+ ["d", "2010-09-20", "2012-09-21", ""],
+ ["e", "foo", "bar", "tulip"],
+ ["f", "foo", "bar", "tulip"],
+ ["g", "foo", "bar", "tulip"],
+ ["h", "42", "1337", "3"],
+ ["i", "42", "17", "3"],
+ ["j", "17:00:25.54", "07:00:25", "03:00:03"],
+ ];
+
+for (var data of testData) {
+ id = data[0];
+ input = document.getElementById(id);
+ originalDefaultValue = data[1];
+ is(originalDefaultValue, input.defaultValue,
+ "Default value isn't the expected one");
+ is(originalDefaultValue, input.value,
+ "input.value original value is different from defaultValue");
+ input.defaultValue = data[2]
+ is(input.defaultValue, input.value,
+ "Changing default value before value was changed should change value too");
+ input.value = data[3];
+ input.defaultValue = originalDefaultValue;
+ is(input.value, data[3],
+ "Changing default value after value was changed should not change value");
+ input.value = data[2];
+ is(originalDefaultValue, input.defaultValue,
+ "defaultValue shouldn't change when changing value");
+ input.defaultValue = data[3];
+ is(input.defaultValue, data[3],
+ "defaultValue should have changed");
+ // Change the value...
+ input.value = data[2];
+ is(input.value, data[2],
+ "value should have changed");
+ // ...then reset the form
+ input.form.reset();
+ is(input.defaultValue, input.value,
+ "reset form should bring back the default value");
+}
+</script>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_input_email.html b/dom/html/test/forms/test_input_email.html
new file mode 100644
index 0000000000..96ff939215
--- /dev/null
+++ b/dom/html/test/forms/test_input_email.html
@@ -0,0 +1,237 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=555559
+https://bugzilla.mozilla.org/show_bug.cgi?id=668817
+https://bugzilla.mozilla.org/show_bug.cgi?id=854812
+-->
+<head>
+ <title>Test for &lt;input type='email'&gt; validity</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555559">Mozilla Bug 555559</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668817">Mozilla Bug 668817</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=854812">Mozilla Bug 854812</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form>
+ <input type='email' name='email' id='i' oninvalid="invalidEventHandler(event);">
+ <form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for <input type='email'> validity **/
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be invalid");
+ gInvalid = true;
+}
+
+function checkValidEmailAddress(element)
+{
+ gInvalid = false;
+ ok(!element.validity.typeMismatch && !element.validity.badInput,
+ "Element should not suffer from type mismatch or bad input (with value='"+element.value+"')");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "The invalid event should not have been thrown");
+ is(element.validationMessage, '',
+ "Validation message should be the empty string");
+ ok(element.matches(":valid"), ":valid pseudo-class should apply");
+}
+
+const VALID = 0;
+const TYPE_MISMATCH = 1 << 0;
+const BAD_INPUT = 1 << 1;
+
+function checkInvalidEmailAddress(element, failedValidityStates)
+{
+ info("Checking " + element.value);
+ gInvalid = false;
+ var expectTypeMismatch = !!(failedValidityStates & TYPE_MISMATCH);
+ var expectBadInput = !!(failedValidityStates & BAD_INPUT);
+ ok(element.validity.typeMismatch == expectTypeMismatch,
+ "Element should " + (expectTypeMismatch ? "" : "not ") + "suffer from type mismatch (with value='"+element.value+"')");
+ ok(element.validity.badInput == expectBadInput,
+ "Element should " + (expectBadInput ? "" : "not ") + "suffer from bad input (with value='"+element.value+"')");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+ ok(gInvalid, "The invalid event should have been thrown");
+ is(element.validationMessage, "Please enter an email address.",
+ "Validation message is not valid");
+ ok(element.matches(":invalid"), ":invalid pseudo-class should apply");
+}
+
+function testEmailAddress(aElement, aValue, aMultiple, aValidityFailures)
+{
+ aElement.multiple = aMultiple;
+ aElement.value = aValue;
+
+ if (!aValidityFailures) {
+ checkValidEmailAddress(aElement);
+ } else {
+ checkInvalidEmailAddress(aElement, aValidityFailures);
+ }
+}
+
+var email = document.forms[0].elements[0];
+
+// Simple values, checking the e-mail syntax validity.
+var values = [
+ [ '' ], // The empty string shouldn't be considered as invalid.
+ [ 'foo@bar.com', VALID ],
+ [ ' foo@bar.com', VALID ],
+ [ 'foo@bar.com ', VALID ],
+ [ '\r\n foo@bar.com', VALID ],
+ [ 'foo@bar.com \n\r', VALID ],
+ [ '\n\n \r\rfoo@bar.com\n\n \r\r', VALID ],
+ [ '\n\r \n\rfoo@bar.com\n\r \n\r', VALID ],
+ [ 'tulip', TYPE_MISMATCH ],
+ // Some checks on the user part of the address.
+ [ '@bar.com', TYPE_MISMATCH ],
+ [ 'f\noo@bar.com', VALID ],
+ [ 'f\roo@bar.com', VALID ],
+ [ 'f\r\noo@bar.com', VALID ],
+ [ 'fü@foo.com', TYPE_MISMATCH ],
+ // Some checks for the domain part.
+ [ 'foo@bar', VALID ],
+ [ 'foo@b', VALID ],
+ [ 'foo@', TYPE_MISMATCH ],
+ [ 'foo@bar.', TYPE_MISMATCH ],
+ [ 'foo@foo.bar', VALID ],
+ [ 'foo@foo..bar', TYPE_MISMATCH ],
+ [ 'foo@.bar', TYPE_MISMATCH ],
+ [ 'foo@tulip.foo.bar', VALID ],
+ [ 'foo@tulip.foo-bar', VALID ],
+ [ 'foo@1.2', VALID ],
+ [ 'foo@127.0.0.1', VALID ],
+ [ 'foo@1.2.3', VALID ],
+ [ 'foo@b\nar.com', VALID ],
+ [ 'foo@b\rar.com', VALID ],
+ [ 'foo@b\r\nar.com', VALID ],
+ [ 'foo@.', TYPE_MISMATCH ],
+ [ 'foo@fü.com', VALID ],
+ [ 'foo@fu.cüm', VALID ],
+ [ 'thisUsernameIsLongerThanSixtyThreeCharactersInLengthRightAboutNow@mozilla.tld', VALID ],
+ // Long strings with UTF-8 in username.
+ [ 'this.is.email.should.be.longer.than.sixty.four.characters.föö@mözillä.tld', TYPE_MISMATCH ],
+ [ 'this-is-email-should-be-longer-than-sixty-four-characters-föö@mözillä.tld', TYPE_MISMATCH, true ],
+ // Long labels (labels greater than 63 chars long are not allowed).
+ [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ],
+ [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ],
+ [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ],
+ [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ],
+ [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ // Long labels with UTF-8 (punycode encoding will increase the label to more than 63 chars).
+ [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ // The domains labels (sub-domains or tld) can't start or finish with a '-'
+ [ 'foo@foo-bar', VALID ],
+ [ 'foo@-foo', TYPE_MISMATCH ],
+ [ 'foo@foo-.bar', TYPE_MISMATCH ],
+ [ 'foo@-.-', TYPE_MISMATCH ],
+ [ 'foo@fo-o.bar', VALID ],
+ [ 'foo@fo-o.-bar', TYPE_MISMATCH ],
+ [ 'foo@fo-o.bar-', TYPE_MISMATCH ],
+ [ 'foo@fo-o.-', TYPE_MISMATCH ],
+ [ 'foo@fo--o', VALID ],
+];
+
+// Multiple values, we don't check e-mail validity, only multiple stuff.
+var multipleValues = [
+ [ 'foo@bar.com, foo@bar.com', VALID ],
+ [ 'foo@bar.com,foo@bar.com', VALID ],
+ [ 'foo@bar.com,foo@bar.com,foo@bar.com', VALID ],
+ [ ' foo@bar.com , foo@bar.com ', VALID ],
+ [ '\tfoo@bar.com\t,\tfoo@bar.com\t', VALID ],
+ [ '\rfoo@bar.com\r,\rfoo@bar.com\r', VALID ],
+ [ '\nfoo@bar.com\n,\nfoo@bar.com\n', VALID ],
+ [ '\ffoo@bar.com\f,\ffoo@bar.com\f', VALID ],
+ [ '\t foo@bar.com\r,\nfoo@bar.com\f', VALID ],
+ [ 'foo@b,ar.com,foo@bar.com', TYPE_MISMATCH ],
+ [ 'foo@bar.com,foo@bar.com,', TYPE_MISMATCH ],
+ [ ' foo@bar.com , foo@bar.com , ', TYPE_MISMATCH ],
+ [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ],
+ [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ],
+ [ 'foo@bar.com,,,foo@bar.com', TYPE_MISMATCH ],
+ [ 'foo@bar.com;foo@bar.com', TYPE_MISMATCH ],
+ [ '<foo@bar.com>, <foo@bar.com>', TYPE_MISMATCH ],
+ [ 'foo@bar, foo@bar.com', VALID ],
+ [ 'foo@bar.com, foo', TYPE_MISMATCH ],
+ [ 'foo, foo@bar.com', TYPE_MISMATCH ],
+];
+
+/* Additional username checks. */
+
+var legalCharacters = "abcdefghijklmnopqrstuvwxyz";
+legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+legalCharacters += "0123456789";
+legalCharacters += "!#$%&'*+-/=?^_`{|}~.";
+
+// Add all username legal characters individually to the list.
+for (c of legalCharacters) {
+ values.push([c + "@bar.com", VALID]);
+}
+// Add the concatenation of all legal characters too.
+values.push([legalCharacters + "@bar.com", VALID]);
+
+// Add username illegal characters, the same way.
+var illegalCharacters = "()<>[]:;@\\, \t";
+for (c of illegalCharacters) {
+ values.push([illegalCharacters + "@bar.com", TYPE_MISMATCH]);
+}
+
+/* Additional domain checks. */
+
+legalCharacters = "abcdefghijklmnopqrstuvwxyz";
+legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+legalCharacters += "0123456789";
+
+// Add domain legal characters (except '.' and '-' because they are special).
+for (c of legalCharacters) {
+ values.push(["foo@foo.bar" + c, VALID]);
+}
+// Add the concatenation of all legal characters too.
+values.push(["foo@bar." + legalCharacters, VALID]);
+
+// Add domain illegal characters.
+illegalCharacters = "()<>[]:;@\\,!#$%&'*+/=?^_`{|}~ \t";
+for (c of illegalCharacters) {
+ values.push(['foo@foo.ba' + c + 'r', TYPE_MISMATCH]);
+}
+
+values.forEach(function([value, valid, todo]) {
+ if (todo === true) {
+ email.value = value;
+ todo_is(email.validity.valid, true, "value should be valid");
+ } else {
+ testEmailAddress(email, value, false, valid);
+ }
+});
+
+multipleValues.forEach(function([value, valid]) {
+ testEmailAddress(email, value, true, valid);
+});
+
+// Make sure setting multiple changes the value.
+email.multiple = false;
+email.value = "foo@bar.com, foo@bar.com";
+checkInvalidEmailAddress(email, TYPE_MISMATCH);
+email.multiple = true;
+checkValidEmailAddress(email);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_event.html b/dom/html/test/forms/test_input_event.html
new file mode 100644
index 0000000000..72863ca335
--- /dev/null
+++ b/dom/html/test/forms/test_input_event.html
@@ -0,0 +1,409 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=851780
+-->
+<head>
+<title>Test for input event</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=851780">Mozilla Bug 851780</a>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Test for input event. This is highly based on test_change_event.html **/
+
+ const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+ let expectedInputType = "";
+ let expectedData = null;
+ let expectedBeforeInputCancelable = false;
+ function checkBeforeInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"beforeinput" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.inputType, expectedInputType,
+ `inputType of "beforeinput" event should be "${expectedInputType}" ${aDescription}`);
+ is(aEvent.data, expectedData,
+ `data of "beforeinput" event should be ${expectedData} ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "beforeinput" event should be null ${aDescription}`);
+ is(aEvent.getTargetRanges().length, 0,
+ `getTargetRanges() of "beforeinput" event should return empty array ${aDescription}`);
+ is(aEvent.cancelable, expectedBeforeInputCancelable,
+ `"beforeinput" event for "${expectedInputType}" should ${expectedBeforeInputCancelable ? "be" : "not be"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"beforeinput" event should always bubble ${aDescription}`);
+ }
+
+ let skipExpectedDataCheck = false;
+ function checkIfInputIsInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.inputType, expectedInputType,
+ `inputType should be "${expectedInputType}" ${aDescription}`);
+ if (!skipExpectedDataCheck)
+ is(aEvent.data, expectedData, `data should be ${expectedData} ${aDescription}`);
+ else
+ info(`data is ${aEvent.data} ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer should be null ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"input" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"input" event should always bubble ${aDescription}`);
+ }
+
+ function checkIfInputIsEvent(aEvent, aDescription) {
+ ok(aEvent instanceof Event && !(aEvent instanceof UIEvent),
+ `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"input" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"input" event should always bubble ${aDescription}`);
+ }
+
+ let textareaInput = 0, textareaBeforeInput = 0;
+ let textTypes = ["text", "email", "search", "tel", "url", "password"];
+ let textBeforeInput = [0, 0, 0, 0, 0, 0];
+ let textInput = [0, 0, 0, 0, 0, 0];
+ let nonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
+ let nonTextBeforeInput = [0, 0, 0, 0, 0, 0];
+ let nonTextInput = [0, 0, 0, 0, 0, 0];
+ let rangeInput = 0, rangeBeforeInput = 0;
+ let numberInput = 0, numberBeforeInput = 0;
+
+ // Don't create elements whose event listener attributes are required before enabling `beforeinput` event.
+ function init() {
+ document.getElementById("content").innerHTML =
+ `<input type="file" id="fileInput">
+ <textarea id="textarea"></textarea>
+ <input type="text" id="input_text">
+ <input type="email" id="input_email">
+ <input type="search" id="input_search">
+ <input type="tel" id="input_tel">
+ <input type="url" id="input_url">
+ <input type="password" id="input_password">
+
+ <!-- "Non-text" inputs-->
+ <input type="button" id="input_button">
+ <input type="submit" id="input_submit">
+ <input type="image" id="input_image">
+ <input type="reset" id="input_reset">
+ <input type="radio" id="input_radio">
+ <input type="checkbox" id="input_checkbox">
+ <input type="range" id="input_range">
+ <input type="number" id="input_number">`;
+
+ document.getElementById("textarea").addEventListener("beforeinput", (aEvent) => {
+ ++textareaBeforeInput;
+ checkBeforeInputEvent(aEvent, "on textarea element");
+ });
+ document.getElementById("textarea").addEventListener("input", (aEvent) => {
+ ++textareaInput;
+ checkIfInputIsInputEvent(aEvent, "on textarea element");
+ });
+
+ // These are the type were the input event apply.
+ for (let id of ["input_text", "input_email", "input_search", "input_tel", "input_url", "input_password"]) {
+ document.getElementById(id).addEventListener("beforeinput", (aEvent) => {
+ ++textBeforeInput[textTypes.indexOf(aEvent.target.type)];
+ checkBeforeInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+ });
+ document.getElementById(id).addEventListener("input", (aEvent) => {
+ ++textInput[textTypes.indexOf(aEvent.target.type)];
+ checkIfInputIsInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+ });
+ }
+
+ // These are the type were the input event does not apply.
+ for (let id of ["input_button", "input_submit", "input_image", "input_reset", "input_radio", "input_checkbox"]) {
+ document.getElementById(id).addEventListener("beforeinput", (aEvent) => {
+ ++nonTextBeforeInput[nonTextTypes.indexOf(aEvent.target.type)];
+ });
+ document.getElementById(id).addEventListener("input", (aEvent) => {
+ ++nonTextInput[nonTextTypes.indexOf(aEvent.target.type)];
+ checkIfInputIsEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+ });
+ }
+
+ document.getElementById("input_range").addEventListener("beforeinput", (aEvent) => {
+ ++rangeBeforeInput;
+ });
+ document.getElementById("input_range").addEventListener("input", (aEvent) => {
+ ++rangeInput;
+ checkIfInputIsEvent(aEvent, "on input element whose type is range");
+ });
+
+ document.getElementById("input_number").addEventListener("beforeinput", (aEvent) => {
+ ++numberBeforeInput;
+ });
+ document.getElementById("input_number").addEventListener("input", (aEvent) => {
+ ++numberInput;
+ checkIfInputIsInputEvent(aEvent, "on input element whose type is number");
+ });
+ }
+
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ function testUserInput() {
+ // Simulating an OK click and with a file name return.
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ var input = document.getElementById('fileInput');
+ input.focus();
+
+ input.addEventListener("beforeinput", function (aEvent) {
+ ok(false, "beforeinput event shouldn't be dispatched on file input.");
+ });
+ input.addEventListener("input", function (aEvent) {
+ ok(true, "input event should've been dispatched on file input.");
+ checkIfInputIsEvent(aEvent, "on file input");
+ });
+
+ input.click();
+ SimpleTest.executeSoon(testUserInput2);
+ }
+
+ function testUserInput2() {
+ // Some generic checks for types that support the input event.
+ for (var i = 0; i < textTypes.length; ++i) {
+ input = document.getElementById("input_" + textTypes[i]);
+ input.focus();
+ expectedInputType = "insertLineBreak";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Enter");
+ is(textBeforeInput[i], 1, "beforeinput event should've been dispatched on " + textTypes[i] + " input element");
+ is(textInput[i], 0, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
+
+ expectedInputType = "insertText";
+ expectedData = "m";
+ expectedBeforeInputCancelable = true;
+ sendString("m");
+ is(textBeforeInput[i], 2, textTypes[i] + " input element should've been dispatched beforeinput event.");
+ is(textInput[i], 1, textTypes[i] + " input element should've been dispatched input event.");
+ expectedInputType = "insertLineBreak";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Enter", {shiftKey: true});
+ is(textBeforeInput[i], 3, "input event should've been dispatched on " + textTypes[i] + " input element");
+ is(textInput[i], 1, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
+
+ expectedInputType = "deleteContentBackward";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Backspace");
+ is(textBeforeInput[i], 4, textTypes[i] + " input element should've been dispatched beforeinput event.");
+ is(textInput[i], 2, textTypes[i] + " input element should've been dispatched input event.");
+ }
+
+ // Some scenarios of value changing from script and from user input.
+ input = document.getElementById("input_text");
+ input.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textBeforeInput[0], 5, "beforeinput event should've been dispatched");
+ is(textInput[0], 3, "input event should've been dispatched");
+ input.blur();
+ is(textBeforeInput[0], 5, "input event should not have been dispatched");
+ is(textInput[0], 3, "input event should not have been dispatched");
+
+ input.focus();
+ input.value = 'foo';
+ is(textBeforeInput[0], 5, "beforeinput event should not have been dispatched");
+ is(textInput[0], 3, "input event should not have been dispatched");
+ input.blur();
+ is(textBeforeInput[0], 5, "beforeinput event should not have been dispatched");
+ is(textInput[0], 3, "input event should not have been dispatched");
+
+ input.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textBeforeInput[0], 6, "beforeinput event should've been dispatched");
+ is(textInput[0], 4, "input event should've been dispatched");
+ input.value = 'bar';
+ is(textBeforeInput[0], 6, "beforeinput event should not have been dispatched");
+ is(textInput[0], 4, "input event should not have been dispatched");
+ input.blur();
+ is(textBeforeInput[0], 6, "beforeinput event should not have been dispatched");
+ is(textInput[0], 4, "input event should not have been dispatched");
+
+ // Same for textarea.
+ var textarea = document.getElementById("textarea");
+ textarea.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textareaBeforeInput, 1, "beforeinput event should've been dispatched");
+ is(textareaInput, 1, "input event should've been dispatched");
+ textarea.blur();
+ is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
+ is(textareaInput, 1, "input event should not have been dispatched");
+
+ textarea.focus();
+ textarea.value = 'foo';
+ is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
+ is(textareaInput, 1, "input event should not have been dispatched");
+ textarea.blur();
+ is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
+ is(textareaInput, 1, "input event should not have been dispatched");
+
+ textarea.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textareaBeforeInput, 2, "beforeinput event should've been dispatched");
+ is(textareaInput, 2, "input event should've been dispatched");
+ textarea.value = 'bar';
+ is(textareaBeforeInput, 2, "beforeinput event should not have been dispatched");
+ is(textareaInput, 2, "input event should not have been dispatched");
+ expectedInputType = "deleteContentBackward";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Backspace");
+ is(textareaBeforeInput, 3, "beforeinput event should've been dispatched");
+ is(textareaInput, 3, "input event should've been dispatched");
+ textarea.blur();
+ is(textareaBeforeInput, 3, "beforeinput event should not have been dispatched");
+ is(textareaInput, 3, "input event should not have been dispatched");
+
+ // Non-text input tests:
+ for (var i = 0; i < nonTextTypes.length; ++i) {
+ // Button, submit, image and reset input type tests.
+ if (i < 4) {
+ input = document.getElementById("input_" + nonTextTypes[i]);
+ input.focus();
+ input.click();
+ is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply");
+ is(nonTextInput[i], 0, "input event doesn't apply");
+ input.blur();
+ is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply");
+ is(nonTextInput[i], 0, "input event doesn't apply");
+ }
+ // For radio and checkboxes, input event should be dispatched.
+ else {
+ input = document.getElementById("input_" + nonTextTypes[i]);
+ input.focus();
+ input.click();
+ is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched");
+ is(nonTextInput[i], 1, "input event should've been dispatched");
+ input.blur();
+ is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched");
+ is(nonTextInput[i], 1, "input event should not have been dispatched");
+
+ // Test that input event is not dispatched if click event is cancelled.
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ input.addEventListener("click", preventDefault);
+ input.click();
+ is(nonTextBeforeInput[i], 0, "beforeinput event shouldn't be dispatched if click event is cancelled");
+ is(nonTextInput[i], 1, "input event shouldn't be dispatched if click event is cancelled");
+ input.removeEventListener("click", preventDefault);
+ }
+ }
+
+ // Type changes.
+ var input = document.createElement('input');
+ input.type = 'text';
+ input.value = 'foo';
+ input.onbeforeinput = function () {
+ ok(false, "we shouldn't get a beforeinput event when the type changes");
+ };
+ input.oninput = function() {
+ ok(false, "we shouldn't get an input event when the type changes");
+ };
+ input.type = 'range';
+ isnot(input.value, 'foo');
+
+ // Tests for type='range'.
+ var range = document.getElementById("input_range");
+
+ range.focus();
+ sendString("a");
+ range.blur();
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on range input " +
+ "element for key changes that don't change its value");
+ is(rangeInput, 0, "input event shouldn't be dispatched on range input " +
+ "element for key changes that don't change its value");
+
+ range.focus();
+ synthesizeKey("KEY_Home");
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched even for key changes");
+ is(rangeInput, 1, "input event should be dispatched for key changes");
+ range.blur();
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on blur");
+ is(rangeInput, 1, "input event shouldn't be dispatched on blur");
+
+ range.focus();
+ var bcr = range.getBoundingClientRect();
+ var centerOfRangeX = bcr.width / 2;
+ var centerOfRangeY = bcr.height / 2;
+ synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" });
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on mousedown if the value changes");
+ is(rangeInput, 2, "Input event should be dispatched on mousedown if the value changes");
+ synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" });
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched during a drag");
+ is(rangeInput, 3, "Input event should be dispatched during a drag");
+ synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" });
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched at the end of a drag");
+ is(rangeInput, 4, "Input event should be dispatched at the end of a drag");
+
+ // Tests for type='number'.
+ // We only test key events here since input events for mouse event changes
+ // are tested in test_input_number_mouse_events.html
+ var number = document.getElementById("input_number");
+
+ if (isDesktop) { // up/down arrow keys not supported on android
+ number.value = "";
+ number.focus();
+ // <input type="number">'s inputType value hasn't been decided, see
+ // https://github.com/w3c/input-events/issues/88
+ expectedInputType = "insertReplacementText";
+ expectedData = "1";
+ expectedBeforeInputCancelable = false;
+ synthesizeKey("KEY_ArrowUp");
+ is(numberBeforeInput, 1, "beforeinput event should be dispatched for up/down arrow key keypress");
+ is(numberInput, 1, "input event should be dispatched for up/down arrow key keypress");
+ is(number.value, "1", "sanity check value of number control after keypress");
+
+ // `data` will be the value of the input, but we can't change
+ // `expectedData` and use {repeat: 3} at the same time.
+ skipExpectedDataCheck = true;
+ synthesizeKey("KEY_ArrowDown", {repeat: 3});
+ is(numberBeforeInput, 4, "beforeinput event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
+ is(numberInput, 4, "input event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
+ is(number.value, "-2", "sanity check value of number control after multiple keydown events");
+ skipExpectedDataCheck = false;
+
+ number.blur();
+ is(numberBeforeInput, 4, "beforeinput event shouldn't be dispatched on blur");
+ is(numberInput, 4, "input event shouldn't be dispatched on blur");
+ }
+
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ document.addEventListener("DOMContentLoaded", () => {
+ init();
+ SimpleTest.waitForFocus(testUserInput);
+ }, {once: true});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_file_picker.html b/dom/html/test/forms/test_input_file_picker.html
new file mode 100644
index 0000000000..296c12bb7e
--- /dev/null
+++ b/dom/html/test/forms/test_input_file_picker.html
@@ -0,0 +1,280 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for &lt;input type='file'&gt; file picker</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 36619</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 377624</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565274">Mozilla Bug 565274</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=701353">Mozilla Bug 701353</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=826176">Mozilla Bug 826176</a>
+<p id="display"></p>
+<div id="content">
+ <input id='a' type='file' accept="image/*">
+ <input id='b' type='file' accept="audio/*">
+ <input id='c' type='file' accept="video/*">
+ <input id='d' type='file' accept="image/*, audio/* ">
+ <input id='e' type='file' accept=" image/*,video/*">
+ <input id='f' type='file' accept="audio/*,video/*">
+ <input id='g' type='file' accept="image/*, audio/* ,video/*">
+ <input id='h' type='file' accept="foo/baz,image/*,bogus/duh">
+ <input id='i' type='file' accept="mime/type;parameter,video/*">
+ <input id='j' type='file' accept="audio/*, audio/*, audio/*">
+ <input id='k' type="file" accept="image/gif,image/png">
+ <input id='l' type="file" accept="image/*,image/gif,image/png">
+ <input id='m' type="file" accept="image/gif,image/gif">
+ <input id='n' type="file" accept="">
+ <input id='o' type="file" accept=".test">
+ <input id='p' type="file" accept="image/gif,.csv">
+ <input id='q' type="file" accept="image/gif,.gif">
+ <input id='r' type="file" accept=".prefix,.prefixPlusSomething">
+ <input id='s' type="file" accept=".xls,.xlsx">
+ <input id='t' type="file" accept=".mp3,.wav,.flac">
+ <input id='u' type="file" accept=".xls, .xlsx">
+ <input id='v' type="file" accept=".xlsx, .xls">
+ <input id='w' type="file" accept=".xlsx; .xls">
+ <input id='x' type="file" accept=".xls, .xlsx">
+ <input id='y' type="file" accept=".xlsx, .xls">
+ <input id='z' type='file' accept="i/am,a,pathological,;,,,,test/case">
+ <input id='A' type="file" accept=".xlsx, .xls*">
+ <input id='mix-ref' type="file" accept="image/jpeg">
+ <input id='mix' type="file" accept="image/jpeg,.jpg">
+ <input id='hidden' hidden type='file'>
+ <input id='untrusted-click' type='file'>
+ <input id='prevent-default' type='file'>
+ <input id='prevent-default-false' type='file'>
+ <input id='right-click' type='file'>
+ <input id='middle-click' type='file'>
+ <input id='left-click' type='file'>
+ <label id='label-1'>foo<input type='file'></label>
+ <label id='label-2' for='labeled-2'>foo</label><input id='labeled-2' type='file'></label>
+ <label id='label-3'>foo<input type='file'></label>
+ <label id='label-4' for='labeled-4'>foo</label><input id='labeled-4' type='file'></label>
+ <input id='by-button' type='file'>
+ <button id='button-click' onclick="document.getElementById('by-button').click();">foo</button>
+ <button id='button-down' onclick="document.getElementById('by-button').click();">foo</button>
+ <button id='button-up' onclick="document.getElementById('by-button').click();">foo</button>
+ <div id='div-click' onclick="document.getElementById('by-button').click();" tabindex='1'>foo</div>
+ <div id='div-click-on-demand' onclick="var i=document.createElement('input'); i.type='file'; i.click();" tabindex='1'>foo</div>
+ <div id='div-keydown' onkeydown="document.getElementById('by-button').click();" tabindex='1'>foo</div>
+ <a id='link-click' href="javascript:document.getElementById('by-button').click();" tabindex='1'>foo</a>
+ <input id='show-picker' type='file'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * This test checks various scenarios and make sure that a file picker is being
+ * shown in all of them (minus a few exceptions).
+ * |testData| defines the tests to do and |launchNextTest| can be used to have
+ * specific behaviour for some tests. Everything else should just work.
+ */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+// The following lists are from toolkit/content/filepicker.properties which is used by filePicker
+var imageExtensionList = "*.jpe; *.jpg; *.jpeg; *.gif; *.png; *.bmp; *.ico; *.svg; *.svgz; *.tif; *.tiff; *.ai; *.drw; *.pct; *.psp; *.xcf; *.psd; *.raw; *.webp; *.heic"
+
+var audioExtensionList = "*.aac; *.aif; *.flac; *.iff; *.m4a; *.m4b; *.mid; *.midi; *.mp3; *.mpa; *.mpc; *.oga; *.ogg; *.opus; *.ra; *.ram; *.snd; *.wav; *.wma"
+
+var videoExtensionList = "*.avi; *.divx; *.flv; *.m4v; *.mkv; *.mov; *.mp4; *.mpeg; *.mpg; *.ogm; *.ogv; *.ogx; *.rm; *.rmvb; *.smil; *.webm; *.wmv; *.xvid"
+
+// [ element name | number of filters | extension list or filter mask | filter index ]
+var testData = [["a", 1, MockFilePicker.filterImages, 1],
+ ["b", 1, MockFilePicker.filterAudio, 1],
+ ["c", 1, MockFilePicker.filterVideo, 1],
+ ["d", 3, imageExtensionList + "; " + audioExtensionList, 1],
+ ["e", 3, imageExtensionList + "; " + videoExtensionList, 1],
+ ["f", 3, audioExtensionList + "; " + videoExtensionList, 1],
+ ["g", 4, imageExtensionList + "; " + audioExtensionList + "; " + videoExtensionList, 1],
+ ["h", 1, MockFilePicker.filterImages, 1],
+ ["i", 1, MockFilePicker.filterVideo, 1],
+ ["j", 1, MockFilePicker.filterAudio, 1],
+ ["k", 3, "*.gif; *.png", 1],
+ ["l", 4, imageExtensionList + "; " + "*.gif; *.png", 1],
+ ["m", 1, "*.gif", 1],
+ ["n", 0, undefined, 0],
+ ["o", 1, "*.test", 1],
+ ["p", 3, "*.gif; *.csv", 1],
+ ["q", 1, "*.gif", 1],
+ ["r", 3, "*.prefix; *.prefixPlusSomething", 1],
+ ["s", 3, "*.xls; *.xlsx", 1],
+ ["t", 4, "*.mp3; *.wav; *.flac", 1],
+ ["u", 3, "*.xls; *.xlsx", 1],
+ ["v", 3, "*.xlsx; *.xls", 1],
+ ["w", 0, undefined, 0],
+ ["x", 3, "*.xls; *.xlsx", 1],
+ ["y", 3, "*.xlsx; *.xls", 1],
+ ["z", 0, undefined, 0],
+ ["A", 1, "*.xlsx", 1],
+ // Note: mix and mix-ref tests extension lists are checked differently: see SimpleTest.executeSoon below
+ ["mix-ref", undefined, undefined, undefined],
+ ["mix", 1, undefined, 1],
+ ["hidden", 0, undefined, 0],
+ ["untrusted-click", 0, undefined, 0],
+ ["prevent-default", 0, undefined, 0, true],
+ ["prevent-default-false", 0, undefined, 0, true],
+ ["right-click", 0, undefined, 0, true],
+ ["middle-click", 0, undefined, 0, true],
+ ["left-click", 0, undefined, 0],
+ ["label-1", 0, undefined, 0],
+ ["label-2", 0, undefined, 0],
+ ["label-3", 0, undefined, 0],
+ ["label-4", 0, undefined, 0],
+ ["button-click", 0, undefined, 0],
+ ["button-down", 0, undefined, 0],
+ ["button-up", 0, undefined, 0],
+ ["div-click", 0, undefined, 0],
+ ["div-click-on-demand", 0, undefined, 0],
+ ["div-keydown", 0, undefined, 0],
+ ["link-click", 0, undefined, 0],
+ ["show-picker", 0, undefined, 0],
+ ];
+
+var currentTest = 0;
+var filterAllAdded;
+var filters;
+var filterIndex;
+var mixRefExtensionList;
+
+// Make sure picker works with popup blocker enabled and no allowed events
+SpecialPowers.pushPrefEnv({'set': [["dom.popup_allowed_events", ""]]}, runTests);
+
+function launchNextTest() {
+ MockFilePicker.shown = false;
+ filterAllAdded = false;
+ filters = [];
+ filterIndex = 0;
+
+ // Focusing the element will scroll them into view so making sure the clicks
+ // will work.
+ document.getElementById(testData[currentTest][0]).focus();
+
+ if (testData[currentTest][0] == "untrusted-click") {
+ var e = document.createEvent('MouseEvents');
+ e.initEvent('click', true, false);
+ document.getElementById(testData[currentTest][0]).dispatchEvent(e);
+ // All tests that should *NOT* show a file picker.
+ } else if (testData[currentTest][0] == "prevent-default" ||
+ testData[currentTest][0] == "prevent-default-false" ||
+ testData[currentTest][0] == "right-click" ||
+ testData[currentTest][0] == "middle-click") {
+ if (testData[currentTest][0] == "right-click" ||
+ testData[currentTest][0] == "middle-click") {
+ var b = testData[currentTest][0] == "middle-click" ? 1 : 2;
+ synthesizeMouseAtCenter(document.getElementById(testData[currentTest][0]),
+ { button: b });
+ } else {
+ if (testData[currentTest][0] == "prevent-default-false") {
+ document.getElementById(testData[currentTest][0]).onclick = function() {
+ return false;
+ };
+ } else {
+ document.getElementById(testData[currentTest][0]).onclick = function(event) {
+ event.preventDefault();
+ };
+ }
+ document.getElementById(testData[currentTest][0]).click();
+ }
+
+ // Wait a bit and assume we can continue. If the file picker shows later,
+ // behaviour is uncertain but that would be a random green, no big deal...
+ setTimeout(function() {
+ ok(true, "we should be there without a file picker being opened");
+ ++currentTest;
+ launchNextTest();
+ }, 500);
+ } else if (testData[currentTest][0] == 'label-3' ||
+ testData[currentTest][0] == 'label-4') {
+ synthesizeMouse(document.getElementById(testData[currentTest][0]), 5, 5, {});
+ } else if (testData[currentTest][0] == 'button-click' ||
+ testData[currentTest][0] == 'button-down' ||
+ testData[currentTest][0] == 'button-up' ||
+ testData[currentTest][0] == 'div-click' ||
+ testData[currentTest][0] == 'div-click-on-demand' ||
+ testData[currentTest][0] == 'link-click') {
+ synthesizeMouseAtCenter(document.getElementById(testData[currentTest][0]), {});
+ } else if (testData[currentTest][0] == 'div-keydown') {
+ sendString("a");
+ } else if (testData[currentTest][0] == 'show-picker') {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ document.getElementById(testData[currentTest][0]).showPicker();
+ } else {
+ document.getElementById(testData[currentTest][0]).click();
+ }
+}
+
+function runTests() {
+ MockFilePicker.appendFilterCallback = function(filepicker, title, val) {
+ filters.push(val);
+ };
+ MockFilePicker.appendFiltersCallback = function(filepicker, val) {
+ if (val === MockFilePicker.filterAll) {
+ filterAllAdded = true;
+ } else {
+ filters.push(val);
+ }
+ };
+ MockFilePicker.showCallback = function(filepicker) {
+ if (testData[currentTest][4]) {
+ ok(false, "we shouldn't have a file picker showing!");
+ return;
+ }
+
+ filterIndex = filepicker.filterIndex;
+ testName = testData[currentTest][0];
+ SimpleTest.executeSoon(function () {
+ ok(MockFilePicker.shown,
+ "File picker show method should have been called (" + testName + ")");
+ ok(filterAllAdded,
+ "filterAll is missing (" + testName + ")");
+ if (testName == "mix-ref") {
+ // Used only for reference for next test: nothing to be tested here
+ mixRefExtensionList = filters[0];
+ if (mixRefExtensionList == undefined) {
+ mixRefExtensionList = "";
+ }
+ } else {
+ if (testName == "mix") {
+ // Mixing mime type and file extension filters ("image/jpeg" and
+ // ".jpg" here) shouldn't restrict the list but only extend it, if file
+ // extension filter isn't a duplicate
+ ok(filters[0].includes(mixRefExtensionList),
+ "Mixing mime types and file extension filters shouldn't restrict extension list: " +
+ mixRefExtensionList + " | " + filters[0]);
+ ok(filters[0].includes("*.jpg"),
+ "Filter should contain '.jpg' extension. Filter was:" + filters[0]);
+ } else {
+ is(filters[0], testData[currentTest][2],
+ "Correct filters should have been added (" + testName + ")");
+ is(filters.length, testData[currentTest][1],
+ "appendFilters not called as often as expected (" + testName + ")");
+ }
+ is(filterIndex, testData[currentTest][3],
+ "File picker should show the correct filter index (" + testName + ")");
+ }
+
+ if (++currentTest == testData.length) {
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ } else {
+ launchNextTest();
+ }
+ });
+ };
+
+ launchNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_hasBeenTypePassword.html b/dom/html/test/forms/test_input_hasBeenTypePassword.html
new file mode 100644
index 0000000000..ac577ae3a9
--- /dev/null
+++ b/dom/html/test/forms/test_input_hasBeenTypePassword.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1330228
+-->
+<head>
+ <title>Test input.hasBeenTypePassword</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1330228">Mozilla Bug 1330228</a>
+<script type="application/javascript">
+
+/** Test input.hasBeenTypePassword **/
+
+var gInputTestData = [
+/* type result */
+ ["tel", false],
+ ["text", false],
+ ["button", false],
+ ["checkbox", false],
+ ["file", false],
+ ["hidden", false],
+ ["reset", false],
+ ["image", false],
+ ["radio", false],
+ ["submit", false],
+ ["search", false],
+ ["email", false],
+ ["url", false],
+ ["number", false],
+ ["range", false],
+ ["date", false],
+ ["time", false],
+ ["color", false],
+ ["month", false],
+ ["week", false],
+ ["datetime-local", false],
+ ["", false],
+ // "password" must be last since we re-use the same <input>.
+ ["password", true],
+];
+
+function checkHasBeenTypePasswordValue(aInput, aResult) {
+ is(aInput.hasBeenTypePassword, aResult,
+ "hasBeenTypePassword should return " + aResult + " for " +
+ aInput.getAttribute("type"));
+}
+
+// Use SpecialPowers since the API is ChromeOnly.
+var input = SpecialPowers.wrap(document.createElement("input"));
+// Check if the method returns the correct value on the first pass.
+for (let [type, expected] of gInputTestData) {
+ input.type = type;
+ checkHasBeenTypePasswordValue(input, expected);
+}
+
+// Now do a second pass but expect `hasBeenTypePassword` to always be true now
+// that the type was 'password'.
+for (let [type] of gInputTestData) {
+ input.type = type;
+ checkHasBeenTypePasswordValue(input, true);
+}
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html b/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html
new file mode 100644
index 0000000000..70a0f8427e
--- /dev/null
+++ b/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1330228
+-->
+<head>
+ <title>Test hasBeenTypePassword is used with bfcache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1330228">Mozilla Bug 1330228</a>
+<p id="display">
+ <iframe id="testframe" src="file_login_fields.html"></iframe>
+</p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test hasBeenTypePassword is used with bfcache **/
+SimpleTest.waitForExplicitFinish();
+
+function afterLoad() {
+ var iframeDoc = $("testframe").contentDocument;
+
+ /* change all the form controls */
+ iframeDoc.getElementById("un").value = "username";
+ iframeDoc.getElementById("pw1").value = "password1";
+
+ // Convert pw2 to a password field temporarily to test hasBeenTypePassword.
+ // We don't want the initial or final value to be type=password or we may
+ // not test the right scenario.
+ iframeDoc.getElementById("pw2").type = "password";
+ iframeDoc.getElementById("pw2").value = "password2";
+ iframeDoc.getElementById("pw2").type = "";
+
+ /* navigate the page */
+ $("testframe").setAttribute("onload", "afterNavigation()");
+ // Use a click on an <a> so that the current page is included in session history.
+ iframeDoc.getElementById("navigate").click();
+}
+
+addLoadEvent(afterLoad);
+
+function afterNavigation() {
+ info("Navigated to a new document");
+ var iframeDoc = $("testframe").contentDocument;
+ $("testframe").setAttribute("onload", "afterBack()");
+ // Calling `history.back()` on the contentWindow from here doesn't use bfcache
+ // so call it from within the contentDocument.
+ iframeDoc.getElementById("back").click();
+}
+
+function afterBack() {
+ info("Should be back showing the first document from bfcache");
+ var iframeDoc = $("testframe").contentDocument;
+
+ is(iframeDoc.getElementById("un").value, "username",
+ "username field value remembered");
+ is(iframeDoc.getElementById("pw1").value, "",
+ "type=password field value not remembered");
+ is(iframeDoc.getElementById("pw2").value, "",
+ "former type=password field value not remembered");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_list_attribute.html b/dom/html/test/forms/test_input_list_attribute.html
new file mode 100644
index 0000000000..62a07dd91a
--- /dev/null
+++ b/dom/html/test/forms/test_input_list_attribute.html
@@ -0,0 +1,253 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=556007
+-->
+<head>
+ <title>Test for Bug 556007</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556007">Mozilla Bug 556007</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 556007 **/
+
+function test0() {
+ var input = document.createElement("input");
+ ok("list" in input, "list should be an input element IDL attribute");
+}
+
+// Default .list returns null.
+function test1(aContent, aInput) {
+ return null;
+}
+
+// Regular test case.
+function test2(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aInput.setAttribute('list', 'd');
+
+ return datalist;
+}
+
+// If none of the element is in doc.
+function test3(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+
+ return null;
+}
+
+// If one of the element isn't in doc.
+function test4(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ return null;
+}
+
+// If one of the element isn't in doc.
+function test5(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(datalist);
+ aInput.setAttribute('list', 'd');
+
+ return null;
+}
+
+// If datalist is added before input.
+function test6(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(datalist);
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ return datalist;
+}
+
+// If setAttribute is set before datalist and input are in doc.
+function test7(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+
+ aContent.appendChild(datalist);
+ aContent.appendChild(aInput);
+
+ return datalist;
+}
+
+// If setAttribute is set before datalist is in doc.
+function test8(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ aContent.appendChild(datalist);
+
+ return datalist;
+}
+
+// If setAttribute is set before datalist is created.
+function test9(aContent, aInput) {
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+ aContent.appendChild(datalist);
+
+ return datalist;
+}
+
+// If another datalist is added _after_ the first one, with the same id.
+function test10(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+ var datalist2 = document.createElement("datalist");
+ datalist2.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aContent.appendChild(datalist2);
+
+ return datalist;
+}
+
+// If another datalist is added _before_ the first one with the same id.
+function test11(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+ var datalist2 = document.createElement("datalist");
+ datalist2.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aContent.insertBefore(datalist2, datalist);
+
+ return datalist2;
+}
+
+// If datalist changes it's id.
+function test12(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+
+ datalist.id = 'foo';
+
+ return null;
+}
+
+// If datalist is removed.
+function test13(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aContent.removeChild(datalist);
+
+ return null;
+}
+
+// If id contain spaces.
+function test14(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'a b c d';
+
+ aInput.setAttribute('list', 'a b c d');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+
+ return datalist;
+}
+
+// If id is the empty string.
+function test15(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = '';
+
+ aInput.setAttribute('list', '');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+
+ return null;
+}
+
+// If the id doesn't point to a datalist.
+function test16(aContent, aInput) {
+ var input = document.createElement("input");
+ input.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(input);
+
+ return null;
+}
+
+// If the first element with the id isn't a datalist.
+function test17(aContent, aInput) {
+ var input = document.createElement("input");
+ input.id = 'd';
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(input);
+ aContent.appendChild(datalist);
+
+ return null;
+}
+
+var tests = [ test1, test2, test3, test4, test5, test6, test7, test8, test9,
+ test10, test11, test12, test13, test14, test15, test16, test17 ];
+
+test0();
+
+for (var test of tests) {
+ var content = document.getElementById('content');
+
+ // Clean-up.
+ content.textContent = '';
+
+ var input = document.createElement("input");
+ var res = test(content, input);
+
+ is(input.list, res, "input.list should be " + res);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_data.js b/dom/html/test/forms/test_input_number_data.js
new file mode 100644
index 0000000000..9ec53f136f
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_data.js
@@ -0,0 +1,54 @@
+var tests = [
+ {
+ desc: "British English",
+ langTag: "en-GB",
+ inputWithGrouping: "123,456.78",
+ inputWithoutGrouping: "123456.78",
+ value: 123456.78,
+ },
+ {
+ desc: "Farsi",
+ langTag: "fa",
+ inputWithGrouping: "Û±Û²Û³Ù¬Û´ÛµÛ¶Ù«Û·Û¸",
+ inputWithoutGrouping: "Û±Û²Û³Û´ÛµÛ¶Ù«Û·Û¸",
+ value: 123456.78,
+ },
+ {
+ desc: "French",
+ langTag: "fr-FR",
+ inputWithGrouping: "123 456,78",
+ inputWithoutGrouping: "123456,78",
+ value: 123456.78,
+ },
+ {
+ desc: "German",
+ langTag: "de",
+ inputWithGrouping: "123.456,78",
+ inputWithoutGrouping: "123456,78",
+ value: 123456.78,
+ },
+ // Bug 1509057 disables grouping separators for now, so this test isn't
+ // currently relevant.
+ // Extra german test to check that a locale that uses '.' as its grouping
+ // separator doesn't result in it being invalid (due to step mismatch) due
+ // to the de-localization code mishandling numbers that look like other
+ // numbers formatted for English speakers (i.e. treating this as 123.456
+ // instead of 123456):
+ //{ desc: "German (test 2)",
+ // langTag: "de", inputWithGrouping: "123.456",
+ // inputWithoutGrouping: "123456", value: 123456
+ //},
+ {
+ desc: "Hebrew",
+ langTag: "he",
+ inputWithGrouping: "123,456.78",
+ inputWithoutGrouping: "123456.78",
+ value: 123456.78,
+ },
+];
+
+var invalidTests = [
+ // Right now this will pass in a 'de' build, but not in the 'en' build that
+ // are used for testing. See bug 1216831.
+ // { desc: "Invalid German", langTag: "de", input: "12.34" }
+];
diff --git a/dom/html/test/forms/test_input_number_focus.html b/dom/html/test/forms/test_input_number_focus.html
new file mode 100644
index 0000000000..4126ecc496
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_focus.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1268556
+-->
+<head>
+ <title>Test focus behaviour for &lt;input type='number'&gt;</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #input_test_style_display {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268556">Mozilla Bug 1268556</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1057858">Mozilla Bug 1057858</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input_test_redirect" type="number">
+ <input id="input_test_style_display" type="number" >
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1268556.
+ * This test checks that when focusing on an input type=number, the focus is
+ * redirected to the anonymous text control, but the document.activeElement
+ * still returns the <input type=number>.
+ *
+ * Tests for bug 1057858.
+ * Checks that adding an element and immediately focusing it triggers exactly
+ * one "focus" event and no "blur" events. The same for switching
+ * `style.display` from `none` to `block`.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test_focus_redirects_to_text_control_but_not_for_activeElement();
+ test_add_element_and_focus_check_one_focus_event();
+ test_style_display_none_change_to_block_check_one_focus_event();
+ SimpleTest.finish();
+});
+
+function test_focus_redirects_to_text_control_but_not_for_activeElement() {
+ document.activeElement.blur();
+ var number = document.getElementById("input_test_redirect");
+ number.focus();
+
+ // The active element returns the input type=number.
+ var activeElement = document.activeElement;
+ is (activeElement, number, "activeElement should be the number element");
+ is (activeElement.localName, "input", "activeElement should be an input element");
+ is (activeElement.getAttribute("type"), "number", "activeElement should of type number");
+
+ // Use FocusManager to check that the actual focus is on the anonymous
+ // text control.
+ var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
+ .getService(SpecialPowers.Ci.nsIFocusManager);
+ var focusedElement = fm.focusedElement;
+ is (focusedElement.localName, "input", "focusedElement should be an input element");
+ is (focusedElement.getAttribute("type"), "number", "focusedElement should of type number");
+}
+
+var blurEventCounter = 0;
+var focusEventCounter = 0;
+
+function append_input_element_with_event_listeners_to_dom() {
+ var inputElement = document.createElement("input");
+ inputElement.type = "number";
+ inputElement.addEventListener("blur", function() { ++blurEventCounter; });
+ inputElement.addEventListener("focus", function() { ++focusEventCounter; });
+ var content = document.getElementById("content");
+ content.appendChild(inputElement);
+ return inputElement;
+}
+
+function test_add_element_and_focus_check_one_focus_event() {
+ document.activeElement.blur();
+ var inputElement = append_input_element_with_event_listeners_to_dom();
+
+ blurEventCounter = 0;
+ focusEventCounter = 0;
+ inputElement.focus();
+
+ is(blurEventCounter, 0, "After focus: no blur events observed.");
+ is(focusEventCounter, 1, "After focus: exactly one focus event observed.");
+}
+
+function test_style_display_none_change_to_block_check_one_focus_event() {
+ document.activeElement.blur();
+ var inputElement = document.getElementById("input_test_style_display");
+ inputElement.addEventListener("blur", function() { ++blurEventCounter; });
+ inputElement.addEventListener("focus", function() { ++focusEventCounter; });
+
+ blurEventCounter = 0;
+ focusEventCounter = 0;
+ inputElement.style.display = "block";
+ inputElement.focus();
+
+ is(blurEventCounter, 0, "After focus: no blur events observed.");
+ is(focusEventCounter, 1, "After focus: exactly one focus event observed.");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_key_events.html b/dom/html/test/forms/test_input_number_key_events.html
new file mode 100644
index 0000000000..eb537f5617
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_key_events.html
@@ -0,0 +1,238 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935506
+-->
+<head>
+ <title>Test key events for number control</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935506">Mozilla Bug 935506</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 935506
+ * This test checks how the value of <input type=number> changes in response to
+ * key events while it is in various states.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+const defaultMinimum = "NaN";
+const defaultMaximum = "NaN";
+const defaultStep = 1;
+
+// Helpers:
+// For the sake of simplicity, we do not currently support fractional value,
+// step, etc.
+
+function getMinimum(element) {
+ return Number(element.min || defaultMinimum);
+}
+
+function getMaximum(element) {
+ return Number(element.max || defaultMaximum);
+}
+
+function getDefaultValue(element) {
+ return 0;
+}
+
+function getValue(element) {
+ return Number(element.value || getDefaultValue(element));
+}
+
+function getStep(element) {
+ if (element.step == "any") {
+ return "any";
+ }
+ var step = Number(element.step || defaultStep);
+ return step <= 0 ? defaultStep : step;
+}
+
+function getStepBase(element) {
+ return Number(element.getAttribute("min") || "NaN") ||
+ Number(element.getAttribute("value") || "NaN") || 0;
+}
+
+function hasStepMismatch(element) {
+ var value = element.value;
+ if (value == "") {
+ value = 0;
+ }
+ var step = getStep(element);
+ if (step == "any") {
+ return false;
+ }
+ return ((value - getStepBase(element)) % step) != 0;
+}
+
+function floorModulo(x, y) {
+ return (x - y * Math.floor(x / y));
+}
+
+function expectedValueAfterStepUpOrDown(stepFactor, element) {
+ var value = getValue(element);
+ if (isNaN(value)) {
+ value = 0;
+ }
+ var step = getStep(element);
+ if (step == "any") {
+ step = 1;
+ }
+
+ var minimum = getMinimum(element);
+ var maximum = getMaximum(element);
+ if (!isNaN(maximum)) {
+ // "max - (max - stepBase) % step" is the nearest valid value to max.
+ maximum = maximum - floorModulo(maximum - getStepBase(element), step);
+ }
+
+ // Cases where we are clearly going in the wrong way.
+ // We don't use ValidityState because we can be higher than the maximal
+ // allowed value and still not suffer from range overflow in the case of
+ // of the value specified in @max isn't in the step.
+ if ((value <= minimum && stepFactor < 0) ||
+ (value >= maximum && stepFactor > 0)) {
+ return value;
+ }
+
+ if (hasStepMismatch(element) &&
+ value != minimum && value != maximum) {
+ if (stepFactor > 0) {
+ value -= floorModulo(value - getStepBase(element), step);
+ } else if (stepFactor < 0) {
+ value -= floorModulo(value - getStepBase(element), step);
+ value += step;
+ }
+ }
+
+ value += step * stepFactor;
+
+ // When stepUp() is called and the value is below minimum, we should clamp on
+ // minimum unless stepUp() moves us higher than minimum.
+ if (element.validity.rangeUnderflow && stepFactor > 0 &&
+ value <= minimum) {
+ value = minimum;
+ } else if (element.validity.rangeOverflow && stepFactor < 0 &&
+ value >= maximum) {
+ value = maximum;
+ } else if (stepFactor < 0 && !isNaN(minimum)) {
+ value = Math.max(value, minimum);
+ } else if (stepFactor > 0 && !isNaN(maximum)) {
+ value = Math.min(value, maximum);
+ }
+
+ return value;
+}
+
+function expectedValAfterKeyEvent(key, element) {
+ return expectedValueAfterStepUpOrDown(key == "KEY_ArrowUp" ? 1 : -1, element);
+}
+
+function test() {
+ var elem = document.getElementById("input");
+ elem.focus();
+
+ elem.min = -5;
+ elem.max = 5;
+ elem.step = 2;
+ var defaultValue = 0;
+ var oldVal, expectedVal;
+
+ for (key of ["KEY_ArrowUp", "KEY_ArrowDown"]) {
+ // Start at middle:
+ oldVal = elem.value = -1;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set between min/max (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Start at maximum:
+ oldVal = elem.value = elem.max;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the maximum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Start at minimum:
+ oldVal = elem.value = elem.min;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the minimum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Test preventDefault():
+ elem.addEventListener("keydown", evt => evt.preventDefault(), {once: true});
+ oldVal = elem.value = 0;
+ expectedVal = 0;
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control where scripted preventDefault() should prevent the value changing");
+
+ // Test step="any" behavior:
+ var oldStep = elem.step;
+ elem.step = "any";
+ oldVal = elem.value = 0;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the midpoint and step='any' (" + oldVal + ")");
+ elem.step = oldStep; // restore
+
+ // Test that invalid input blocks UI initiated stepping:
+ oldVal = elem.value = "";
+ elem.select();
+ sendString("abc");
+ synthesizeKey(key);
+ is(elem.value, "", "Test " + key + " does nothing when the input is invalid");
+
+ // Test that no value does not block UI initiated stepping:
+ oldVal = elem.value = "";
+ elem.setAttribute("required", "required");
+ elem.select();
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the empty string and with the 'required' attribute set");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Reset 'required' attribute:
+ elem.removeAttribute("required");
+ }
+
+ // Test that key events are correctly dispatched
+ elem.max = "";
+ elem.value = "";
+ sendString("7837281");
+ is(elem.value, "7837281", "Test keypress event dispatch for number control");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_l10n.html b/dom/html/test/forms/test_input_number_l10n.html
new file mode 100644
index 0000000000..c8202028ed
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_l10n.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=844744
+-->
+<head>
+ <title>Test localization of number control input</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="test_input_number_data.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844744">Mozilla Bug 844744</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number" step="any">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 844744
+ * This test checks that localized input that is typed into <input type=number>
+ * is correctly handled.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ startTests();
+ SimpleTest.finish();
+});
+
+var elem;
+
+function runTest(test) {
+ elem.lang = test.langTag;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.inputWithGrouping);
+ is(elem.value, "", "Test " + test.desc + " ('" + test.langTag +
+ "') localization with grouping separator");
+ elem.value = 0;
+ elem.select();
+ sendString(test.inputWithoutGrouping);
+ is(elem.valueAsNumber, test.value, "Test " + test.desc + " ('" + test.langTag +
+ "') localization without grouping separator");
+ is(elem.value, String(test.value), "Test " + test.desc + " ('" + test.langTag +
+ "') localization without grouping separator as string");
+}
+
+function runInvalidInputTest(test) {
+ elem.lang = test.langTag;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.input);
+ is(elem.value, "", "Test " + test.desc + " ('" + test.langTag +
+ "') with invalid input: " + test.input);
+}
+
+function startTests() {
+ elem = document.getElementById("input");
+ for (var test of tests) {
+ runTest(test, elem);
+ }
+ for (var test of invalidTests) {
+ runInvalidInputTest(test, elem);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_mouse_events.html b/dom/html/test/forms/test_input_number_mouse_events.html
new file mode 100644
index 0000000000..a3e5732beb
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_mouse_events.html
@@ -0,0 +1,272 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935501
+-->
+<head>
+ <title>Test mouse events for number</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"/>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ input {
+ margin: 0;
+ border: 0;
+ padding: 0;
+ width: 200px;
+ box-sizing: border-box;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935501">Mozilla Bug 935501</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number">
+</div>
+<pre id="test">
+<script>
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/**
+ * Test for Bug 935501
+ * This test checks how the value of <input type=number> changes in response to
+ * various mouse events.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+SimpleTest.waitForFocus(function() {
+ test();
+});
+
+const kIsWin = AppConstants.platform == "win";
+const kIsLinux = AppConstants.platform == "linux";
+
+var input = document.getElementById("input");
+var inputRect = input.getBoundingClientRect();
+
+// Points over the input's spin-up and spin-down buttons (as offsets from the
+// top-left of the input's bounding client rect):
+const SPIN_UP_X = inputRect.width - 3;
+const SPIN_UP_Y = 3;
+const SPIN_DOWN_X = inputRect.width - 3;
+const SPIN_DOWN_Y = inputRect.height - 3;
+
+function checkInputEvent(aEvent, aDescription) {
+ // Probably, key operation should fire "input" event with InputEvent interface.
+ // See https://github.com/w3c/input-events/issues/88
+ ok(aEvent instanceof InputEvent, `"input" event should be dispatched with InputEvent interface on input element whose type is number ${aDescription}`);
+ is(aEvent.cancelable, false, `"input" event should be never cancelable on input element whose type is number ${aDescription}`);
+ is(aEvent.bubbles, true, `"input" event should always bubble on input element whose type is number ${aDescription}`);
+ info(`Data: ${aEvent.data}, value: ${aEvent.target.value}`);
+}
+
+function test() {
+ input.value = 0;
+
+ // Test click on spin-up button:
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ is(input.value, "1", "Test step-up on mousedown on spin-up button");
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ is(input.value, "1", "Test mouseup on spin-up button");
+
+ // Test click on spin-down button:
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ is(input.value, "0", "Test step-down on mousedown on spin-down button");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ is(input.value, "0", "Test mouseup on spin-down button");
+
+ // Test clicks with modifiers that mean we should ignore the click:
+ var modifiersIgnore = ["altGrKey", "fnKey"];
+ if (kIsWin || kIsLinux) {
+ modifiersIgnore.push("metaKey");
+ }
+ for (var modifier of modifiersIgnore) {
+ input.value = 0;
+ var eventParams = { type: "mousedown" };
+ eventParams[modifier] = true;
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, eventParams);
+ is(input.value, "0", "We should ignore mousedown on spin-up button with modifier " + modifier);
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ }
+
+ // Test clicks with modifiers that mean we should allow the click:
+ var modifiersAllow = ["shiftKey", "ctrlKey", "altKey"];
+ if (!modifiersIgnore.includes("metaKey")) {
+ modifiersAllow.push("metaKey");
+ }
+ for (var modifier of modifiersAllow) {
+ input.value = 0;
+ var eventParams = { type: "mousedown" };
+ eventParams[modifier] = true;
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, eventParams);
+ is(input.value, "1", "We should allow mousedown on spin-up button with modifier " + modifier);
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ }
+
+ // Test step="any" behavior:
+ input.value = 0;
+ var oldStep = input.step;
+ input.step = "any";
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ is(input.value, "1", "Test step-up on mousedown on spin-up button with step='any'");
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ is(input.value, "1", "Test mouseup on spin-up button with step='any'");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ is(input.value, "0", "Test step-down on mousedown on spin-down button with step='any'");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ is(input.value, "0", "Test mouseup on spin-down button with step='any'");
+ input.step = oldStep; // restore
+
+ // Test that preventDefault() works:
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ input.value = 1;
+ input.addEventListener("mousedown", preventDefault);
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, {});
+ is(input.value, "1", "Test that preventDefault() works for click on spin-up button");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, {});
+ is(input.value, "1", "Test that preventDefault() works for click on spin-down button");
+ input.removeEventListener("mousedown", preventDefault);
+
+ // Test for bug 1707070.
+ input.style.paddingRight = "30px";
+ input.getBoundingClientRect(); // flush layout
+
+ input.value = 0;
+ synthesizeMouse(input, SPIN_UP_X - 30, SPIN_UP_Y, { type: "mousedown" });
+ is(input.value, "1", "Spinner down works on with padding (mousedown)");
+ synthesizeMouse(input, SPIN_UP_X - 30, SPIN_UP_Y, { type: "mouseup" });
+ is(input.value, "1", "Spinner down works with padding (mouseup)");
+
+ synthesizeMouse(input, SPIN_DOWN_X - 30, SPIN_DOWN_Y, { type: "mousedown" });
+ is(input.value, "0", "Spinner works with padding (mousedown)");
+ synthesizeMouse(input, SPIN_DOWN_X - 30, SPIN_DOWN_Y, { type: "mouseup" });
+ is(input.value, "0", "Spinner works with padding (mouseup)");
+
+ input.style.paddingRight = "";
+ input.getBoundingClientRect(); // flush layout
+
+ // Run the spin tests:
+ runNextSpinTest();
+}
+
+function runNextSpinTest() {
+ var nextTest = spinTests.shift();
+ if (!nextTest) {
+ SimpleTest.finish();
+ return;
+ }
+ nextTest();
+}
+
+function waitForTick() {
+ return new Promise(SimpleTest.executeSoon);
+}
+
+const SETTIMEOUT_DELAY = 500;
+
+var spinTests = [
+ // Test spining when the mouse button is kept depressed on the spin-up
+ // button, then moved over the spin-down button:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", async function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#1");
+ if (inputEventCount == 3) {
+ is(input.value, "3", "Testing spin-up button");
+ await waitForTick();
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousemove" });
+ } else if (inputEventCount == 6) {
+ is(input.value, "0", "Testing spin direction is reversed after mouse moves from spin-up button to spin-down button");
+ input.removeEventListener("input", arguments.callee);
+ await waitForTick();
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ runNextSpinTest();
+ }
+ });
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ },
+
+ // Test spining when the mouse button is kept depressed on the spin-down
+ // button, then moved over the spin-up button:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", async function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#2");
+ if (inputEventCount == 3) {
+ is(input.value, "-3", "Testing spin-down button");
+ await waitForTick();
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousemove" });
+ } else if (inputEventCount == 6) {
+ is(input.value, "0", "Testing spin direction is reversed after mouse moves from spin-down button to spin-up button");
+ input.removeEventListener("input", arguments.callee);
+ await waitForTick();
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ runNextSpinTest();
+ }
+ });
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ },
+
+ // Test that the spin is stopped when the mouse button is depressod on the
+ // spin-up button, then moved outside both buttons once the spin starts:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", async function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#3");
+ if (inputEventCount == 3) {
+ await waitForTick();
+ synthesizeMouse(input, -1, -1, { type: "mousemove" });
+ var eventHandler = arguments.callee;
+ setTimeout(function() {
+ is(input.value, "3", "Testing moving the mouse outside the spin buttons stops the spin");
+ is(inputEventCount, 3, "Testing moving the mouse outside the spin buttons stops the spin input events");
+ input.removeEventListener("input", eventHandler);
+ synthesizeMouse(input, -1, -1, { type: "mouseup" });
+ runNextSpinTest();
+ }, SETTIMEOUT_DELAY);
+ }
+ });
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ },
+
+ // Test that changing the input type in the middle of a spin cancels the spin:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#4");
+ if (inputEventCount == 3) {
+ input.type = "text"
+ var eventHandler = arguments.callee;
+ setTimeout(function() {
+ is(input.value, "-3", "Testing changing input type during a spin stops the spin");
+ is(inputEventCount, 3, "Testing changing input type during a spin stops the spin input events");
+ input.removeEventListener("input", eventHandler);
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ input.type = "number"; // restore
+ runNextSpinTest();
+ }, SETTIMEOUT_DELAY);
+ }
+ });
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ }
+];
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_placeholder_shown.html b/dom/html/test/forms/test_input_number_placeholder_shown.html
new file mode 100644
index 0000000000..c9c2a7f515
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_placeholder_shown.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>Test for :placeholder-shown on input elements and invalid values.</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<style>
+input {
+ border: 1px solid purple;
+}
+input:placeholder-shown {
+ border-color: blue;
+}
+</style>
+<input type="number" placeholder="foo">
+<script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+ });
+
+ function test() {
+ let input = document.querySelector('input');
+ input.focus();
+ is(getComputedStyle(input).borderLeftColor, "rgb(0, 0, 255)",
+ ":placeholder-shown should apply")
+ sendString("x");
+ isnot(getComputedStyle(input).borderLeftColor, "rgb(0, 0, 255)",
+ ":placeholder-shown should not apply, even though the value is invalid")
+ }
+</script>
diff --git a/dom/html/test/forms/test_input_number_rounding.html b/dom/html/test/forms/test_input_number_rounding.html
new file mode 100644
index 0000000000..d162727557
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_rounding.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=783607
+-->
+<head>
+ <title>Test rounding behaviour for &lt;input type='number'&gt;</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=783607">Mozilla Bug 783607</a>
+<p id="display"></p>
+<div id="content">
+ <input id=number type=number value=0 step=0.01 max=1>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 783607.
+ * This test checks that when <input type=number> has fractional step values,
+ * the values that a content author will see in their script will not have
+ * ugly rounding errors.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+/**
+ * We can _NOT_ generate these values by looping and simply incrementing a
+ * variable by 0.01 and stringifying it, since we'll end up with strings like
+ * "0.060000000000000005" due to the inability of binary floating point numbers
+ * to accurately represent decimal values.
+ */
+var stepVals = [
+ "0", "0.01", "0.02", "0.03", "0.04", "0.05", "0.06", "0.07", "0.08", "0.09",
+ "0.1", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "0.19",
+ "0.2", "0.21", "0.22", "0.23", "0.24", "0.25", "0.26", "0.27", "0.28", "0.29",
+ "0.3", "0.31", "0.32", "0.33", "0.34", "0.35", "0.36", "0.37", "0.38", "0.39",
+ "0.4", "0.41", "0.42", "0.43", "0.44", "0.45", "0.46", "0.47", "0.48", "0.49",
+ "0.5", "0.51", "0.52", "0.53", "0.54", "0.55", "0.56", "0.57", "0.58", "0.59",
+ "0.6", "0.61", "0.62", "0.63", "0.64", "0.65", "0.66", "0.67", "0.68", "0.69",
+ "0.7", "0.71", "0.72", "0.73", "0.74", "0.75", "0.76", "0.77", "0.78", "0.79",
+ "0.8", "0.81", "0.82", "0.83", "0.84", "0.85", "0.86", "0.87", "0.88", "0.89",
+ "0.9", "0.91", "0.92", "0.93", "0.94", "0.95", "0.96", "0.97", "0.98", "0.99",
+ "1"
+];
+
+var pgUpDnVals = [
+ "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1"
+];
+
+function test() {
+ var elem = document.getElementById("number");
+
+ elem.focus();
+
+ /**
+ * TODO:
+ * When <input type='number'> widget will have a widge we should test PAGE_UP,
+ * PAGE_DOWN, UP and DOWN keys. For the moment, there is no widget so those
+ * keys do not have any effect.
+ * The tests using those keys as marked as todo_is() hoping that at least part
+ * of them will fail when the widget will be implemented.
+ */
+
+/* No other implementations implement this, so we don't either, for now.
+ Seems like it might be nice though.
+
+ for (var i = 1; i < pgUpDnVals.length; ++i) {
+ synthesizeKey("KEY_PageUp");
+ todo_is(elem.value, pgUpDnVals[i], "Test KEY_PageUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+
+ for (var i = pgUpDnVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_PageDown");
+ // TODO: this condition is there because the todo_is() below would pass otherwise.
+ if (stepVals[i] == 0) { continue; }
+ todo_is(elem.value, pgUpDnVals[i], "Test KEY_PageDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+*/
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ synthesizeKey("KEY_ArrowUp");
+ is(elem.value, stepVals[i], "Test KEY_ArrowUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_ArrowDown");
+ // TODO: this condition is there because the todo_is() below would pass otherwise.
+ if (stepVals[i] == 0) { continue; }
+ is(elem.value, stepVals[i], "Test KEY_ArrowDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ elem.stepUp();
+ is(elem.value, stepVals[i], "Test stepUp()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ elem.stepDown();
+ is(elem.value, stepVals[i], "Test stepDown()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_validation.html b/dom/html/test/forms/test_input_number_validation.html
new file mode 100644
index 0000000000..c19c1fde1c
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_validation.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=827161
+-->
+<head>
+ <title>Test validation of number control input</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="test_input_number_data.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=827161">Mozilla Bug 827161</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number" step="0.01" oninvalid="invalidEventHandler(event);">
+ <input id="requiredinput" type="number" step="0.01" required
+ oninvalid="invalidEventHandler(event);">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 827161.
+ * This test checks that validation works correctly for <input type=number>.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ startTests();
+ SimpleTest.finish();
+});
+
+var elem;
+
+function runTest(test) {
+ elem.lang = test.langTag;
+
+ gInvalid = false; // reset
+ var desc = `${test.desc} (lang='${test.langTag}', id='${elem.id}')`;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.inputWithGrouping);
+ checkIsInvalid(elem, `${desc} with grouping separator`);
+ sendChar("a");
+ checkIsInvalid(elem, `${desc} with grouping separator`);
+
+ gInvalid = false; // reset
+ elem.value = 0;
+ elem.select();
+ sendString(test.inputWithoutGrouping);
+ checkIsValid(elem, `${desc} without grouping separator`);
+ sendChar("a");
+ checkIsInvalid(elem, `${desc} without grouping separator`);
+}
+
+function runInvalidInputTest(test) {
+ elem.lang = test.langTag;
+
+ gInvalid = false; // reset
+ var desc = `${test.desc} (lang='${test.langTag}', id='${elem.id}')`;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.input);
+ checkIsInvalid(elem, `${desc} with invalid input ${test.input}`);
+}
+
+function startTests() {
+ elem = document.getElementById("input");
+ for (var test of tests) {
+ runTest(test);
+ }
+ for (var test of invalidTests) {
+ runInvalidInputTest(test);
+ }
+ elem = document.getElementById("requiredinput");
+ for (var test of tests) {
+ runTest(test);
+ }
+
+ gInvalid = false; // reset
+ elem.value = "";
+ checkIsInvalidEmptyValue(elem, "empty value");
+}
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be 'invalid'");
+ gInvalid = true;
+}
+
+function checkIsValid(element, infoStr)
+{
+ ok(!element.validity.badInput,
+ "Element should not suffer from bad input for " + infoStr);
+ ok(element.validity.valid, "Element should be valid for " + infoStr);
+ ok(element.checkValidity(), "checkValidity() should return true for " + infoStr);
+ ok(!gInvalid, "The invalid event should not have been thrown for " + infoStr);
+ is(element.validationMessage, '',
+ "Validation message should be the empty string for " + infoStr);
+ ok(element.matches(":valid"), ":valid pseudo-class should apply for " + infoStr);
+}
+
+function checkIsInvalid(element, infoStr)
+{
+ ok(element.validity.badInput,
+ "Element should suffer from bad input for " + infoStr);
+ ok(!element.validity.valid, "Element should not be valid for " + infoStr);
+ ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr);
+ ok(gInvalid, "The invalid event should have been thrown for " + infoStr);
+ is(element.validationMessage, "Please enter a number.",
+ "Validation message is not the expected message for " + infoStr);
+ ok(element.matches(":invalid"), ":invalid pseudo-class should apply for " + infoStr);
+}
+
+function checkIsInvalidEmptyValue(element, infoStr)
+{
+ ok(!element.validity.badInput,
+ "Element should not suffer from bad input for " + infoStr);
+ ok(element.validity.valueMissing,
+ "Element should suffer from value missing for " + infoStr);
+ ok(!element.validity.valid, "Element should not be valid for " + infoStr);
+ ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr);
+ ok(gInvalid, "The invalid event should have been thrown for " + infoStr);
+ is(element.validationMessage, "Please enter a number.",
+ "Validation message is not the expected message for " + infoStr);
+ ok(element.matches(":invalid"), ":invalid pseudo-class should apply for " + infoStr);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_password_click_show_password_button.html b/dom/html/test/forms/test_input_password_click_show_password_button.html
new file mode 100644
index 0000000000..76f4e066f5
--- /dev/null
+++ b/dom/html/test/forms/test_input_password_click_show_password_button.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=502258
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 502258</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script>
+
+ SimpleTest.waitForExplicitFinish();
+
+ async function click_show_password(aId) {
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+ var element = document.getElementById(aId);
+ element.focus();
+ await new Promise(resolve => setTimeout(resolve, 0));
+ var rect = element.getBoundingClientRect();
+ var x = rect.right - 8;
+ var y = rect.top + 8;
+ wu.sendMouseEvent("mousedown", x, y, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", x, y, 0, 1, 0);
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ }
+
+ async function test_show_password(aId) {
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+ var element = document.getElementById(aId);
+
+ var baseSnapshot = await snapshotWindow(window);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ element.type = "text";
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ var typeTextSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(baseSnapshot, typeTextSnapshot, true);
+ ok(results[0], aId + ": type=text should render the same as type=password that is showing the password");
+
+ // Re-setting value shouldn't change anything.
+ // eslint-disable-next-line no-self-assign
+ element.value = element.value;
+ var tmpSnapshot = await snapshotWindow(window);
+
+ results = compareSnapshots(baseSnapshot, tmpSnapshot, true);
+ ok(results[0], aId + ": re-setting the value should change nothing");
+ }
+
+ async function reset_show_password(aId, concealedSnapshot) {
+ var element = document.getElementById(aId);
+ element.type = "password";
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ var typePasswordSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(concealedSnapshot, typePasswordSnapshot, true);
+ ok(results[0], aId + ": changing the type attribute should conceal the password again");
+ }
+
+ async function runTest() {
+ await SpecialPowers.pushPrefEnv({set: [["layout.forms.reveal-password-button.enabled", true]]});
+ document.getElementById("content").style.display = "";
+ document.getElementById("content").getBoundingClientRect();
+ var concealedSnapshot = await snapshotWindow(window);
+ // test1 checks that the Show Password button becomes invisible when the value becomes empty
+ document.getElementById('test1').value = "123";
+ await click_show_password('test1');
+ document.getElementById('test1').value = "";
+ // test2 checks that clicking the Show Password button unmasks the value
+ await click_show_password('test2');
+ await test_show_password('test1');
+ await test_show_password('test2');
+ // checks that changing the type attribute resets thhe revealed state
+ await reset_show_password('test2', concealedSnapshot);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(runTest);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502258">Mozilla Bug 502258</a>
+<p id="display"></p>
+<style>input {appearance:none} .ref {display:none}</style>
+<div id="content" style="display: none">
+ <input id="test1" type=password>
+ <div style="position:relative; margin: 1em 0;">
+ <input id="test2" type=password value="123" style="position:absolute">
+ <div style="position:absolute; top:0;left:10ch; width:20ch; height:2em; background:black; pointer-events:none"></div>
+ </div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_password_show_password_button.html b/dom/html/test/forms/test_input_password_show_password_button.html
new file mode 100644
index 0000000000..09bec8ae82
--- /dev/null
+++ b/dom/html/test/forms/test_input_password_show_password_button.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=502258
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 502258</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ async function test_append_char(aId) {
+ let element = document.getElementById(aId);
+ element.focus();
+
+ let baseSnapshot = await snapshotWindow(window);
+
+ element.selectionStart = element.selectionEnd = element.value.length;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ sendString('f');
+
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+
+ let selectionAtTheEndSnapshot = await snapshotWindow(window);
+ assertSnapshots(baseSnapshot, selectionAtTheEndSnapshot, /* equal = */ false, /* fuzz = */ null, "baseSnapshot", "selectionAtTheEndSnapshot");
+
+ // Re-setting value shouldn't change anything.
+ // eslint-disable-next-line no-self-assign
+ element.value = element.value;
+ let tmpSnapshot = await snapshotWindow(window);
+
+ assertSnapshots(baseSnapshot, tmpSnapshot, /* equal = */ false, /* fuzz = */ null, "baseSnapshot", "tmpSnapshot");
+ assertSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, /* equal = */ true, /* fuzz = */ null, "selectionAtTheEndSnapshot", "tmpSnapshot");
+
+ element.selectionStart = element.selectionEnd = 0;
+ element.blur();
+ }
+
+ async function runTest() {
+ await SpecialPowers.pushPrefEnv({set: [["layout.forms.reveal-password-button.enabled", true]]});
+ document.getElementById("content").style.display = "";
+ document.getElementById("content").getBoundingClientRect();
+ await test_append_char('test1');
+ await test_append_char('test2');
+ await test_append_char('test3');
+ await test_append_char('test4');
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(runTest);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502258">Mozilla Bug 502258</a>
+<p id="display"></p>
+<style>input {appearance:none}</style>
+<div id="content" style="display: none">
+ <input id="test1" type=password>
+ <input id="test2" type=password value="123">
+ <!-- text value masked off -->
+ <div style="position:relative; margin: 1em 0;">
+ <input id="test3" type=password style="position:absolute">
+ <div style="position:absolute; top:0;left:0; width:10ch; height:2em; background:black"></div>
+ </div>
+ <br>
+ <!-- Show Password button masked off -->
+ <div style="position:relative; margin: 1em 0;">
+ <input id="test4" type=password style="position:absolute">
+ <div style="position:absolute; top:0;left:10ch; width:20ch; height:2em; background:black"></div>
+ </div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_radio_indeterminate.html b/dom/html/test/forms/test_input_radio_indeterminate.html
new file mode 100644
index 0000000000..0fe7028b1e
--- /dev/null
+++ b/dom/html/test/forms/test_input_radio_indeterminate.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885359
+-->
+<head>
+ <title>Test for Bug 885359</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885359">Mozilla Bug 343444</a>
+<p id="display"></p>
+<form>
+ <input type="radio" id='radio1'/><br/>
+
+ <input type="radio" id="g1radio1" name="group1"/>
+ <input type="radio" id="g1radio2" name="group1"/></br>
+ <input type="radio" id="g1radio3" name="group1"/></br>
+
+ <input type="radio" id="g2radio1" name="group2"/>
+ <input type="radio" id="g2radio2" name="group2" checked/></br>
+</form>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var radio1 = document.getElementById("radio1");
+var g1radio1 = document.getElementById("g1radio1");
+var g1radio2 = document.getElementById("g1radio2");
+var g1radio3 = document.getElementById("g1radio3");
+var g2radio1 = document.getElementById("g2radio1");
+var g2radio2 = document.getElementById("g2radio2");
+
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function verifyIndeterminateState(aElement, aIsIndeterminate, aMessage) {
+ is(aElement.mozMatchesSelector(':indeterminate'), aIsIndeterminate, aMessage);
+}
+
+function test() {
+ // Initial State.
+ verifyIndeterminateState(radio1, true,
+ "Unchecked radio in its own group (no name attribute)");
+ verifyIndeterminateState(g1radio1, true, "No selected radio in its group");
+ verifyIndeterminateState(g1radio2, true, "No selected radio in its group");
+ verifyIndeterminateState(g1radio3, true, "No selected radio in its group");
+ verifyIndeterminateState(g2radio1, false, "Selected radio in its group");
+ verifyIndeterminateState(g2radio2, false, "Selected radio in its group");
+
+ // Selecting radio buttion.
+ g1radio1.checked = true;
+ verifyIndeterminateState(g1radio1, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio2, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio3, false,
+ "Selecting a radio should affect all radios in the group");
+
+ // Changing the selected radio button.
+ g1radio3.checked = true;
+ verifyIndeterminateState(g1radio1, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio2, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio3, false,
+ "Selecting a radio should affect all radios in the group");
+
+ // Deselecting radio button.
+ g2radio2.checked = false;
+ verifyIndeterminateState(g2radio1, true,
+ "Deselecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g2radio2, true,
+ "Deselecting a radio should affect all radios in the group");
+
+ // Move a selected radio button to another group.
+ g1radio3.name = "group2";
+
+ // The radios' state in the original group becomes indeterminated.
+ verifyIndeterminateState(g1radio1, true,
+ "Removing a radio from a group should affect all radios in the group");
+ verifyIndeterminateState(g1radio2, true,
+ "Removing a radio from a group should affect all radios in the group");
+
+ // The radios' state in the new group becomes determinated.
+ verifyIndeterminateState(g1radio3, false,
+ "Adding a radio from a group should affect all radios in the group");
+ verifyIndeterminateState(g2radio1, false,
+ "Adding a radio from a group should affect all radios in the group");
+ verifyIndeterminateState(g2radio2, false,
+ "Adding a radio from a group should affect all radios in the group");
+
+ // Change input type to 'text'.
+ g1radio3.type = "text";
+ verifyIndeterminateState(g1radio3, false,
+ "Input type text does not have an indeterminate state");
+ verifyIndeterminateState(g2radio1, true,
+ "Changing input type should affect all radios in the group");
+ verifyIndeterminateState(g2radio2, true,
+ "Changing input type should affect all radios in the group");
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_input_radio_radiogroup.html b/dom/html/test/forms/test_input_radio_radiogroup.html
new file mode 100644
index 0000000000..62767def72
--- /dev/null
+++ b/dom/html/test/forms/test_input_radio_radiogroup.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=343444
+-->
+<head>
+ <title>Test for Bug 343444</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=343444">Mozilla Bug 343444</a>
+<p id="display"></p>
+<form>
+ <fieldset id="testradio">
+ <input type="radio" name="testradio" id="start"></input>
+ <input type="text" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ </fieldset>
+
+ <fieldset>
+ <input type="radio" name="testtwo" id="start2"></input>
+ <input type="radio" name="testtwo"></input>
+ <input type="radio" name="error" id="testtwo"></input>
+ <input type="radio" name="testtwo" id="end"></input>
+ </fieldset>
+
+ <fieldset>
+ <input type="radio" name="testthree" id="start3"></input>
+ <input type="radio" name="errorthree" id="testthree"></input>
+ </fieldset>
+</form>
+<script class="testbody" type="text/javascript">
+/** Test for Bug 343444 **/
+SimpleTest.waitForExplicitFinish();
+startTest();
+function startTest() {
+ document.getElementById("start").focus();
+ var count=0;
+ while (count < 2) {
+ sendKey("DOWN");
+ is(document.activeElement.type, "radio", "radioGroup should ignore non-radio input fields");
+ if (document.activeElement.id == "start") {
+ count++;
+ }
+ }
+
+ document.getElementById("start2").focus();
+ count = 0;
+ while (count < 3) {
+ is(document.activeElement.name, "testtwo",
+ "radioGroup should only contain elements with the same @name")
+ sendKey("DOWN");
+ count++;
+ }
+
+ document.getElementById("start3").focus();
+ sendKey("DOWN");
+ is(document.activeElement.name, "testthree", "we don't have an infinite-loop");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_input_radio_required.html b/dom/html/test/forms/test_input_radio_required.html
new file mode 100644
index 0000000000..ae02aab2ff
--- /dev/null
+++ b/dom/html/test/forms/test_input_radio_required.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}
+-->
+<head>
+ <title>Test for Bug 1100535</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1100535">Mozilla Bug 1100535</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<form>
+ <input type="radio" name="a">
+</form>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ var input = document.querySelector("input");
+ input.setAttribute("required", "x");
+ input.setAttribute("required", "y");
+ is(document.forms[0].checkValidity(), false);
+ input.required = false;
+ is(document.forms[0].checkValidity(), true);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_attr_order.html b/dom/html/test/forms/test_input_range_attr_order.html
new file mode 100644
index 0000000000..dc3f1ac95c
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_attr_order.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=841941
+-->
+<head>
+ <title>Test @min/@max/@step order for range</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=841941">Mozilla Bug 841941</a>
+<p id="display"></p>
+<div id="content">
+ <input type=range value=2 max=1.5 step=0.5>
+ <input type=range value=2 step=0.5 max=1.5>
+ <input type=range value=2 max=1.5 step=0.5>
+ <input type=range value=2 step=0.5 max=1.5>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 841941
+ * This test checks that the order in which @min/@max/@step are specified in
+ * markup makes no difference to the value that <input type=range> will be
+ * given. Basically this checks that sanitization of the value does not occur
+ * until after the parser has finished with the element.
+ */
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ var ranges = document.querySelectorAll("input[type=range]");
+ for (var i = 0; i < ranges.length; i++) {
+ is(ranges.item(i).value, "1.5", "Check sanitization order for range " + i);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_key_events.html b/dom/html/test/forms/test_input_range_key_events.html
new file mode 100644
index 0000000000..6daf572916
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_key_events.html
@@ -0,0 +1,207 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=843725
+-->
+<head>
+ <title>Test key events for range</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=843725">Mozilla Bug 843725</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 843725
+ * This test checks how the value of <input type=range> changes in response to
+ * various key events while it is in various states.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+const defaultMinimum = 0;
+const defaultMaximum = 100;
+const defaultStep = 1;
+
+// Helpers:
+// For the sake of simplicity, we do not currently support fractional value,
+// step, etc.
+
+function minimum(element) {
+ return Number(element.min || defaultMinimum);
+}
+
+function maximum(element) {
+ return Number(element.max || defaultMaximum);
+}
+
+function range(element) {
+ var max = maximum(element);
+ var min = minimum(element);
+ if (max < min) {
+ return 0;
+ }
+ return max - min;
+}
+
+function defaultValue(element) {
+ return minimum(element) + range(element)/2;
+}
+
+function value(element) {
+ return Number(element.value || defaultValue(element));
+}
+
+function step(element) {
+ var stepSize = Number(element.step || defaultStep);
+ return stepSize <= 0 ? defaultStep : stepSize;
+}
+
+function clampToRange(val, element) {
+ var min = minimum(element);
+ var max = maximum(element);
+ if (max < min) {
+ return min;
+ }
+ if (val < min) {
+ return min;
+ }
+ if (val > max) {
+ return max;
+ }
+ return val;
+}
+
+// Functions used to specify expected test results:
+
+function valuePlusStep(element) {
+ return clampToRange(value(element) + step(element), element);
+}
+
+function valueMinusStep(element) {
+ return clampToRange(value(element) - step(element), element);
+}
+
+/**
+ * Returns the current value of the range plus whichever is greater of either
+ * 10% of the range or its current step value, clamped to the range's minimum/
+ * maximum. The reason for using the step if it is greater than 10% of the
+ * range is because otherwise the PgUp/PgDn keys would do nothing in that case.
+ */
+function valuePlusTenPctOrStep(element) {
+ var tenPct = range(element)/10;
+ var stp = step(element);
+ return clampToRange(value(element) + Math.max(tenPct, stp), element);
+}
+
+function valueMinusTenPctOrStep(element) {
+ var tenPct = range(element)/10;
+ var stp = step(element);
+ return clampToRange(value(element) - Math.max(tenPct, stp), element);
+}
+
+// Test table:
+
+const LTR = "ltr";
+const RTL = "rtl";
+
+var testTable = [
+ ["KEY_ArrowLeft", LTR, valueMinusStep],
+ ["KEY_ArrowLeft", RTL, valuePlusStep],
+ ["KEY_ArrowRight", LTR, valuePlusStep],
+ ["KEY_ArrowRight", RTL, valueMinusStep],
+ ["KEY_ArrowUp", LTR, valuePlusStep],
+ ["KEY_ArrowUp", RTL, valuePlusStep],
+ ["KEY_ArrowDown", LTR, valueMinusStep],
+ ["KEY_ArrowDown", RTL, valueMinusStep],
+ ["KEY_PageUp", LTR, valuePlusTenPctOrStep],
+ ["KEY_PageUp", RTL, valuePlusTenPctOrStep],
+ ["KEY_PageDown", LTR, valueMinusTenPctOrStep],
+ ["KEY_PageDown", RTL, valueMinusTenPctOrStep],
+ ["KEY_Home", LTR, minimum],
+ ["KEY_Home", RTL, minimum],
+ ["KEY_End", LTR, maximum],
+ ["KEY_End", RTL, maximum],
+]
+
+function test() {
+ var elem = document.createElement("input");
+ elem.type = "range";
+
+ var content = document.getElementById("content");
+ content.appendChild(elem);
+ elem.focus();
+
+ for (test of testTable) {
+ var [key, dir, expectedFunc] = test;
+ var oldVal, expectedVal;
+
+ elem.step = "2";
+ elem.style.direction = dir;
+ var flush = document.body.clientWidth;
+
+ // Start at middle:
+ elem.value = oldVal = defaultValue(elem);
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the midpoint (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // Start at maximum:
+ elem.value = oldVal = maximum(elem);
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the maximum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // Start at minimum:
+ elem.value = oldVal = minimum(elem);
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the minimum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // Test for a step value that is greater than 10% of the range:
+ elem.step = 20;
+ elem.value = 60;
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with a step that is greater than 10% of the range (step=" + elem.step + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // reset step:
+ elem.step = 2;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_mouse_and_touch_events.html b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html
new file mode 100644
index 0000000000..5957ede81d
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html
@@ -0,0 +1,240 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=846380
+-->
+<head>
+ <title>Test mouse and touch events for range</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"/>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ /* synthesizeMouse and synthesizeFunc uses getBoundingClientRect. We set
+ * the following properties to avoid fractional values in the rect returned
+ * by getBoundingClientRect in order to avoid rounding that would occur
+ * when event coordinates are internally converted to be relative to the
+ * top-left of the element. (Such rounding would make it difficult to
+ * predict exactly what value the input should take on for events at
+ * certain coordinates.)
+ */
+ input { margin: 0 ! important; width: 200px ! important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=846380">Mozilla Bug 846380</a>
+<p id="display"></p>
+<div id="content">
+ <input id="range" type="range">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/**
+ * Test for Bug 846380
+ * This test checks how the value of <input type=range> changes in response to
+ * various mouse and touch events.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test(synthesizeMouse, "click", "mousedown", "mousemove", "mouseup");
+ test(synthesizeTouch, "tap", "touchstart", "touchmove", "touchend");
+ SimpleTest.finish();
+});
+
+const kIsWin = AppConstants.platform == "win";
+const kIsLinux = AppConstants.platform == "linux";
+
+const MIDDLE_OF_RANGE = "50";
+const MINIMUM_OF_RANGE = "0";
+const MAXIMUM_OF_RANGE = "100";
+const QUARTER_OF_RANGE = "25";
+const THREE_QUARTERS_OF_RANGE = "75";
+
+function flush() {
+ // Flush style, specifically to flush the 'direction' property so that the
+ // browser uses the new value for thumb positioning.
+ document.body.clientWidth;
+}
+
+function test(synthesizeFunc, clickOrTap, startName, moveName, endName) {
+ var elem = document.getElementById("range");
+ elem.focus();
+ flush();
+
+ var width = parseFloat(window.getComputedStyle(elem).width);
+ var height = parseFloat(window.getComputedStyle(elem).height);
+ var borderLeft = parseFloat(window.getComputedStyle(elem).borderLeftWidth);
+ var borderTop = parseFloat(window.getComputedStyle(elem).borderTopWidth);
+ var paddingLeft = parseFloat(window.getComputedStyle(elem).paddingLeft);
+ var paddingTop = parseFloat(window.getComputedStyle(elem).paddingTop);
+
+ // Extrema for mouse/touch events:
+ var midY = height / 2 + borderTop + paddingTop;
+ var minX = borderLeft + paddingLeft;
+ var midX = minX + width / 2;
+ var maxX = minX + width;
+
+ // Test click/tap in the middle of the range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, {});
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + clickOrTap + " in middle of range");
+
+ // Test mouse/touch dragging of ltr range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ synthesizeFunc(elem, minX, midY, { type: moveName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to left of ltr range");
+
+ synthesizeFunc(elem, maxX, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to right of ltr range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX, midY, { type: endName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to right of ltr range (" + endName + ")");
+
+ // Test mouse/touch dragging of rtl range:
+ elem.value = QUARTER_OF_RANGE;
+ elem.style.direction = "rtl";
+ flush();
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of rtl range");
+ synthesizeFunc(elem, minX, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to left of rtl range");
+
+ synthesizeFunc(elem, maxX, midY, { type: moveName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to right of rtl range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX, midY, { type: endName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to right of rtl range (" + endName + ")");
+
+ elem.style.direction = "ltr"; // reset direction
+ flush();
+
+ // Test mouse/touch capturing by moving pointer to a position outside the range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ synthesizeFunc(elem, maxX+100, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to position outside range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX+100, midY, { type: endName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to position outside range (" + endName + ")");
+
+ // Test mouse/touch capturing by moving pointer to a position outside a rtl range:
+ elem.value = QUARTER_OF_RANGE;
+ elem.style.direction = "rtl";
+ flush();
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of rtl range");
+ synthesizeFunc(elem, maxX+100, midY, { type: moveName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to position outside range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX+100, midY, { type: endName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to position outside range (" + endName + ")");
+
+ elem.style.direction = "ltr"; // reset direction
+ flush();
+
+ // Test mouse/touch events with certain modifiers are ignored:
+ var modifiersIgnore = ["ctrlKey", "altGrKey", "fnKey"];
+ if (kIsWin || kIsLinux) {
+ modifiersIgnore.push("metaKey");
+ }
+ for (var modifier of modifiersIgnore) {
+ elem.value = QUARTER_OF_RANGE;
+ var eventParams = {};
+ eventParams[modifier] = true;
+ synthesizeFunc(elem, midX, midY, eventParams);
+ is(elem.value, QUARTER_OF_RANGE, "Test " + clickOrTap + " in the middle of range with " + modifier + " modifier key is ignored");
+ }
+
+ // Test mouse/touch events with certain modifiers are allowed:
+ var modifiersAllow = ["shiftKey", "altKey"];
+ if (!modifiersIgnore.includes("metaKey")) {
+ modifiersAllow.push("metaKey");
+ }
+ for (var modifier of modifiersAllow) {
+ elem.value = QUARTER_OF_RANGE;
+ var eventParams = {};
+ eventParams[modifier] = true;
+ synthesizeFunc(elem, midX, midY, eventParams);
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + clickOrTap + " in the middle of range with " + modifier + " modifier key is allowed");
+ }
+
+ // Test that preventDefault() works:
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ elem.value = QUARTER_OF_RANGE;
+ elem.addEventListener(startName, preventDefault);
+ synthesizeFunc(elem, midX, midY, {});
+ is(elem.value, QUARTER_OF_RANGE, "Test that preventDefault() works");
+ elem.removeEventListener(startName, preventDefault);
+
+ // Test that changing the input type in the middle of a drag cancels the drag:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ elem.type = "text";
+ is(elem.value, QUARTER_OF_RANGE, "Test that changing the input type cancels a drag");
+ synthesizeFunc(elem, midX, midY, { type: endName });
+ is(elem.value, QUARTER_OF_RANGE, "Test that changing the input type cancels a drag (after " + endName + ")");
+ elem.type = "range";
+
+ // Check that we do not drag when the mousedown/touchstart occurs outside the range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, maxX+100, midY, { type: startName });
+ is(elem.value, QUARTER_OF_RANGE, "Test " + startName + " outside range doesn't change its value");
+ synthesizeFunc(elem, midX, midY, { type: moveName });
+ is(elem.value, QUARTER_OF_RANGE, "Test dragging is not occurring when " + startName + " was outside range");
+
+ synthesizeFunc(elem, midX, midY, { type: endName });
+ is(elem.value, QUARTER_OF_RANGE, "Test dragging is not occurring when " + startName + " was outside range");
+
+ elem.focus(); // RESTORE FOCUS SO WE GET THE FOCUSED STYLE FOR TESTING OR ELSE minX/midX/maxX may be wrong!
+
+ // Check what happens when a value changing key is pressed during a drag:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ synthesizeKey("KEY_Home");
+ // The KEY_Home tests are disabled until I can figure out why they fail on Android -jwatt
+ //is(elem.value, MINIMUM_OF_RANGE, "Test KEY_Home during a drag sets the value to the minimum of the range");
+ synthesizeFunc(elem, midX+100, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test " + moveName + " outside range after key press that occurred during a drag changes the value");
+ synthesizeFunc(elem, midX, midY, { type: moveName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + moveName + " in middle of range");
+ synthesizeKey("KEY_Home");
+ //is(elem.value, MINIMUM_OF_RANGE, "Test KEY_Home during a drag sets the value to the minimum of the range (second time)");
+ synthesizeFunc(elem, maxX+100, midY, { type: endName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test " + endName + " outside range after key press that occurred during a drag changes the value");
+
+ function hideElement() {
+ elem.parentNode.style.display = 'none';
+ elem.parentNode.offsetLeft;
+ }
+
+ if (clickOrTap == "click") {
+ elem.addEventListener("mousedown", hideElement);
+ } else if (clickOrTap == "tap") {
+ elem.addEventListener("touchstart", hideElement);
+ }
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ synthesizeFunc(elem, midX, midY, { type: endName });
+ elem.removeEventListener("mousedown", hideElement);
+ elem.removeEventListener("touchstart", hideElement);
+ ok(true, "Hiding the element during mousedown/touchstart shouldn't crash the process.");
+ elem.parentNode.style.display = "block";
+ elem.parentNode.offsetLeft;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_rounding.html b/dom/html/test/forms/test_input_range_rounding.html
new file mode 100644
index 0000000000..9c3c21ce6e
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_rounding.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=853525
+-->
+<head>
+ <title>Test key events for range</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=853525">Mozilla Bug 853525</a>
+<p id="display"></p>
+<div id="content">
+ <input id=range type=range value=0 step=0.01 max=1>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 853525
+ * This test checks that when <input type=range> has fractional step values,
+ * the values that a content author will see in their script will not have
+ * ugly rounding errors.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+/**
+ * We can _NOT_ generate these values by looping and simply incrementing a
+ * variable by 0.01 and stringifying it, since we'll end up with strings like
+ * "0.060000000000000005" due to the inability of binary floating point numbers
+ * to accurately represent decimal values.
+ */
+var stepVals = [
+ "0", "0.01", "0.02", "0.03", "0.04", "0.05", "0.06", "0.07", "0.08", "0.09",
+ "0.1", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "0.19",
+ "0.2", "0.21", "0.22", "0.23", "0.24", "0.25", "0.26", "0.27", "0.28", "0.29",
+ "0.3", "0.31", "0.32", "0.33", "0.34", "0.35", "0.36", "0.37", "0.38", "0.39",
+ "0.4", "0.41", "0.42", "0.43", "0.44", "0.45", "0.46", "0.47", "0.48", "0.49",
+ "0.5", "0.51", "0.52", "0.53", "0.54", "0.55", "0.56", "0.57", "0.58", "0.59",
+ "0.6", "0.61", "0.62", "0.63", "0.64", "0.65", "0.66", "0.67", "0.68", "0.69",
+ "0.7", "0.71", "0.72", "0.73", "0.74", "0.75", "0.76", "0.77", "0.78", "0.79",
+ "0.8", "0.81", "0.82", "0.83", "0.84", "0.85", "0.86", "0.87", "0.88", "0.89",
+ "0.9", "0.91", "0.92", "0.93", "0.94", "0.95", "0.96", "0.97", "0.98", "0.99",
+ "1"
+];
+
+var pgUpDnVals = [
+ "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1"
+];
+
+function test() {
+ var elem = document.getElementById("range");
+
+ elem.focus();
+
+ for (var i = 1; i < pgUpDnVals.length; ++i) {
+ synthesizeKey("KEY_PageUp");
+ is(elem.value, pgUpDnVals[i], "Test KEY_PageUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+
+ for (var i = pgUpDnVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_PageDown");
+ is(elem.value, pgUpDnVals[i], "Test KEY_PageDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ synthesizeKey("KEY_ArrowUp");
+ is(elem.value, stepVals[i], "Test KEY_ArrowUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_ArrowDown");
+ is(elem.value, stepVals[i], "Test KEY_ArrowDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ elem.stepUp();
+ is(elem.value, stepVals[i], "Test stepUp()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ elem.stepDown();
+ is(elem.value, stepVals[i], "Test stepDown()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_sanitization.html b/dom/html/test/forms/test_input_sanitization.html
new file mode 100644
index 0000000000..474ddd621d
--- /dev/null
+++ b/dom/html/test/forms/test_input_sanitization.html
@@ -0,0 +1,585 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=549475
+-->
+<head>
+ <title>Test for Bug 549475</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549475">Mozilla Bug 549475</a>
+<p id="display"></p>
+<pre id="test">
+<div id='content'>
+ <form>
+ </form>
+</div>
+<script type="application/javascript">
+
+SimpleTest.requestLongerTimeout(2);
+
+/**
+ * This files tests the 'value sanitization algorithm' for the various input
+ * types. Note that an input's value is affected by more than just its type's
+ * value sanitization algorithm; e.g. some type=range has actions that the user
+ * agent must perform to change the element's value to avoid underflow/overflow
+ * and step mismatch (when possible). We specifically avoid triggering these
+ * other actions here so that this test only tests the value sanitization
+ * algorithm for the various input types.
+ *
+ * XXXjwatt splitting out testing of the value sanitization algorithm and
+ * "other things" that affect .value makes it harder to know what we're testing
+ * and what we've missed, because what's included in the value sanitization
+ * algorithm and what's not is different from input type to input type. It
+ * seems to me it would be better to have a test (maybe one per type) focused
+ * on testing .value for permutations of all other inputs that can affect it.
+ * The value sanitization algorithm is just an internal spec concept after all.
+ */
+
+// We buffer up the results of sets of sub-tests, and avoid outputting log
+// entries for them all if they all pass. Otherwise, we have an enormous amount
+// of test output.
+
+var delayedTests = [];
+var anyFailedDelayedTests = false;
+
+function delayed_is(actual, expected, description)
+{
+ var result = actual == expected;
+ delayedTests.push({ actual, expected, description });
+ if (!result) {
+ anyFailedDelayedTests = true;
+ }
+}
+
+function flushDelayedTests(description)
+{
+ if (anyFailedDelayedTests) {
+ info("Outputting individual results for \"" + description + "\" due to failures in subtests");
+ for (var test of delayedTests) {
+ is(test.actual, test.expected, test.description);
+ }
+ } else {
+ ok(true, description + " (" + delayedTests.length + " subtests)");
+ }
+ delayedTests = [];
+ anyFailedDelayedTests = false;
+}
+
+// We are excluding "file" because it's too different from the other types.
+// And it has no sanitizing algorithm.
+var inputTypes =
+[
+ "text", "password", "search", "tel", "hidden", "checkbox", "radio",
+ "submit", "image", "reset", "button", "email", "url", "number", "date",
+ "time", "range", "color", "month", "week", "datetime-local"
+];
+
+var valueModeValue =
+[
+ "text", "search", "url", "tel", "email", "password", "date", "datetime",
+ "month", "week", "time", "datetime-local", "number", "range", "color",
+];
+
+function sanitizeDate(aValue)
+{
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string
+ function getNumbersOfDaysInMonth(aMonth, aYear) {
+ if (aMonth === 2) {
+ return (aYear % 400 === 0 || (aYear % 100 != 0 && aYear % 4 === 0)) ? 29 : 28;
+ }
+ return (aMonth === 1 || aMonth === 3 || aMonth === 5 || aMonth === 7 ||
+ aMonth === 8 || aMonth === 10 || aMonth === 12) ? 31 : 30;
+ }
+
+ var match = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var year = Number(match[1]);
+ if (year === 0) {
+ return "";
+ }
+ var month = Number(match[2]);
+ if (month > 12 || month < 1) {
+ return "";
+ }
+ var day = Number(match[3]);
+ return 1 <= day && day <= getNumbersOfDaysInMonth(month, year) ? aValue : "";
+}
+
+function sanitizeTime(aValue)
+{
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string
+ var match = /^([0-9]{2}):([0-9]{2})(.*)$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var hours = match[1];
+ if (hours < 0 || hours > 23) {
+ return "";
+ }
+ var minutes = match[2];
+ if (minutes < 0 || minutes > 59) {
+ return "";
+ }
+ var other = match[3];
+ if (other == "") {
+ return aValue;
+ }
+ match = /^:([0-9]{2})(.*)$/.exec(other);
+ if (!match) {
+ return "";
+ }
+ var seconds = match[1];
+ if (seconds < 0 || seconds > 59) {
+ return "";
+ }
+ var other = match[2];
+ if (other == "") {
+ return aValue;
+ }
+ match = /^.([0-9]{1,3})$/.exec(other);
+ if (!match) {
+ return "";
+ }
+ return aValue;
+}
+
+function sanitizeDateTimeLocal(aValue)
+{
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-local-date-and-time-string
+ if (aValue.length < 16) {
+ return "";
+ }
+
+ var sepIndex = aValue.indexOf("T");
+ if (sepIndex == -1) {
+ sepIndex = aValue.indexOf(" ");
+ if (sepIndex == -1) {
+ return "";
+ }
+ }
+
+ var [date, time] = aValue.split(aValue[sepIndex]);
+ if (!sanitizeDate(date)) {
+ return "";
+ }
+
+ if (!sanitizeTime(time)) {
+ return "";
+ }
+
+ // Normalize datetime-local string.
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string
+ if (aValue[sepIndex] == " ") {
+ aValue = date + "T" + time;
+ }
+
+ if ((aValue.length - sepIndex) == 6) {
+ return aValue;
+ }
+
+ if ((aValue.length - sepIndex) > 9) {
+ var milliseconds = aValue.substring(sepIndex + 10);
+ if (Number(milliseconds) != 0) {
+ return aValue;
+ }
+ aValue = aValue.slice(0, sepIndex + 9);
+ }
+
+ var seconds = aValue.substring(sepIndex + 7);
+ if (Number(seconds) != 0) {
+ return aValue;
+ }
+ aValue = aValue.slice(0, sepIndex + 6);
+
+ return aValue;
+}
+
+function sanitizeValue(aType, aValue)
+{
+ // http://www.whatwg.org/html/#value-sanitization-algorithm
+ switch (aType) {
+ case "text":
+ case "password":
+ case "search":
+ case "tel":
+ return aValue.replace(/[\n\r]/g, "");
+ case "url":
+ case "email":
+ return aValue.replace(/[\n\r]/g, "").replace(/^[\u0020\u0009\t\u000a\u000c\u000d]+|[\u0020\u0009\t\u000a\u000c\u000d]+$/g, "");
+ case "number":
+ return isNaN(Number(aValue)) ? "" : aValue;
+ case "range":
+ var defaultMinimum = 0;
+ var defaultMaximum = 100;
+ var value = Number(aValue);
+ if (isNaN(value)) {
+ return ((defaultMaximum - defaultMinimum)/2).toString(); // "50"
+ }
+ if (value < defaultMinimum) {
+ return defaultMinimum.toString();
+ }
+ if (value > defaultMaximum) {
+ return defaultMaximum.toString();
+ }
+ return aValue;
+ case "date":
+ return sanitizeDate(aValue);
+ case "time":
+ return sanitizeTime(aValue);
+ case "month":
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-month-string
+ var match = /^([0-9]{4,})-([0-9]{2})$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var year = Number(match[1]);
+ if (year === 0) {
+ return "";
+ }
+ var month = Number(match[2]);
+ if (month > 12 || month < 1) {
+ return "";
+ }
+ return aValue;
+ case "week":
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-week-string
+ function isLeapYear(aYear) {
+ return ((aYear % 4 == 0) && (aYear % 100 != 0)) || (aYear % 400 == 0);
+ }
+ function getDayofWeek(aYear, aMonth, aDay) { /* 0 = Sunday */
+ // Tomohiko Sakamoto algorithm.
+ var monthTable = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
+ aYear -= Number(aMonth < 3);
+
+ return (aYear + parseInt(aYear / 4) - parseInt(aYear / 100) +
+ parseInt(aYear / 400) + monthTable[aMonth - 1] + aDay) % 7;
+ }
+ function getMaximumWeekInYear(aYear) {
+ var day = getDayofWeek(aYear, 1, 1);
+ return day == 4 || (day == 3 && isLeapYear(aYear)) ? 53 : 52;
+ }
+
+ var match = /^([0-9]{4,})-W([0-9]{2})$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var year = Number(match[1]);
+ if (year === 0) {
+ return "";
+ }
+ var week = Number(match[2]);
+ if (week > 53 || month < 1) {
+ return "";
+ }
+ return 1 <= week && week <= getMaximumWeekInYear(year) ? aValue : "";
+ case "datetime-local":
+ return sanitizeDateTimeLocal(aValue);
+ case "color":
+ return /^#[0-9A-Fa-f]{6}$/.exec(aValue) ? aValue.toLowerCase() : "#000000";
+ default:
+ return aValue;
+ }
+}
+
+function checkSanitizing(element, inputTypeDescription)
+{
+ var testData =
+ [
+ // For text, password, search, tel, email:
+ "\n\rfoo\n\r",
+ "foo\n\rbar",
+ " foo ",
+ " foo\n\r bar ",
+ // For url:
+ "\r\n foobar \n\r",
+ "\u000B foo \u000B",
+ "\u000A foo \u000A",
+ "\u000C foo \u000C",
+ "\u000d foo \u000d",
+ "\u0020 foo \u0020",
+ " \u0009 foo \u0009 ",
+ // For number and range:
+ "42",
+ "13.37",
+ "1.234567898765432",
+ "12foo",
+ "1e2",
+ "3E42",
+ // For date:
+ "1970-01-01",
+ "1234-12-12",
+ "1234567890-01-02",
+ "2012-12-31",
+ "2012-02-29",
+ "2000-02-29",
+ "1234",
+ "1234-",
+ "12345",
+ "1234-01",
+ "1234-012",
+ "1234-01-",
+ "12-12",
+ "999-01-01",
+ "1234-56-78-91",
+ "1234-567-78",
+ "1234--7-78",
+ "abcd-12-12",
+ "thisinotadate",
+ "2012-13-01",
+ "1234-12-42",
+ " 2012-13-01",
+ " 123-01-01",
+ "2012- 3-01",
+ "12- 10- 01",
+ " 12-0-1",
+ "2012-3-001",
+ "2012-12-00",
+ "2012-12-1r",
+ "2012-11-31",
+ "2011-02-29",
+ "2100-02-29",
+ "a2000-01-01",
+ "2000a-01-0'",
+ "20aa00-01-01",
+ "2000a2000-01-01",
+ "2000-1-1",
+ "2000-1-01",
+ "2000-01-1",
+ "2000-01-01 ",
+ "2000- 01-01",
+ "-1970-01-01",
+ "0000-00-00",
+ "0001-00-00",
+ "0000-01-01",
+ "1234-12 12",
+ "1234 12-12",
+ "1234 12 12",
+ // For time:
+ "1",
+ "10",
+ "10:",
+ "10:1",
+ "21:21",
+ ":21:21",
+ "-21:21",
+ " 21:21",
+ "21-21",
+ "21:21:",
+ "21:211",
+ "121:211",
+ "21:21 ",
+ "00:00",
+ "-1:00",
+ "24:00",
+ "00:60",
+ "01:01",
+ "23:59",
+ "99:99",
+ "8:30",
+ "19:2",
+ "19:a2",
+ "4c:19",
+ "10:.1",
+ "1.:10",
+ "13:37:42",
+ "13:37.42",
+ "13:37:42 ",
+ "13:37:42.",
+ "13:37:61.",
+ "13:37:00",
+ "13:37:99",
+ "13:37:b5",
+ "13:37:-1",
+ "13:37:.1",
+ "13:37:1.",
+ "13:37:42.001",
+ "13:37:42.001",
+ "13:37:42.abc",
+ "13:37:42.00c",
+ "13:37:42.a23",
+ "13:37:42.12e",
+ "13:37:42.1e1",
+ "13:37:42.e11",
+ "13:37:42.1",
+ "13:37:42.99",
+ "13:37:42.0",
+ "13:37:42.00",
+ "13:37:42.000",
+ "13:37:42.-1",
+ "13:37:42.1.1",
+ "13:37:42.1,1",
+ "13:37:42.",
+ "foo12:12",
+ "13:37:42.100000000000",
+ // For color
+ "#00ff00",
+ "#000000",
+ "red",
+ "#0f0",
+ "#FFFFAA",
+ "FFAABB",
+ "fFAaBb",
+ "FFAAZZ",
+ "ABCDEF",
+ "#7654321",
+ // For month
+ "1970-01",
+ "1234-12",
+ "123456789-01",
+ "2013-13",
+ "0000-00",
+ "2015-00",
+ "0001-01",
+ "1-1",
+ "888-05",
+ "2013-3",
+ "2013-may",
+ "2000-1a",
+ "2013-03-13",
+ "december",
+ "abcdef",
+ "12",
+ " 2013-03",
+ "2013 - 03",
+ "2013 03",
+ "2013/03",
+ // For week
+ "1970-W01",
+ "1970-W53",
+ "1964-W53",
+ "1900-W10",
+ "2004-W53",
+ "2065-W53",
+ "2099-W53",
+ "2010-W53",
+ "2016-W30",
+ "1900-W3",
+ "2016-w30",
+ "2016-30",
+ "16-W30",
+ "2016-Week30",
+ "2000-100",
+ "0000-W01",
+ "00-W01",
+ "123456-W05",
+ "1985-W100",
+ "week",
+ // For datetime-local
+ "1970-01-01T00:00",
+ "1970-01-01Z12:00",
+ "1970-01-01 00:00:00",
+ "1970-01-01T00:00:00.0",
+ "1970-01-01T00:00:00.00",
+ "1970-01-01T00:00:00.000",
+ "1970-01-01 00:00:00.20",
+ "1969-12-31 23:59",
+ "1969-12-31 23:59:00",
+ "1969-12-31 23:59:00.000",
+ "1969-12-31 23:59:00.30",
+ "123456-01-01T12:00",
+ "123456-01-01T12:00:00",
+ "123456-01-01T12:00:00.0",
+ "123456-01-01T12:00:00.00",
+ "123456-01-01T12:00:00.000",
+ "123456-01-01T12:00:30",
+ "123456-01-01T12:00:00.123",
+ "10000-12-31 20:00",
+ "10000-12-31 20:00:00",
+ "10000-12-31 20:00:00.0",
+ "10000-12-31 20:00:00.00",
+ "10000-12-31 20:00:00.000",
+ "10000-12-31 20:00:30",
+ "10000-12-31 20:00:00.123",
+ "2016-13-01T12:00",
+ "2016-12-32T12:00",
+ "2016-11-08 15:40:30.0",
+ "2016-11-08T15:40:30.00",
+ "2016-11-07T17:30:10",
+ "2016-12-1T12:45",
+ "2016-12-01T12:45:30.123456",
+ "2016-12-01T24:00",
+ "2016-12-01T12:88:30",
+ "2016-12-01T12:30:99",
+ "2016-12-01T12:30:100",
+ "2016-12-01",
+ "2016-12-01T",
+ "2016-Dec-01T00:00",
+ "12-05-2016T00:00",
+ "datetime-local"
+ ];
+
+ for (value of testData) {
+ element.setAttribute('value', value);
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), value,
+ "The content value should not have been sanitized");
+
+ if (type in valueModeValue) {
+ element.setAttribute('value', 'tulip');
+ element.value = value;
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), 'tulip',
+ "The content value should not have been sanitized");
+ }
+
+ element.setAttribute('value', '');
+ form.reset();
+ element.type = 'checkbox'; // We know this type has no sanitizing algorithm.
+ element.setAttribute('value', value);
+ delayed_is(element.value, value, "The value should not have been sanitized");
+ element.type = type;
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), value,
+ "The content value should not have been sanitized");
+
+ element.setAttribute('value', '');
+ form.reset();
+ element.setAttribute('value', value);
+ form.reset();
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), value,
+ "The content value should not have been sanitized");
+
+ // Cleaning-up.
+ element.setAttribute('value', '');
+ form.reset();
+ }
+
+ flushDelayedTests(inputTypeDescription);
+}
+
+for (type of inputTypes) {
+ var form = document.forms[0];
+ var element = document.createElement("input");
+ element.style.display = "none";
+ element.type = type;
+ form.appendChild(element);
+
+ checkSanitizing(element, "type=" + type + ", no frame, no editor");
+
+ element.style.display = "";
+ checkSanitizing(element, "type=" + type + ", frame, no editor");
+
+ element.focus();
+ element.blur();
+ checkSanitizing(element, "type=" + type + ", frame, editor");
+
+ element.style.display = "none";
+ checkSanitizing(element, "type=" + type + ", no frame, editor");
+
+ form.removeChild(element);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_setting_value.html b/dom/html/test/forms/test_input_setting_value.html
new file mode 100644
index 0000000000..b6ddd66d24
--- /dev/null
+++ b/dom/html/test/forms/test_input_setting_value.html
@@ -0,0 +1,619 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for setting input value</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content"><input type="text"></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
+
+ let input = document.querySelector("input[type=text]");
+
+ // Setting value during composition causes committing composition before setting the value.
+ input.focus();
+ let description = 'Setting input value at first "compositionupdate" event: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be the specified value at "compositionupdate" event (after setting the value)`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "def", `${description}input value should be the specified value at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ todo_is(input.value, "def", `${description}input value should be the specified value at "input" event`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ todo_is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value at second "compositionupdate" event: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ input.value = "def";
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ is(input.value, "def", `${description}input value should be specified value at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "def", `${description}input value should be specified value at "input" event`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value at "input" event for first composition update: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value at "input" event for second composition update: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value and reframing at "input" event for first composition update: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing at "input" event for second composition update: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing with flushing layout at "input" event for first composition update: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ document.documentElement.scrollTop;
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing with flushing layout at "input" event for second composition update: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ document.documentElement.scrollTop;
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ // autocomplete and correcting misspelled word by spellchecker cause an "input" event with same path as setting input value.
+ input.value = "";
+ description = 'Setting input value at "input" event whose inputType is "insertReplacementText';
+ let inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+
+ input.value = "";
+ description = 'Setting input value and reframing at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ input.style.width = "1000px";
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing with flushing layout at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ input.style.width = "1000px";
+ document.documentElement.scrollTop;
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and destroying the frame at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ input.style.display = "none";
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.style.display = "inline";
+
+ input.value = "";
+ description = 'Changing input type at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before changing type)`);
+ input.type = "button";
+ is(input.value, "abc", `${description}input value should keep inserted value at "input" event (after changing type)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(input.value, "abc", `${description}input value should keep inserted value after the last "input" event`);
+ is(input.type, "button", `${description}input type should be changed correctly`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.type = "text";
+ is(input.value, "abc", `${description}input value should keep inserted value immediately after restoring the type`);
+ todo(SpecialPowers.wrap(input).hasEditor, `${description}restoring input type should create editor if it's focused element`);
+ input.blur();
+ input.focus();
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "abc", `${description}input value should keep inserted value after creating editor`);
+
+ input.value = "";
+ description = 'Changing input type and flush layout at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before changing type)`);
+ input.type = "button";
+ input.getBoundingClientRect().height;
+ is(input.value, "abc", `${description}input value should keep inserted value at "input" event (after changing type)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(input.value, "abc", `${description}input value should keep inserted value after the last "input" event`);
+ is(input.type, "button", `${description}input type should be changed correctly`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.type = "text";
+ is(input.value, "abc", `${description}input value should keep inserted value immediately after restoring the type`);
+ todo(SpecialPowers.wrap(input).hasEditor, `${description}restoring input type should create editor if it's focused element`);
+ input.blur();
+ input.focus();
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "abc", `${description}input value should keep inserted value after creating editor`);
+
+ function testSettingValueFromBeforeInput(aWithEditor, aPreventDefaultOfBeforeInput) {
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function recordEvent(aEvent) {
+ if (aEvent.type === "beforeinput") {
+ beforeInputEvents.push(aEvent);
+ } else {
+ inputEvents.push(aEvent);
+ }
+ }
+ let condition = `(${aWithEditor ? "with editor" : "without editor"}${aPreventDefaultOfBeforeInput ? ' and canceling "beforeinput" event' : ""}, the pref ${kSetUserInputCancelable ? "allows" : "disallows"} to cancel "beforeinput" event})`;
+ function Reset() {
+ beforeInputEvents = [];
+ inputEvents = [];
+ if (SpecialPowers.wrap(input).hasEditor != aWithEditor) {
+ if (aWithEditor) {
+ input.blur();
+ input.focus(); // Associate `TextEditor` with input
+ if (!SpecialPowers.wrap(input).hasEditor) {
+ ok(false, `${description}Failed to associate TextEditor with the input ${condition}`);
+ return false;
+ }
+ } else {
+ input.blur();
+ input.type = "button";
+ input.type = "text";
+ if (SpecialPowers.wrap(input).hasEditor) {
+ ok(false, `${description}Failed to disassociate TextEditor from the input ${condition}`);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is different from Chrome (verified with spellchecker).
+ // Chrome inserts the new text to current value and selected range.
+ // It might be reasonable, but we don't touch this for now since it
+ // requires a lot of changes.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ is(inputEvents.length, 1, `${description}"input" event should be fired`);
+ if (inputEvents.length) {
+ is(inputEvents[0].inputType,
+ "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ is(inputEvents[0].data, "def",
+ `${description}data of "input" event should be the value specified by setUserInput()`);
+ }
+ }
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" and changing the type to "button" ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ input.type = "button";
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is same as Chrome (verified with spellchecker).
+ // But this behavior is not consistent with just setting the value on Chrome.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ // Same as Chrome
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since the input element's type is changed`);
+ }
+ input.type = "text";
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" and destroying the frame ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ input.style.display = "none";
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is same as Chrome (verified with spellchecker).
+ // But this behavior is not consistent with just setting the value on Chrome.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ // Different from Chrome
+ is(inputEvents.length, 1,
+ `${description}"input" event should be fired even if the frame of target is destroyed`);
+ if (inputEvents.length) {
+ is(inputEvents[0].inputType,
+ "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ is(inputEvents[0].data, "def",
+ `${description}data of "input" event should be the value specified by setUserInput()`);
+ }
+ }
+ input.style.display = "inline";
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+
+ if (aWithEditor) {
+ return;
+ }
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText and create editor" ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ input.focus();
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is different from Chrome (verified with spellchecker).
+ // Chrome inserts the new text to current value and selected range.
+ // It might be reasonable, but we don't touch this for now since it
+ // requires a lot of changes.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ is(inputEvents.length, 1, `${description}"input" event should be fired`);
+ if (inputEvents.length) {
+ is(inputEvents[0].inputType,
+ "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ is(inputEvents[0].data, "def",
+ `${description}data of "input" event should be the value specified by setUserInput()`);
+ }
+ }
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+ }
+ // testSettingValueFromBeforeInput(true, true);
+ // testSettingValueFromBeforeInput(true, false);
+ testSettingValueFromBeforeInput(false, true);
+ testSettingValueFromBeforeInput(false, false);
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html
new file mode 100644
index 0000000000..79a0f3d15a
--- /dev/null
+++ b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=829606
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 829606</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 829606 **/
+ /*
+ * This test checks that setting .value on an text field (input or textarea)
+ * doesn't scroll the field to its beginning.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var gTestRunner = null;
+
+ async function test(aElementName)
+ {
+ var element = document.getElementsByTagName(aElementName)[0];
+ element.focus();
+
+ var baseSnapshot = await snapshotWindow(window);
+
+ // This is a sanity check.
+ var s2 = await snapshotWindow(window);
+ var results = compareSnapshots(baseSnapshot, await snapshotWindow(window), true);
+ ok(results[0], "sanity check: screenshots should be the same");
+
+ element.selectionStart = element.selectionEnd = element.value.length;
+
+ setTimeout(function() {
+ sendString('f');
+
+ requestAnimationFrame(async function() {
+ var selectionAtTheEndSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(baseSnapshot, selectionAtTheEndSnapshot, false);
+ ok(results[0], "after appending a character, string should have changed");
+
+ // Re-setting value shouldn't change anything.
+ // eslint-disable-next-line no-self-assign
+ element.value = element.value;
+ var tmpSnapshot = await snapshotWindow(window);
+
+ results = compareSnapshots(baseSnapshot, tmpSnapshot, false);
+ ok(results[0], "re-setting the value should change nothing");
+
+ results = compareSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, true);
+ ok(results[0], "re-setting the value should change nothing");
+
+ element.selectionStart = element.selectionEnd = 0;
+ element.blur();
+
+ gTestRunner.next();
+ });
+ }, 0);
+ }
+
+ // This test checks that when a textarea has a long list of values and the
+ // textarea's value is then changed, the values are shown correctly.
+ async function testCorrectUpdateOnScroll()
+ {
+ var textarea = document.createElement('textarea');
+ textarea.rows = 5;
+ textarea.cols = 10;
+ textarea.value = 'a\nb\nc\nd';
+ document.getElementById('content').appendChild(textarea);
+
+ var baseSnapshot = await snapshotWindow(window);
+
+ textarea.value = '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n';
+ textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
+
+ var fullSnapshot = await snapshotWindow(window);
+ var results = compareSnapshots(baseSnapshot, fullSnapshot, false);
+ ok(results[0], "sanity check: screenshots should not be the same");
+
+ textarea.value = 'a\nb\nc\nd';
+
+ var tmpSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(baseSnapshot, tmpSnapshot, true);
+ ok(results[0], "textarea view should look like the beginning");
+
+ setTimeout(function() {
+ gTestRunner.next();
+ }, 0);
+ }
+
+ function* runTest()
+ {
+ test('input');
+ yield undefined;
+ test('textarea');
+ yield undefined;
+ testCorrectUpdateOnScroll();
+ yield undefined;
+ SimpleTest.finish();
+ }
+
+ gTestRunner = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ gTestRunner.next();
+ });;
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=829606">Mozilla Bug 829606</a>
+<p id="display"></p>
+<div id="content">
+ <textarea rows='1' cols='5' style='-moz-appearance:none;'>this is a \n long text</textarea>
+ <input size='5' value="this is a very long text" style='-moz-appearance:none;'>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_time_key_events.html b/dom/html/test/forms/test_input_time_key_events.html
new file mode 100644
index 0000000000..c738816653
--- /dev/null
+++ b/dom/html/test/forms/test_input_time_key_events.html
@@ -0,0 +1,221 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+ <title>Test key events for time control</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="time">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+var testData = [
+ /**
+ * keys: keys to send to the input element.
+ * initialVal: initial value set to the input element.
+ * expectedVal: expected value of the input element after sending the keys.
+ */
+ {
+ // Type 16 in the hour field will automatically change time to 4PM in 12-hour clock
+ keys: ["16"],
+ initialVal: "01:00",
+ expectedVal: "16:00"
+ },
+ {
+ // Type 00 in hour field will automatically convert to 12AM in 12-hour clock
+ keys: ["00"],
+ initialVal: "03:00",
+ expectedVal: "00:00"
+ },
+ {
+ // Type hour > 23 such as 24 will automatically convert to 2
+ keys: ["24"],
+ initialVal: "04:00",
+ expectedVal: "02:00"
+ },
+ {
+ // Type 1030 and select AM.
+ keys: ["1030"],
+ initialVal: "",
+ expectedVal: "10:30"
+ },
+ {
+ // Type 3 in the hour field will automatically advance to the minute field.
+ keys: ["330"],
+ initialVal: "",
+ expectedVal: "03:30"
+ },
+ {
+ // Type 5 in the hour field will automatically advance to the minute field.
+ // Type 7 in the minute field will automatically advance to the AM/PM field.
+ keys: ["57"],
+ initialVal: "",
+ expectedVal: "05:07"
+ },
+ {
+ // Advance to AM/PM field and change to PM.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "10:30",
+ expectedVal: "22:30"
+ },
+ {
+ // Right key should do the same thing as TAB key.
+ keys: ["KEY_ArrowRight", "KEY_ArrowRight", "KEY_ArrowDown"],
+ initialVal: "10:30",
+ expectedVal: "22:30"
+ },
+ {
+ // Advance to minute field then back to hour field and decrement.
+ keys: ["KEY_ArrowRight", "KEY_ArrowLeft", "KEY_ArrowDown"],
+ initialVal: "10:30",
+ expectedVal: "09:30"
+ },
+ {
+ // Focus starts on the first field, hour in this case, and increment.
+ keys: ["KEY_ArrowUp"],
+ initialVal: "16:00",
+ expectedVal: "17:00"
+ },
+ {
+ // Advance to minute field and decrement.
+ keys: ["KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "16:00",
+ expectedVal: "16:59"
+ },
+ {
+ // Advance to minute field and increment.
+ keys: ["KEY_Tab", "KEY_ArrowUp"],
+ initialVal: "16:59",
+ expectedVal: "16:00"
+ },
+ {
+ // PageUp on hour field increments hour by 3.
+ keys: ["KEY_PageUp"],
+ initialVal: "05:00",
+ expectedVal: "08:00"
+ },
+ {
+ // PageDown on hour field decrements hour by 3.
+ keys: ["KEY_PageDown"],
+ initialVal: "05:00",
+ expectedVal: "02:00"
+ },
+ {
+ // PageUp on minute field increments minute by 10.
+ keys: ["KEY_Tab", "KEY_PageUp"],
+ initialVal: "14:00",
+ expectedVal: "14:10"
+ },
+ {
+ // PageDown on minute field decrements minute by 10.
+ keys: ["KEY_Tab", "KEY_PageDown"],
+ initialVal: "14:00",
+ expectedVal: "14:50"
+ },
+ {
+ // Home key on hour field sets it to the minimum hour, which is 1 in 12-hour
+ // clock.
+ keys: ["KEY_Home"],
+ initialVal: "03:10",
+ expectedVal: "01:10"
+ },
+ {
+ // End key on hour field sets it to the maximum hour, which is 12PM in 12-hour
+ // clock.
+ keys: ["KEY_End"],
+ initialVal: "03:10",
+ expectedVal: "12:10"
+ },
+ {
+ // Home key on minute field sets it to the minimum minute, which is 0.
+ keys: ["KEY_Tab", "KEY_Home"],
+ initialVal: "19:30",
+ expectedVal: "19:00"
+ },
+ {
+ // End key on minute field sets it to the minimum minute, which is 59.
+ keys: ["KEY_Tab", "KEY_End"],
+ initialVal: "19:30",
+ expectedVal: "19:59"
+ },
+ // Second field will show up when needed.
+ {
+ // PageUp on second field increments second by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageUp"],
+ initialVal: "08:10:10",
+ expectedVal: "08:10:20"
+ },
+ {
+ // PageDown on second field increments second by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageDown"],
+ initialVal: "08:10:10",
+ expectedVal: "08:10:00"
+ },
+ {
+ // Home key on second field sets it to the minimum second, which is 0.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Home"],
+ initialVal: "16:00:30",
+ expectedVal: "16:00:00"
+ },
+ {
+ // End key on second field sets it to the minimum second, which is 59.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_End"],
+ initialVal: "16:00:30",
+ expectedVal: "16:00:59"
+ },
+ {
+ // Incomplete value maps to empty .value.
+ keys: ["1"],
+ initialVal: "",
+ expectedVal: ""
+ },
+];
+
+function sendKeys(aKeys, aElem) {
+ for (let i = 0; i < aKeys.length; i++) {
+ // Force layout flush between keys to ensure focus is correct.
+ // This shouldn't be necessary; bug 1450219 tracks this.
+ aElem.clientTop;
+ let key = aKeys[i];
+ if (key.startsWith("KEY_")) {
+ synthesizeKey(key);
+ } else {
+ sendString(key);
+ }
+ }
+}
+
+function test() {
+ var elem = document.getElementById("input");
+
+ for (let { keys, initialVal, expectedVal } of testData) {
+ elem.focus();
+ elem.value = initialVal;
+ sendKeys(keys, elem);
+ is(elem.value, expectedVal,
+ "Test with " + keys + ", result should be " + expectedVal);
+ elem.value = "";
+ elem.blur();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_time_sec_millisec_field.html b/dom/html/test/forms/test_input_time_sec_millisec_field.html
new file mode 100644
index 0000000000..71db4942a9
--- /dev/null
+++ b/dom/html/test/forms/test_input_time_sec_millisec_field.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1374967
+-->
+<head>
+ <title>Test second and millisecond fields in input type=time</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"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1374967">Mozilla Bug 1374967</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input1" type="time">
+ <input id="input2" type="time" value="12:30:40">
+ <input id="input3" type="time" value="12:30:40.567">
+ <input id="input4" type="time" step="1">
+ <input id="input5" type="time" step="61">
+ <input id="input6" type="time" step="120">
+ <input id="input7" type="time" step="0.01">
+ <input id="input8" type="time" step="0.001">
+ <input id="input9" type="time" step="1.001">
+ <input id="input10" type="time" min="01:30:05">
+ <input id="input11" type="time" min="01:30:05.100">
+ <input id="dummy">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+const NUM_OF_FIELDS_DEFAULT = 3;
+const NUM_OF_FIELDS_WITH_SECOND = NUM_OF_FIELDS_DEFAULT + 1;
+const NUM_OF_FIELDS_WITH_MILLISEC = NUM_OF_FIELDS_WITH_SECOND + 1;
+
+function countNumberOfFields(aElement) {
+ is(aElement.type, "time", "Input element type should be 'time'");
+
+ let inputRect = aElement.getBoundingClientRect();
+ let firstField_X = 15;
+ let firstField_Y = inputRect.height / 2;
+
+ // Make sure to start on the first field.
+ synthesizeMouse(aElement, firstField_X, firstField_Y, {});
+ is(document.activeElement, aElement, "Input element should be focused");
+
+ let n = 0;
+ while (document.activeElement == aElement) {
+ n++;
+ synthesizeKey("KEY_Tab");
+ }
+
+ return n;
+}
+
+function test() {
+ // Normal input time element.
+ let elem = document.getElementById("input1");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_DEFAULT, "Default input time");
+
+ // Dynamically changing the value with second part.
+ elem.value = "10:20:30";
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time after changing value with second part");
+
+ // Dynamically changing the step to 1 millisecond.
+ elem.step = "0.001";
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time after changing step to 1 millisecond");
+
+ // Input time with value with second part.
+ elem = document.getElementById("input2");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with value with second part");
+
+ // Input time with value with second and millisecond part.
+ elem = document.getElementById("input3");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with value with second and millisecond part");
+
+ // Input time with step set as 1 second.
+ elem = document.getElementById("input4");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with step set as 1 second");
+
+ // Input time with step set as 61 seconds.
+ elem = document.getElementById("input5");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with step set as 61 seconds");
+
+ // Input time with step set as 2 minutes.
+ elem = document.getElementById("input6");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_DEFAULT,
+ "Input time with step set as 2 minutes");
+
+ // Input time with step set as 10 milliseconds.
+ elem = document.getElementById("input7");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with step set as 10 milliseconds");
+
+ // Input time with step set as 100 milliseconds.
+ elem = document.getElementById("input8");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with step set as 100 milliseconds");
+
+ // Input time with step set as 1001 milliseconds.
+ elem = document.getElementById("input9");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with step set as 1001 milliseconds");
+
+ // Input time with min with second part and default step (60 seconds). Note
+ // that step base is min, when there is a min.
+ elem = document.getElementById("input10");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with min with second part");
+
+ // Input time with min with second and millisecond part and default step (60
+ // seconds). Note that step base is min, when there is a min.
+ elem = document.getElementById("input11");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with min with second and millisecond part");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_types_pref.html b/dom/html/test/forms/test_input_types_pref.html
new file mode 100644
index 0000000000..1222e88a86
--- /dev/null
+++ b/dom/html/test/forms/test_input_types_pref.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=764481
+-->
+<head>
+ <title>Test for Bug 764481</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=764481">Mozilla Bug 764481</a>
+<p id="display"></p>
+<div id="content" style="display: none" >
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+ var input = document.createElement("input");
+
+ var testData = [
+ {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "month",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "month",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", true]],
+ inputType: "month",
+ expectedType: "month"
+ }, {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "week",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "week",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", true]],
+ inputType: "week",
+ expectedType: "week"
+ }
+ ];
+
+ function testInputTypePreference(aData) {
+ return SpecialPowers.pushPrefEnv({'set': aData.prefs})
+ .then(() => {
+ // Change the type of input to text and then back to the tested input type,
+ // so that HTMLInputElement::ParseAttribute gets called with the pref enabled.
+ input.type = "text";
+ input.type = aData.inputType;
+ is(input.type, aData.expectedType, "input type should be '" +
+ aData.expectedType + "'' when pref " + aData.prefs + " is set");
+ is(input.getAttribute('type'), aData.inputType,
+ "input 'type' attribute should not change");
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ let promise = Promise.resolve();
+ for (let i = 0; i < testData.length; i++) {
+ let data = testData[i];
+ promise = promise.then(() => testInputTypePreference(data));
+ }
+
+ promise.catch(error => ok(false, "Promise reject: " + error))
+ .then(() => SimpleTest.finish());
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_typing_sanitization.html b/dom/html/test/forms/test_input_typing_sanitization.html
new file mode 100644
index 0000000000..fef0ebed06
--- /dev/null
+++ b/dom/html/test/forms/test_input_typing_sanitization.html
@@ -0,0 +1,217 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=765772
+-->
+<head>
+ <title>Test for Bug 765772</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug 765772</a>
+<p id="display"></p>
+<iframe name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content">
+ <form id='f' target="submit_frame" action="foo">
+ <input name=i id="i" step='any' >
+ </form>
+</div>
+<pre id="test">
+<script>
+
+/*
+ * This test checks that when a user types in some input types, it will not be
+ * in a state where the value will be un-sanitized and usable (by a script).
+ */
+
+var input = document.getElementById('i');
+var form = document.getElementById('f');
+var submitFrame = document.getElementsByTagName('iframe')[0];
+var testData = [];
+var gCurrentTest = null;
+var gValidData = [];
+var gInvalidData = [];
+
+function submitForm() {
+ form.submit();
+}
+
+function sendKeyEventToSubmitForm() {
+ sendKey("return");
+}
+
+function urlify(aStr) {
+ return aStr.replace(/:/g, '%3A');
+}
+
+function runTestsForNextInputType()
+{
+ let {done} = testRunner.next();
+ if (done) {
+ SimpleTest.finish();
+ }
+}
+
+function checkValueSubmittedIsValid()
+{
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?i=${urlify(gValidData[valueIndex++])}`,
+ "The submitted value should not have been sanitized");
+
+ input.value = "";
+
+ if (valueIndex >= gValidData.length) {
+ if (gCurrentTest.canHaveBadInputValidityState) {
+ // Don't run the submission tests on the invalid input if submission
+ // will be blocked by invalid input.
+ runTestsForNextInputType();
+ return;
+ }
+ valueIndex = 0;
+ submitFrame.onload = checkValueSubmittedIsInvalid;
+ testData = gInvalidData;
+ }
+ testSubmissions();
+}
+
+function checkValueSubmittedIsInvalid()
+{
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?i=`,
+ "The submitted value should have been sanitized");
+
+ valueIndex++;
+ input.value = "";
+
+ if (valueIndex >= gInvalidData.length) {
+ if (submitMethod == sendKeyEventToSubmitForm) {
+ runTestsForNextInputType();
+ return;
+ }
+ valueIndex = 0;
+ submitMethod = sendKeyEventToSubmitForm;
+ submitFrame.onload = checkValueSubmittedIsValid;
+ testData = gValidData;
+ }
+ testSubmissions();
+}
+
+function testSubmissions() {
+ input.focus();
+ sendString(testData[valueIndex]);
+ submitMethod();
+}
+
+var valueIndex = 0;
+var submitMethod = submitForm;
+
+SimpleTest.waitForExplicitFinish();
+
+function* runTest()
+{
+ SimpleTest.requestLongerTimeout(4);
+
+ var data = [
+ {
+ type: 'number',
+ canHaveBadInputValidityState: true,
+ validData: [
+ "42",
+ "-42", // should work for negative values
+ "42.1234",
+ "123.123456789123", // double precision
+ "1e2", // e should be usable
+ "2e1",
+ "1e-1", // value after e can be negative
+ "1E2", // E can be used instead of e
+ ],
+ invalidData: [
+ "e",
+ "e2",
+ "1e0.1",
+ "foo",
+ "42,13", // comma can't be used as a decimal separator
+ ]
+ },
+ {
+ type: 'month',
+ validData: [
+ '0001-01',
+ '2012-12',
+ '100000-01',
+ ],
+ invalidData: [
+ '1-01',
+ '-',
+ 'december',
+ '2012-dec',
+ '2012/12',
+ '2012-99',
+ '2012-1',
+ ]
+ },
+ {
+ type: 'week',
+ validData: [
+ '0001-W01',
+ '1970-W53',
+ '100000-W52',
+ '2016-W30',
+ ],
+ invalidData: [
+ '1-W01',
+ 'week',
+ '2016-30',
+ '2010-W80',
+ '2000/W30',
+ '1985-W00',
+ '1000-W'
+ ]
+ },
+ ];
+
+ for (test of data) {
+ gCurrentTest = test;
+
+ input.type = test.type;
+ gValidData = test.validData;
+ gInvalidData = test.invalidData;
+
+ for (data of gValidData) {
+ input.value = "";
+ input.focus();
+ sendString(data);
+ input.blur();
+ is(input.value, data, "valid user input should not be sanitized");
+ }
+
+ for (data of gInvalidData) {
+ input.value = "";
+ input.focus();
+ sendString(data);
+ input.blur();
+ is(input.value, "", "invalid user input should be sanitized");
+ }
+
+ input.value = '';
+
+ testData = gValidData;
+ valueIndex = 0;
+ submitFrame.onload = checkValueSubmittedIsValid;
+ testSubmissions();
+ yield undefined;
+ }
+}
+
+var testRunner = runTest();
+
+addLoadEvent(function () {
+ testRunner.next();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_untrusted_key_events.html b/dom/html/test/forms/test_input_untrusted_key_events.html
new file mode 100644
index 0000000000..78e35f525f
--- /dev/null
+++ b/dom/html/test/forms/test_input_untrusted_key_events.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for untrusted DOM KeyboardEvent on input element</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">
+ <input id="input">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runNextTest, window);
+
+const kTests = [
+ { type: "text", value: "foo", key: "b", expectedNewValue: "foo" },
+ { type: "number", value: "123", key: "4", expectedNewValue: "123" },
+ { type: "number", value: "123", key: KeyEvent.DOM_VK_UP, expectedNewValue: "123" },
+ { type: "number", value: "123", key: KeyEvent.DOM_VK_DOWN, expectedNewValue: "123" },
+];
+
+function sendUntrustedKeyEvent(eventType, keyCode, target) {
+ var evt = new KeyboardEvent(eventType, {
+ bubbles: true,
+ cancelable: true,
+ view: document.defaultView,
+ keyCode,
+ charCode: 0,
+ });
+ target.dispatchEvent(evt);
+}
+
+var input = document.getElementById("input");
+
+var gotEvents = {};
+
+function handleEvent(event) {
+ gotEvents[event.type] = true;
+}
+
+input.addEventListener("keydown", handleEvent);
+input.addEventListener("keyup", handleEvent);
+input.addEventListener("keypress", handleEvent);
+
+var previousTest = null;
+
+function runNextTest() {
+ if (previousTest) {
+ var msg = "For <input " + "type=" + previousTest.type + ">, ";
+ is(gotEvents.keydown, true, msg + "checking got keydown");
+ is(gotEvents.keyup, true, msg + "checking got keyup");
+ is(gotEvents.keypress, true, msg + "checking got keypress");
+ is(input.value, previousTest.expectedNewValue, msg + "checking element " +
+ " after being sent '" + previousTest.key + "' key events");
+ }
+
+ // reset flags
+ gotEvents.keydown = false;
+ gotEvents.keyup = false;
+ gotEvents.keypress = false;
+
+
+ var test = kTests.shift();
+ if (!test) {
+ SimpleTest.finish();
+ return; // We're all done
+ }
+
+ input.type = test.type;
+ input.focus(); // make sure we still have focus after type change
+ input.value = test.value;
+
+ sendUntrustedKeyEvent("keydown", test.key, input);
+ sendUntrustedKeyEvent("keyup", test.key, input);
+ sendUntrustedKeyEvent("keypress", test.key, input);
+
+ previousTest = test;
+
+ SimpleTest.executeSoon(runNextTest);
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_url.html b/dom/html/test/forms/test_input_url.html
new file mode 100644
index 0000000000..3cdf1070bb
--- /dev/null
+++ b/dom/html/test/forms/test_input_url.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tests for &lt;input type='url'&gt; validity</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input type='url' id='i' oninvalid='invalidEventHandler(event);'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Tests for <input type='url'> validity **/
+
+// More checks are done in test_bug551670.html.
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be invalid");
+ gInvalid = true;
+}
+
+function checkValidURL(element)
+{
+ info(`Checking ${element.value}\n`);
+ gInvalid = false;
+ ok(!element.validity.typeMismatch,
+ "Element should not suffer from type mismatch");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "The invalid event should not have been thrown");
+ is(element.validationMessage, '',
+ "Validation message should be the empty string");
+ ok(element.matches(":valid"), ":valid pseudo-class should apply");
+}
+
+function checkInvalidURL(element)
+{
+ gInvalid = false;
+ ok(element.validity.typeMismatch,
+ "Element should suffer from type mismatch");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+ ok(gInvalid, "The invalid event should have been thrown");
+ is(element.validationMessage, "Please enter a URL.",
+ "Validation message should be related to invalid URL");
+ ok(element.matches(":invalid"),
+ ":invalid pseudo-class should apply");
+}
+
+var url = document.getElementById('i');
+
+var values = [
+ // [ value, validity ]
+ // The empty string should be considered as valid.
+ [ "", true ],
+ [ "foo", false ],
+ [ "http://mozilla.com/", true ],
+ [ "http://mozilla.com", true ],
+ [ "http://mozil\nla\r.com/", true ],
+ [ " http://mozilla.com/ ", true ],
+ [ "\r http://mozilla.com/ \n", true ],
+ [ "file:///usr/bin/tulip", true ],
+ [ "../../bar.html", false ],
+ [ "http://mozillá.org", true ],
+ [ "https://mózillä.org", true ],
+ [ "http://mózillä.órg", true ],
+ [ "ht://mózillä.órg", true ],
+ [ "httŭ://mózillä.órg", false ],
+ [ "chrome://bookmarks", true ],
+];
+
+values.forEach(function([value, valid]) {
+ url.value = value;
+
+ if (valid) {
+ checkValidURL(url);
+ } else {
+ checkInvalidURL(url);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_interactive_content_in_label.html b/dom/html/test/forms/test_interactive_content_in_label.html
new file mode 100644
index 0000000000..b8d9c81d51
--- /dev/null
+++ b/dom/html/test/forms/test_interactive_content_in_label.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=229925
+-->
+<head>
+ <title>Test for Bug 229925</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=229925">Mozilla Bug 229925</a>
+<p id="display"></p>
+<form action="#">
+ <label>
+ <span id="text">label</span>
+ <input type="button" id="target" value="target">
+
+ <a class="yes" href="#">a</a>
+ <audio class="yes" controls></audio>
+ <button class="yes">button</button>
+ <details class="yes">details</details>
+ <embed class="yes">embed</embed>
+ <iframe class="yes" src="data:text/plain," style="width: 16px; height: 16px;"></iframe>
+ <img class="yes" src="data:image/png," usemap="#map">
+ <input class="yes" type="text" size="4">
+ <keygen class="no">
+ <label class="yes">label</label>
+ <object class="yes" usemap="#map">object</object>
+ <select class="yes"><option>select</option></select>
+ <textarea class="yes" cols="1" rows="1"></textarea>
+ <video class="yes" controls></video>
+
+ <!-- Tests related to shadow tree. -->
+ <div id="root1"> <!-- content will be added by script below. --> </div>
+ <button><div id="root2"> <!-- content will be added by script below. --> </div></button>
+
+ <a class="no">a</a>
+ <audio class="no"></audio>
+ <img class="no" src="data:image/png,">
+ <input class="no" type="hidden">
+ <object class="no">object</object>
+ <video class="no"></video>
+
+ <span class="no" tabindex="1">tabindex</span>
+ <audio class="no" tabindex="1"></audio>
+ <img class="no" src="data:image/png," tabindex="1">
+ <input class="no" type="hidden" tabindex="1">
+ <object class="no" tabindex="1">object</object>
+ <video class="no" tabindex="1"></video>
+ </label>
+</form>
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 229925 **/
+
+var target = document.getElementById("target");
+
+var yes_nodes = Array.from(document.getElementsByClassName("yes"));
+
+var root1 = document.getElementById("root1");
+root1.attachShadow({ mode: "open" }).innerHTML = "<button class=yes>button in shadow tree</button>";
+var root2 = document.getElementById("root2");
+root2.attachShadow({ mode: "open" }).innerHTML = "<div class=yes>text in shadow tree</div>";
+var yes_nodes_in_shadow_tree =
+ Array.from(root1.shadowRoot.querySelectorAll(".yes")).concat(
+ Array.from(root2.shadowRoot.querySelectorAll(".yes")));
+
+var no_nodes = Array.from(document.getElementsByClassName("no"));
+
+var target_clicked = false;
+target.addEventListener("click", function() {
+ target_clicked = true;
+});
+
+var node;
+for (node of yes_nodes) {
+ target_clicked = false;
+ node.click();
+ is(target_clicked, false, "mouse click on interactive content " + node.nodeName + " shouldn't dispatch event to label target");
+}
+
+for (node of yes_nodes_in_shadow_tree) {
+ target_clicked = false;
+ node.click();
+ is(target_clicked, false, "mouse click on content in shadow tree " + node.nodeName + " shouldn't dispatch event to label target");
+}
+
+for (node of no_nodes) {
+ target_clicked = false;
+ node.click();
+ is(target_clicked, true, "mouse click on non interactive content " + node.nodeName + " should dispatch event to label target");
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_interactive_content_in_summary.html b/dom/html/test/forms/test_interactive_content_in_summary.html
new file mode 100644
index 0000000000..f8bac77d89
--- /dev/null
+++ b/dom/html/test/forms/test_interactive_content_in_summary.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1524893
+-->
+<head>
+ <title>Test for Bug 1524893</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1524893">Mozilla Bug 1524893</a>
+
+<details id="details">
+ <summary>
+ <a class="yes" href="#">a</a>
+ <audio class="yes" controls></audio>
+ <button class="yes">button</button>
+ <details class="yes">details</details>
+ <embed class="yes">embed</embed>
+ <iframe class="yes" src="data:text/plain," style="width: 16px; height: 16px;"></iframe>
+ <img class="yes" src="data:image/png," usemap="#map">
+ <input class="yes" type="text" size="4">
+ <keygen class="no">
+ <label class="yes">label</label>
+ <object class="yes" usemap="#map">object</object>
+ <select class="yes"><option>select</option></select>
+ <textarea class="yes" cols="1" rows="1"></textarea>
+ <video class="yes" controls></video>
+
+ <!-- Tests related to shadow tree. -->
+ <div id="root1"> <!-- content will be added by script below. --> </div>
+ <button><div id="root2"> <!-- content will be added by script below. --> </div></button>
+
+ <a class="no">a</a>
+ <audio class="no"></audio>
+ <img class="no" src="data:image/png,">
+ <input class="no" type="hidden">
+ <object class="no">object</object>
+ <video class="no"></video>
+
+ <span class="no" tabindex="1">tabindex</span>
+ <audio class="no" tabindex="1"></audio>
+ <img class="no" src="data:image/png," tabindex="1">
+ <input class="no" type="hidden" tabindex="1">
+ <object class="no" tabindex="1">object</object>
+ <video class="no" tabindex="1"></video>
+ </summary>
+ <div>This is details</div>
+</details>
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1524893 **/
+
+var details = document.getElementById("details");
+
+var yes_nodes = Array.from(document.getElementsByClassName("yes"));
+
+var root1 = document.getElementById("root1");
+root1.attachShadow({ mode: "open" }).innerHTML = "<button class=yes>button in shadow tree</button>";
+var root2 = document.getElementById("root2");
+root2.attachShadow({ mode: "open" }).innerHTML = "<div class=yes>text in shadow tree</div>";
+var yes_nodes_in_shadow_tree =
+ Array.from(root1.shadowRoot.querySelectorAll(".yes")).concat(
+ Array.from(root2.shadowRoot.querySelectorAll(".yes")));
+
+var no_nodes = Array.from(document.getElementsByClassName("no"));
+
+var node;
+for (node of yes_nodes) {
+ details.removeAttribute('open');
+ node.click();
+ ok(!details.hasAttribute('open'),
+ "mouse click on interactive content " + node.nodeName + " shouldn't not open details");
+}
+
+for (node of yes_nodes_in_shadow_tree) {
+ details.removeAttribute('open');
+ node.click();
+ ok(!details.hasAttribute('open'),
+ "mouse click on content in shadow tree " + node.nodeName + " shouldn't open details");
+}
+
+for (node of no_nodes) {
+ details.removeAttribute('open');
+ node.click();
+ ok(details.hasAttribute('open'),
+ "mouse click on non interactive content " + node.nodeName + " should open details");
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_label_control_attribute.html b/dom/html/test/forms/test_label_control_attribute.html
new file mode 100644
index 0000000000..efc04cd787
--- /dev/null
+++ b/dom/html/test/forms/test_label_control_attribute.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=562932
+-->
+<head>
+ <title>Test for Bug 562932</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=562932">Mozilla Bug 562932</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- No @for, we have to check the content -->
+ <label id='l1'><input id='i1'></label>
+ <label id='l2'><input id='i2'><input></label>
+ <label id='l3'></label>
+ <label id='l4a'><fieldset id='f'>foo</fieldset></label>
+ <label id='l4b'><label id='l4c'><input id='i3'></label></label>
+ <label id='l4d'><label id='l4e'><input id='i3b'></label><input></label>
+
+ <!-- With @for, we do no check the content -->
+ <label id='l5' for='i1'></label>
+ <label id='l6' for='i4'></label>
+ <label id='l7' for='i4'><input></label>
+ <label id='l8' for='i1 i2'></label>
+ <label id='l9' for='i1 i2'><input></label>
+ <label id='l10' for='f'></label>
+ <label id='l11' for='i4'></label>
+ <label id='l12' for='i5'></label>
+ <label id='l13' for=''><input></label>
+ <!-- <label id='l14'> is created in script -->
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 562932 **/
+
+function checkControl(aLabelId, aElementId, aMsg)
+{
+ var element = null;
+
+ if (aElementId != null) {
+ element = document.getElementById(aElementId);
+ }
+
+ is(document.getElementById(aLabelId).control, element, aMsg);
+}
+
+ok('control' in document.createElement('label'),
+ "label element should have a control IDL attribute");
+
+checkControl('l1', 'i1', "label control should be the first form element");
+checkControl('l2', 'i2', "label control should be the first form element");
+checkControl('l3', null, "label control should be null when there is no child");
+checkControl('l4a', null, "label control should be null when there is no \
+ labelable form element child");
+checkControl('l4b', 'i3', "label control should be the first labelable element \
+ in tree order");
+checkControl('l4c', 'i3', "label control should be the first labelable element \
+ in tree order");
+checkControl('l4d', 'i3b', "label control should be the first labelable element \
+ in tree order");
+checkControl('l4e', 'i3b', "label control should be the first labelable element \
+ in tree order");
+checkControl('l5', 'i1', "label control should be the id in @for");
+checkControl('l6', null,
+ "label control should be null if the id in @for is not valid");
+checkControl('l7', null,
+ "label control should be null if the id in @for is not valid");
+checkControl('l8', null,
+ "label control should be null if there are more than one id in @for");
+checkControl('l9', null,
+ "label control should be null if there are more than one id in @for");
+checkControl('l10', null, "label control should be null if the id in @for \
+ is not an id from a labelable form element");
+
+var inputOutOfDocument = document.createElement('input');
+inputOutOfDocument.id = 'i4';
+checkControl('l11', null, "label control should be null if the id in @for \
+ is not an id from an element in the document");
+
+var inputInDocument = document.createElement('input');
+inputInDocument.id = 'i5';
+document.getElementById('content').appendChild(inputInDocument);
+checkControl('l12', 'i5', "label control should be the id in @for");
+
+checkControl('l13', null, "label control should be null if the id in @for \
+ is empty");
+
+var labelOutOfDocument = document.createElement('label');
+labelOutOfDocument.htmlFor = 'i1';
+is(labelOutOfDocument.control, null, "out of document label shouldn't \
+ labelize a form control");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_label_input_controls.html b/dom/html/test/forms/test_label_input_controls.html
new file mode 100644
index 0000000000..fe9410b608
--- /dev/null
+++ b/dom/html/test/forms/test_label_input_controls.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=597650
+-->
+<head>
+ <title>Test for Bug 597650</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597650">Mozilla Bug 597650</a>
+ <p id="display"></p>
+ <div id="content">
+ <label id="l">
+ <input id="h"></input>
+ <input type="text" id="i"></input>
+ </label>
+ <label id="lh" for="h"></label>
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ /** Test for Bug 597650 **/
+ label = document.getElementById("l");
+ labelForH = document.getElementById("lh");
+ inputI = document.getElementById("i");
+ inputH = document.getElementById("h");
+
+ var labelableTypes = ["text", "search", "tel", "url", "email", "password",
+ "datetime", "date", "month", "week", "time",
+ "number", "range", "color", "checkbox", "radio",
+ "file", "submit", "image", "reset", "button"];
+ var nonLabelableTypes = ["hidden"];
+
+ for (var i in labelableTypes) {
+ test(labelableTypes[i], true);
+ }
+
+ for (var i in nonLabelableTypes) {
+ test(nonLabelableTypes[i], false);
+ }
+
+ function test(type, isLabelable) {
+ inputH.type = type;
+ if (isLabelable) {
+ testControl(label, inputH, type, true);
+ testControl(labelForH, inputH, type, true);
+ } else {
+ testControl(label, inputI, type, false);
+ testControl(labelForH, null, type, false);
+
+ inputH.type = "text";
+ testControl(label, inputH, "text", true);
+ testControl(labelForH, inputH, "text", true);
+
+ inputH.type = type;
+ testControl(label, inputI, type, false);
+ testControl(labelForH, null, type, false);
+
+ label.removeChild(inputH);
+ testControl(label, inputI, "text", true);
+
+ var element = document.createElement('input');
+ element.type = type;
+ label.insertBefore(element, inputI);
+ testControl(label, inputI, "text", true);
+ }
+ }
+
+ function testControl(label, control, type, labelable) {
+ if (labelable) {
+ is(label.control, control, "Input controls of type " + type
+ + " should be labeled");
+ } else {
+ is(label.control, control, "Input controls of type " + type
+ + " should be ignored by <label>");
+ }
+ }
+ </script>
+ </pre>
+ </body>
+</html>
+
diff --git a/dom/html/test/forms/test_max_attribute.html b/dom/html/test/forms/test_max_attribute.html
new file mode 100644
index 0000000000..f6e9c9bd8e
--- /dev/null
+++ b/dom/html/test/forms/test_max_attribute.html
@@ -0,0 +1,473 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635499
+-->
+<head>
+ <title>Test for Bug 635499</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635499 **/
+
+var data = [
+ { type: 'hidden', apply: false },
+ { type: 'text', apply: false },
+ { type: 'search', apply: false },
+ { type: 'tel', apply: false },
+ { type: 'url', apply: false },
+ { type: 'email', apply: false },
+ { type: 'password', apply: false },
+ { type: 'date', apply: true },
+ { type: 'month', apply: true },
+ { type: 'week', apply: true },
+ { type: 'time', apply: true },
+ { type: 'datetime-local', apply: true },
+ { type: 'number', apply: true },
+ { type: 'range', apply: true },
+ { type: 'color', apply: false },
+ { type: 'checkbox', apply: false },
+ { type: 'radio', apply: false },
+ { type: 'file', apply: false },
+ { type: 'submit', apply: false },
+ { type: 'image', apply: false },
+ { type: 'reset', apply: false },
+ { type: 'button', apply: false },
+];
+
+var input = document.createElement("input");
+document.getElementById('content').appendChild(input);
+
+/**
+ * @aValidity - boolean indicating whether the element is expected to be valid
+ * (aElement.validity.valid is true) or not. The value passed is ignored and
+ * overridden with true if aApply is false.
+ * @aApply - boolean indicating whether the min/max attributes apply to this
+ * element type.
+ * @aRangeApply - A boolean that's set to true if the current input type is a
+ * "[candidate] for constraint validation" and it "[has] range limitations"
+ * per http://www.whatwg.org/specs/web-apps/current-work/multipage/selectors.html#selector-in-range
+ * (in other words, one of the pseudo classes :in-range and :out-of-range
+ * should apply (which, depends on aValidity)).
+ * Else (neither :in-range or :out-of-range should match) set to false.
+ */
+function checkValidity(aElement, aValidity, aApply, aRangeApply)
+{
+ aValidity = aApply ? aValidity : true;
+
+ is(aElement.validity.valid, aValidity,
+ "element validity should be " + aValidity);
+ is(aElement.validity.rangeOverflow, !aValidity,
+ "element overflow status should be " + !aValidity);
+ var overflowMsg =
+ (aElement.type == "date" || aElement.type == "time" ||
+ aElement.type == "month" || aElement.type == "week" ||
+ aElement.type == "datetime-local") ?
+ ("Please select a value that is no later than " + aElement.max + ".") :
+ ("Please select a value that is no more than " + aElement.max + ".");
+ is(aElement.validationMessage,
+ aValidity ? "" : overflowMsg, "Checking range overflow validation message");
+
+ is(aElement.matches(":valid"), aElement.willValidate && aValidity,
+ (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply");
+ is(aElement.matches(":invalid"), aElement.willValidate && !aValidity,
+ (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply");
+
+ if (!aRangeApply) {
+ ok(!aElement.matches(":in-range"), ":in-range should not match");
+ ok(!aElement.matches(":out-of-range"),
+ ":out-of-range should not match");
+ } else {
+ is(aElement.matches(":in-range"), aValidity,
+ ":in-range matches status should be " + aValidity);
+ is(aElement.matches(":out-of-range"), !aValidity,
+ ":out-of-range matches status should be " + !aValidity);
+ }
+}
+
+for (var test of data) {
+ input.type = test.type;
+ var apply = test.apply;
+
+ // The element should be valid. Range should not apply when @min and @max are
+ // undefined, except if the input type is 'range' (since that type has a
+ // default minimum and maximum).
+ if (input.type == 'range') {
+ checkValidity(input, true, apply, true);
+ } else {
+ checkValidity(input, true, apply, false);
+ }
+ checkValidity(input, true, apply, test.type == 'range');
+
+ switch (input.type) {
+ case 'hidden':
+ case 'text':
+ case 'search':
+ case 'password':
+ case 'url':
+ case 'tel':
+ case 'email':
+ case 'number':
+ case 'checkbox':
+ case 'radio':
+ case 'file':
+ case 'submit':
+ case 'reset':
+ case 'button':
+ case 'image':
+ case 'color':
+ input.max = '-1';
+ break;
+ case 'date':
+ input.max = '2012-06-27';
+ break;
+ case 'time':
+ input.max = '02:20';
+ break;
+ case 'range':
+ // range is special, since setting max to -1 will make it invalid since
+ // it's default would then be 0, meaning it suffers from overflow.
+ input.max = '-1';
+ checkValidity(input, false, apply, apply);
+ // Now make it something that won't cause an error below:
+ input.max = '10';
+ break;
+ case 'month':
+ input.max = '2016-12';
+ break;
+ case 'week':
+ input.max = '2016-W39';
+ break;
+ case 'datetime-local':
+ input.max = '2016-12-31T23:59:59';
+ break;
+ default:
+ ok(false, 'please, add a case for this new type (' + input.type + ')');
+ }
+
+ checkValidity(input, true, apply, apply);
+
+ switch (input.type) {
+ case 'text':
+ case 'hidden':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'radio':
+ case 'checkbox':
+ case 'reset':
+ case 'button':
+ case 'submit':
+ case 'image':
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'url':
+ input.value = 'http://mozilla.org';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'email':
+ input.value = 'foo@bar.com';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'file':
+ var file = new File([''], '635499_file');
+
+ SpecialPowers.wrap(input).mozSetFileArray([file]);
+ checkValidity(input, true, apply, apply);
+
+ break;
+ case 'date':
+ input.max = '2012-06-27';
+ input.value = '2012-06-26';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-27';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-28';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2012-06-30';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-07-05';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20120-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0049-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'number':
+ input.max = '2';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '3';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '5';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '42';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ // Check that we correctly convert input.max to a double in validationMessage.
+ if (input.type == 'number') {
+ input.max = "4.333333333333333333333333333333333331";
+ input.value = "5";
+ is(input.validationMessage,
+ "Please select a value that is no more than 4.33333333333333.",
+ "validation message");
+ }
+
+ break;
+ case 'range':
+ input.max = '2';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '3';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.max, "the value should have been set to max");
+
+ input.max = '5';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '42';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.max, "the value should have been set to max");
+
+ input.max = '';
+ checkValidity(input, true, apply, apply);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ // Check that we correctly convert input.max to a double in validationMessage.
+ input.step = 'any';
+ input.min = 5;
+ input.max = 0.6666666666666666;
+ input.value = 1;
+ is(input.validationMessage,
+ "Please select a value that is no more than 0.666666666666667.",
+ "validation message")
+
+ break;
+ case 'time':
+ // Don't worry about that.
+ input.step = 'any';
+
+ input.max = '10:10';
+ input.value = '10:09';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:10';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:10:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:10:00.000';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:11';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10:10:00.001';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '01:00:00.01';
+ input.value = '01:00:00.001';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '01:00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '01:00:00.1';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'month':
+ input.value = '2016-06';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2017-07';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-12';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20160-01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0049-12';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'week':
+ input.value = '2016-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-W39';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2017-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-W52';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2100-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0049-W52';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'datetime-local':
+ input.value = '2016-01-01T12:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12-31T23:59:59';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12-31T23:59:59.123';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '2017-01-01T10:00';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2017-01-01T10:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01-01T10:00:30';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01T12:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2100-01-01T12:00';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-12-31T23:59:59.999';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0050-12-31T23:59:59';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ }
+
+ // Cleaning up,
+ input.removeAttribute('max');
+ input.value = '';
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_maxlength_attribute.html b/dom/html/test/forms/test_maxlength_attribute.html
new file mode 100644
index 0000000000..bd76e277e5
--- /dev/null
+++ b/dom/html/test/forms/test_maxlength_attribute.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345624
+-->
+<head>
+ <title>Test for Bug 345624</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"/>
+ <style>
+ input, textarea { background-color: rgb(0,0,0) !important; }
+ :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; }
+ :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a>
+<p id="display"></p>
+<div id="content">
+ <input id='i'>
+ <textarea id='t'></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345624 **/
+
+/**
+ * This test is checking only tooLong related features
+ * related to constraint validation.
+ */
+
+function checkTooLongValidity(element)
+{
+ element.value = "foo";
+ ok(!element.validity.tooLong,
+ "Element should not be too long when maxlength is not set");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.maxLength = 1;
+ ok(!element.validity.tooLong,
+ "Element should not be too long unless the user edits it");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.focus();
+
+ synthesizeKey("KEY_Backspace");
+ is(element.value, "fo", "value should have changed");
+ ok(element.validity.tooLong,
+ "Element should be too long after a user edit that does not make it short enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please shorten this text to 1 characters or less (you are currently using 2 characters).",
+ "The validation message text is not correct");
+
+ synthesizeKey("KEY_Backspace");
+ is(element.value, "f", "value should have changed");
+ ok(!element.validity.tooLong,
+ "Element should not be too long after a user edit makes it short enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.maxLength = 2;
+ ok(!element.validity.tooLong,
+ "Element should remain valid if maxlength changes but maxlength > length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.maxLength = 1;
+ ok(!element.validity.tooLong,
+ "Element should remain valid if maxlength changes but maxlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.maxLength = 0;
+ ok(element.validity.tooLong,
+ "Element should become invalid if maxlength changes and maxlength < length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please shorten this text to 0 characters or less (you are currently using 1 characters).",
+ "The validation message text is not correct");
+
+ element.maxLength = 1;
+ ok(!element.validity.tooLong,
+ "Element should become valid if maxlength changes and maxlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.value = "test";
+ ok(!element.validity.tooLong,
+ "Element should stay valid after programmatic edit (even if value is too long)");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.setCustomValidity("custom message");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ is(element.validationMessage, "custom message",
+ "Custom message should be shown instead of too long one");
+}
+
+checkTooLongValidity(document.getElementById('i'));
+checkTooLongValidity(document.getElementById('t'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_meter_element.html b/dom/html/test/forms/test_meter_element.html
new file mode 100644
index 0000000000..5e1073d53d
--- /dev/null
+++ b/dom/html/test/forms/test_meter_element.html
@@ -0,0 +1,376 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=657938
+-->
+<head>
+ <title>Test for <meter></title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657938">Mozilla Bug 657938</a>
+<p id="display"></p>
+<iframe name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="visibility: hidden;">
+ <form id='f' method='get' target='submit_frame' action='foo'>
+ <meter id='m' value=0.5></meter>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for <meter> **/
+
+function checkFormIDLAttribute(aElement)
+{
+ is('form' in aElement, false, "<meter> shouldn't have a form attribute");
+}
+
+function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL)
+{
+ var expectedValueForIDL = aNewValue;
+ var expectedValueForContent = String(aNewValue);
+
+ if (aExpectedValueForIDL !== undefined) {
+ expectedValueForIDL = aExpectedValueForIDL;
+ }
+
+ if (aNewValue != null) {
+ aElement.setAttribute(aAttribute, aNewValue);
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+
+ if (parseFloat(aNewValue) == aNewValue) {
+ aElement[aAttribute] = aNewValue;
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], parseFloat(expectedValueForIDL),
+ aAttribute + " IDL attribute should be " + parseFloat(expectedValueForIDL));
+ }
+ } else {
+ aElement.removeAttribute(aAttribute);
+ is(aElement.getAttribute(aAttribute), null,
+ aAttribute + " content attribute should be null");
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+ }
+}
+
+function checkValueAttribute()
+{
+ var tests = [
+ // value has to be a valid float, its default value is 0.0 otherwise.
+ [ null, 0.0 ],
+ [ 'foo', 0.0 ],
+ // If value < 0.0, 0.0 is used instead.
+ [ -1.0, 0.0 ],
+ // If value >= max, max is used instead (max default value is 1.0).
+ [ 2.0, 1.0 ],
+ [ 1.0, 0.5, 0.5 ],
+ [ 10.0, 5.0, 5.0 ],
+ [ 13.37, 13.37, 42.0 ],
+ // If value <= min, min is used instead (min default value is 0.0).
+ [ 0.5, 1.0, 10.0 ,1.0 ],
+ [ 10.0, 13.37, 42.0 , 13.37],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('max', test[2]);
+ }
+
+ if (test[3]) {
+ element.setAttribute('min', test[3]);
+ }
+
+ checkAttribute(element, 'value', test[0], test[1]);
+
+ element.removeAttribute('max');
+ element.removeAttribute('min');
+ }
+}
+
+function checkMinAttribute()
+{
+ var tests = [
+ // min default value is 0.0.
+ [ null, 0.0 ],
+ [ 'foo', 0.0 ],
+ // Regular reflection.
+ [ 0.5 ],
+ [ 1.0 ],
+ [ 2.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ checkAttribute(element, 'min', test[0], test[1]);
+ }
+}
+
+function checkMaxAttribute()
+{
+ var tests = [
+ // max default value is 1.0.
+ [ null, 1.0 ],
+ [ 'foo', 1.0 ],
+ // If value <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0 ],
+ [ 42, 42, 13.37 ],
+ // Regular reflection.
+ [ 0.5 ],
+ [ 1.0 ],
+ [ 2.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+
+ checkAttribute(element, 'max', test[0], test[1]);
+
+ element.removeAttribute('min');
+ }
+}
+
+function checkLowAttribute()
+{
+ var tests = [
+ // low default value is min (min default value is 0.0).
+ [ null, 0.0 ],
+ [ 'foo', 0.0 ],
+ [ 'foo', 1.0, 1.0],
+ // If low <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0, 42.0 ],
+ [ 42.0, 42.0, 13.37, 100.0 ],
+ // If low >= max, max is used instead.
+ [ 2.0, 1.0 ],
+ [ 10.0, 5.0 , 0.5, 5.0 ],
+ [ 13.37, 13.37, 0.0, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+ if (test[3]) {
+ element.setAttribute('max', test[3]);
+ }
+
+ checkAttribute(element, 'low', test[0], test[1]);
+
+ element.removeAttribute('min');
+ element.removeAttribute('max');
+ }
+}
+
+function checkHighAttribute()
+{
+ var tests = [
+ // high default value is max (max default value is 1.0).
+ [ null, 1.0 ],
+ [ 'foo', 1.0 ],
+ [ 'foo', 42.0, 0.0, 42.0],
+ // If high <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0, 42.0 ],
+ [ 42.0, 42.0, 13.37, 100.0 ],
+ // If high >= max, max is used instead.
+ [ 2.0, 1.0 ],
+ [ 10.0, 5.0 , 0.5, 5.0 ],
+ [ 13.37, 13.37, 0.0, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+ if (test[3]) {
+ element.setAttribute('max', test[3]);
+ }
+
+ checkAttribute(element, 'high', test[0], test[1]);
+
+ element.removeAttribute('min');
+ element.removeAttribute('max');
+ }
+}
+
+function checkOptimumAttribute()
+{
+ var tests = [
+ // opt default value is (max-min)/2 (thus default value is 0.5).
+ [ null, 0.5 ],
+ [ 'foo', 0.5 ],
+ [ 'foo', 2.0, 1.0, 3.0],
+ // If opt <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0, 42.0 ],
+ [ 42.0, 42.0, 13.37, 100.0 ],
+ // If opt >= max, max is used instead.
+ [ 2.0, 1.0 ],
+ [ 10.0, 5.0 , 0.5, 5.0 ],
+ [ 13.37, 13.37, 0.0, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+ if (test[3]) {
+ element.setAttribute('max', test[3]);
+ }
+
+ checkAttribute(element, 'optimum', test[0], test[1]);
+
+ element.removeAttribute('min');
+ element.removeAttribute('max');
+ }
+}
+
+function checkFormListedElement(aElement)
+{
+ is(document.forms[0].elements.length, 0, "the form should have no element");
+}
+
+function checkLabelable(aElement)
+{
+ var content = document.getElementById('content');
+ var label = document.createElement('label');
+
+ content.appendChild(label);
+ label.appendChild(aElement);
+ is(label.control, aElement, "meter should be labelable");
+
+ // Cleaning-up.
+ content.removeChild(label);
+ content.appendChild(aElement);
+}
+
+function checkNotResetableAndFormSubmission(aElement)
+{
+ // Creating an input element to check the submission worked.
+ var form = document.forms[0];
+ var input = document.createElement('input');
+
+ input.name = 'a';
+ input.value = 'tulip';
+ form.appendChild(input);
+
+ // Setting values.
+ aElement.value = 42.0;
+ aElement.max = 100.0;
+
+ document.getElementsByName('submit_frame')[0].addEventListener("load", function() {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?a=tulip`,
+ "The meter element value should not be submitted");
+
+ checkNotResetable();
+ }, {once: true});
+
+ form.submit();
+}
+
+function checkNotResetable()
+{
+ // Try to reset the form.
+ var form = document.forms[0];
+ var element = document.getElementById('m');
+
+ element.value = 3.0;
+ element.max = 42.0;
+
+ form.reset();
+
+ SimpleTest.executeSoon(function() {
+ is(element.value, 3.0, "meter.value should not have changed");
+ is(element.max, 42.0, "meter.max should not have changed");
+
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var m = document.getElementById('m');
+
+ok(m instanceof HTMLMeterElement,
+ "The meter element should be instance of HTMLMeterElement");
+is(m.constructor, HTMLMeterElement,
+ "The meter element constructor should be HTMLMeterElement");
+
+// There is no such attribute.
+checkFormIDLAttribute(m);
+
+checkValueAttribute();
+
+checkMinAttribute();
+
+checkMaxAttribute();
+
+checkLowAttribute();
+
+checkHighAttribute();
+
+checkOptimumAttribute();
+
+checkFormListedElement(m);
+
+checkLabelable(m);
+
+checkNotResetableAndFormSubmission(m);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_meter_pseudo-classes.html b/dom/html/test/forms/test_meter_pseudo-classes.html
new file mode 100644
index 0000000000..e317a58405
--- /dev/null
+++ b/dom/html/test/forms/test_meter_pseudo-classes.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660238
+-->
+<head>
+ <title>Test for Bug 660238</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=770238">Mozilla Bug 660238</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 660238 **/
+
+function checkOptimum(aElement, aValue, aOptimum, expectedResult)
+{
+ var errorString = expectedResult
+ ? "value attribute should be in the optimum region"
+ : "value attribute should not be in the optimum region";
+
+ aElement.setAttribute('value', aValue);
+ aElement.setAttribute('optimum', aOptimum);
+ is(aElement.matches(":-moz-meter-optimum"),
+ expectedResult, errorString);
+}
+
+function checkSubOptimum(aElement, aValue, aOptimum, expectedResult)
+{
+ var errorString = "value attribute should be in the suboptimal region";
+ if (!expectedResult) {
+ errorString = "value attribute should not be in the suboptimal region";
+ }
+ aElement.setAttribute('value', aValue);
+ aElement.setAttribute('optimum', aOptimum);
+ is(aElement.matches(":-moz-meter-sub-optimum"),
+ expectedResult, errorString);
+}
+
+function checkSubSubOptimum(aElement, aValue, aOptimum, expectedResult)
+{
+ var errorString = "value attribute should be in the sub-suboptimal region";
+ if (!expectedResult) {
+ errorString = "value attribute should not be in the sub-suboptimal region";
+ }
+ aElement.setAttribute('value', aValue);
+ aElement.setAttribute('optimum', aOptimum);
+ is(aElement.matches(":-moz-meter-sub-sub-optimum"),
+ expectedResult, errorString);
+}
+
+function checkMozMatchesSelector()
+{
+ var element = document.createElement('meter');
+ // all tests realised with default values for min and max (0 and 1)
+ // low = 0.3 and high = 0.7
+ element.setAttribute('low', 0.3);
+ element.setAttribute('high', 0.7);
+
+ var tests = [
+ /*
+ * optimum = 0.0 =>
+ * optimum region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * sub-suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.0, true, false, false ],
+ [ 0.1, 0.0, true, false, false ],
+ [ 0.3, 0.0, false, true, false ],
+ [ 0.5, 0.0, false, true, false ],
+ [ 0.7, 0.0, false, true, false ],
+ [ 0.8, 0.0, false, false, true ],
+ [ 1.0, 0.0, false, false, true ],
+ /*
+ * optimum = 0.1 =>
+ * optimum region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * sub-suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.1, true, false, false ],
+ [ 0.1, 0.1, true, false, false ],
+ [ 0.3, 0.1, false, true, false ],
+ [ 0.5, 0.1, false, true, false ],
+ [ 0.7, 0.1, false, true, false ],
+ [ 0.8, 0.1, false, false, true ],
+ [ 1.0, 0.1, false, false, true ],
+ /*
+ * optimum = 0.3 =>
+ * suboptimal region = [ 0.0, 0.3 [
+ * optimum region = [ 0.3, 0.7 ]
+ * suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.3, false, true, false ],
+ [ 0.1, 0.3, false, true, false ],
+ [ 0.3, 0.3, true, false, false ],
+ [ 0.5, 0.3, true, false, false ],
+ [ 0.7, 0.3, true, false, false ],
+ [ 0.8, 0.3, false, true, false ],
+ [ 1.0, 0.3, false, true, false ],
+ /*
+ * optimum = 0.5 =>
+ * suboptimal region = [ 0.0, 0.3 [
+ * optimum region = [ 0.3, 0.7 ]
+ * suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.5, false, true, false ],
+ [ 0.1, 0.5, false, true, false ],
+ [ 0.3, 0.5, true, false, false ],
+ [ 0.5, 0.5, true, false, false ],
+ [ 0.7, 0.5, true, false, false ],
+ [ 0.8, 0.5, false, true, false ],
+ [ 1.0, 0.5, false, true, false ],
+ /*
+ * optimum = 0.7 =>
+ * suboptimal region = [ 0.0, 0.3 [
+ * optimum region = [ 0.3, 0.7 ]
+ * suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.7, false, true, false ],
+ [ 0.1, 0.7, false, true, false ],
+ [ 0.3, 0.7, true, false, false ],
+ [ 0.5, 0.7, true, false, false ],
+ [ 0.7, 0.7, true, false, false ],
+ [ 0.8, 0.7, false, true, false ],
+ [ 1.0, 0.7, false, true, false ],
+ /*
+ * optimum = 0.8 =>
+ * sub-suboptimal region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * optimum region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.8, false, false, true ],
+ [ 0.1, 0.8, false, false, true ],
+ [ 0.3, 0.8, false, true, false ],
+ [ 0.5, 0.8, false, true, false ],
+ [ 0.7, 0.8, false, true, false ],
+ [ 0.8, 0.8, true, false, false ],
+ [ 1.0, 0.8, true, false, false ],
+ /*
+ * optimum = 1.0 =>
+ * sub-suboptimal region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * optimum region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 1.0, false, false, true ],
+ [ 0.1, 1.0, false, false, true ],
+ [ 0.3, 1.0, false, true, false ],
+ [ 0.5, 1.0, false, true, false ],
+ [ 0.7, 1.0, false, true, false ],
+ [ 0.8, 1.0, true, false, false ],
+ [ 1.0, 1.0, true, false, false ],
+ ];
+
+ for (var test of tests) {
+ checkOptimum(element, test[0], test[1], test[2]);
+ checkSubOptimum(element, test[0], test[1], test[3]);
+ checkSubSubOptimum(element, test[0], test[1], test[4]);
+ }
+}
+
+checkMozMatchesSelector();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_min_attribute.html b/dom/html/test/forms/test_min_attribute.html
new file mode 100644
index 0000000000..a603a37d29
--- /dev/null
+++ b/dom/html/test/forms/test_min_attribute.html
@@ -0,0 +1,473 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635553
+-->
+<head>
+ <title>Test for Bug 635553</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635553 **/
+
+var data = [
+ { type: 'hidden', apply: false },
+ { type: 'text', apply: false },
+ { type: 'search', apply: false },
+ { type: 'tel', apply: false },
+ { type: 'url', apply: false },
+ { type: 'email', apply: false },
+ { type: 'password', apply: false },
+ { type: 'date', apply: true },
+ { type: 'month', apply: true },
+ { type: 'week', apply: true },
+ { type: 'time', apply: true },
+ { type: 'datetime-local', apply: true },
+ { type: 'number', apply: true },
+ { type: 'range', apply: true },
+ { type: 'color', apply: false },
+ { type: 'checkbox', apply: false },
+ { type: 'radio', apply: false },
+ { type: 'file', apply: false },
+ { type: 'submit', apply: false },
+ { type: 'image', apply: false },
+ { type: 'reset', apply: false },
+ { type: 'button', apply: false },
+];
+
+var input = document.createElement("input");
+document.getElementById('content').appendChild(input);
+
+/**
+ * @aValidity - boolean indicating whether the element is expected to be valid
+ * (aElement.validity.valid is true) or not. The value passed is ignored and
+ * overridden with true if aApply is false.
+ * @aApply - boolean indicating whether the min/max attributes apply to this
+ * element type.
+ * @aRangeApply - A boolean that's set to true if the current input type is a
+ * "[candidate] for constraint validation" and it "[has] range limitations"
+ * per http://www.whatwg.org/specs/web-apps/current-work/multipage/selectors.html#selector-in-range
+ * (in other words, one of the pseudo classes :in-range and :out-of-range
+ * should apply (which, depends on aValidity)).
+ * Else (neither :in-range or :out-of-range should match) set to false.
+ */
+function checkValidity(aElement, aValidity, aApply, aRangeApply)
+{
+ aValidity = aApply ? aValidity : true;
+
+ is(aElement.validity.valid, aValidity,
+ "element validity should be " + aValidity);
+ is(aElement.validity.rangeUnderflow, !aValidity,
+ "element underflow status should be " + !aValidity);
+ var underflowMsg =
+ (aElement.type == "date" || aElement.type == "time" ||
+ aElement.type == "month" || aElement.type == "week" ||
+ aElement.type == "datetime-local") ?
+ ("Please select a value that is no earlier than " + aElement.min + ".") :
+ ("Please select a value that is no less than " + aElement.min + ".");
+ is(aElement.validationMessage,
+ aValidity ? "" : underflowMsg, "Checking range underflow validation message");
+
+ is(aElement.matches(":valid"), aElement.willValidate && aValidity,
+ (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply");
+ is(aElement.matches(":invalid"), aElement.willValidate && !aValidity,
+ (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply");
+
+ if (!aRangeApply) {
+ ok(!aElement.matches(":in-range"), ":in-range should not match");
+ ok(!aElement.matches(":out-of-range"),
+ ":out-of-range should not match");
+ } else {
+ is(aElement.matches(":in-range"), aValidity,
+ ":in-range matches status should be " + aValidity);
+ is(aElement.matches(":out-of-range"), !aValidity,
+ ":out-of-range matches status should be " + !aValidity);
+ }
+}
+
+for (var test of data) {
+ input.type = test.type;
+ var apply = test.apply;
+
+ if (test.todo) {
+ todo_is(input.type, test.type, test.type + " isn't implemented yet");
+ continue;
+ }
+
+ // The element should be valid. Range should not apply when @min and @max are
+ // undefined, except if the input type is 'range' (since that type has a
+ // default minimum and maximum).
+ if (input.type == 'range') {
+ checkValidity(input, true, apply, true);
+ } else {
+ checkValidity(input, true, apply, false);
+ }
+
+ switch (input.type) {
+ case 'hidden':
+ case 'text':
+ case 'search':
+ case 'password':
+ case 'url':
+ case 'tel':
+ case 'email':
+ case 'number':
+ case 'checkbox':
+ case 'radio':
+ case 'file':
+ case 'submit':
+ case 'reset':
+ case 'button':
+ case 'image':
+ case 'color':
+ input.min = '999';
+ break;
+ case 'date':
+ input.min = '2012-06-27';
+ break;
+ case 'time':
+ input.min = '20:20';
+ break;
+ case 'range':
+ // range is special, since setting min to 999 will make it invalid since
+ // it's default maximum is 100, its value would be 999, and it would
+ // suffer from overflow.
+ break;
+ case 'month':
+ input.min = '2016-06';
+ break;
+ case 'week':
+ input.min = '2016-W39';
+ break;
+ case 'datetime-local':
+ input.min = '2017-01-01T00:00';
+ break;
+ default:
+ ok(false, 'please, add a case for this new type (' + input.type + ')');
+ }
+
+ // The element should still be valid and range should apply if it can.
+ checkValidity(input, true, apply, apply);
+
+ switch (input.type) {
+ case 'text':
+ case 'hidden':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'radio':
+ case 'checkbox':
+ case 'reset':
+ case 'button':
+ case 'submit':
+ case 'image':
+ case 'color':
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'url':
+ input.value = 'http://mozilla.org';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'email':
+ input.value = 'foo@bar.com';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'file':
+ var file = new File([''], '635499_file');
+
+ SpecialPowers.wrap(input).mozSetFileArray([file]);
+ checkValidity(input, true, apply, apply);
+
+ break;
+ case 'date':
+ input.value = '2012-06-28';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-27';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-26';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2012-02-29';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-02-28';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '20120-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0050-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0049-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'number':
+ input.min = '0';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-1';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '-1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-42';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+
+ // Check that we correctly convert input.min to a double in
+ // validationMessage.
+ input.min = "4.333333333333333333333333333333333331";
+ input.value = "2";
+ is(input.validationMessage,
+ "Please select a value that is no less than 4.33333333333333.",
+ "validation message");
+ break;
+ case 'range':
+ input.min = '0';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-1';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.min, "the value should have been set to min");
+
+ input.min = '-1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-42';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.min, "the value should have been set to min");
+
+ input.min = '';
+ checkValidity(input, true, apply, true);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, true);
+
+ // We don't check the conversion of input.min to a double in
+ // validationMessage for 'range' since range will always clamp the value
+ // up to at least the minimum (so we will never see the min in a
+ // validationMessage).
+
+ break;
+ case 'time':
+ // Don't worry about that.
+ input.step = 'any';
+
+ input.min = '20:20';
+ input.value = '20:20:01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:00';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '20:20:00.001';
+ input.value = '20:20';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '00:00';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '23:59';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:00.01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:00.1';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '00:00:00';
+ input.value = '01:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '00:00:00.000';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'month':
+ input.value = '2016-07';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-06';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-05';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2016-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2015-12';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10000-01';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0010-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0001-01';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'week':
+ input.value = '2016-W40';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-W39';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-W38';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2016-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2015-W53';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10000-01';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0010-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0001-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'datetime-local':
+ input.value = '2017-12-31T23:59';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01-01T00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01-01T00:00:00.123';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12-31T23:59';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2016-01-01T00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2015-12-31T23:59';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01T00:00';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10000-01-01T00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0010-01-01T12:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0010-01-01T10:00';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ default:
+ ok(false, 'write tests for ' + input.type);
+ }
+
+ // Cleaning up,
+ input.removeAttribute('min');
+ input.value = '';
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_minlength_attribute.html b/dom/html/test/forms/test_minlength_attribute.html
new file mode 100644
index 0000000000..154343a512
--- /dev/null
+++ b/dom/html/test/forms/test_minlength_attribute.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345624
+-->
+<head>
+ <title>Test for Bug 345624</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"/>
+ <style>
+ input, textarea { background-color: rgb(0,0,0) !important; }
+ :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; }
+ :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a>
+<p id="display"></p>
+<div id="content">
+ <input id='i'>
+ <textarea id='t'></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345624 **/
+
+/**
+ * This test is checking only tooShort related features
+ * related to constraint validation.
+ */
+
+function checkTooShortValidity(element)
+{
+ element.value = "foo";
+ ok(!element.validity.tooShort,
+ "Element should not be too short when minlength is not set");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.minLength = 5;
+ ok(!element.validity.tooShort,
+ "Element should not be too short unless the user edits it");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.focus();
+
+ sendString("o");
+ is(element.value, "fooo", "value should have changed");
+ ok(element.validity.tooShort,
+ "Element should be too short after a user edit that does not make it short enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please use at least 5 characters (you are currently using 4 characters).",
+ "The validation message text is not correct");
+
+ sendString("o");
+ is(element.value, "foooo", "value should have changed");
+ ok(!element.validity.tooShort,
+ "Element should not be too short after a user edit makes it long enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.minLength = 2;
+ ok(!element.validity.tooShort,
+ "Element should remain valid if minlength changes but minlength < length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.minLength = 1;
+ ok(!element.validity.tooShort,
+ "Element should remain valid if minlength changes but minlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.minLength = 6;
+ ok(element.validity.tooShort,
+ "Element should become invalid if minlength changes and minlength > length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please use at least 6 characters (you are currently using 5 characters).",
+ "The validation message text is not correct");
+
+ element.minLength = 5;
+ ok(!element.validity.tooShort,
+ "Element should become valid if minlength changes and minlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.value = "test";
+ ok(!element.validity.tooShort,
+ "Element should stay valid after programmatic edit (even if value is too short)");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.setCustomValidity("custom message");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ is(element.validationMessage, "custom message",
+ "Custom message should be shown instead of too short one");
+}
+
+checkTooShortValidity(document.getElementById('i'));
+checkTooShortValidity(document.getElementById('t'));
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_mozistextfield.html b/dom/html/test/forms/test_mozistextfield.html
new file mode 100644
index 0000000000..3f92a3d05d
--- /dev/null
+++ b/dom/html/test/forms/test_mozistextfield.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=565538
+-->
+<head>
+ <title>Test for Bug 565538</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565538">Mozilla Bug 565538</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 565538 **/
+
+var gElementTestData = [
+/* element result */
+ ['input', true],
+ ['button', false],
+ ['fieldset', false],
+ ['label', false],
+ ['option', false],
+ ['optgroup', false],
+ ['output', false],
+ ['legend', false],
+ ['select', false],
+ ['textarea', false],
+ ['object', false],
+];
+
+var gInputTestData = [
+/* type result */
+ ['password', true],
+ ['tel', true],
+ ['text', true],
+ ['button', false],
+ ['checkbox', false],
+ ['file', false],
+ ['hidden', false],
+ ['reset', false],
+ ['image', false],
+ ['radio', false],
+ ['submit', false],
+ ['search', true],
+ ['email', true],
+ ['url', true],
+ ['number', false],
+ ['range', false],
+ ['date', false],
+ ['time', false],
+ ['color', false],
+ ['month', false],
+ ['week', false],
+ ['datetime-local', false],
+];
+
+function checkMozIsTextFieldDefined(aElement, aResult)
+{
+ var element = document.createElement(aElement);
+
+ var msg = "mozIsTextField should be "
+ if (aResult) {
+ msg += "defined";
+ } else {
+ msg += "undefined";
+ }
+
+ is('mozIsTextField' in element, aResult, msg);
+}
+
+function checkMozIsTextFieldValue(aInput, aResult)
+{
+ is(aInput.mozIsTextField(false), aResult,
+ "mozIsTextField(false) should return " + aResult);
+
+ if (aInput.type == 'password') {
+ ok(!aInput.mozIsTextField(true),
+ "mozIsTextField(true) should return false for password");
+ } else {
+ is(aInput.mozIsTextField(true), aResult,
+ "mozIsTextField(true) should return " + aResult);
+ }
+}
+
+function checkMozIsTextFieldValueTodo(aInput, aResult)
+{
+ todo_is(aInput.mozIsTextField(false), aResult,
+ "mozIsTextField(false) should return " + aResult);
+ todo_is(aInput.mozIsTextField(true), aResult,
+ "mozIsTextField(true) should return " + aResult);
+}
+
+// Check if the method is defined for the correct elements.
+for (data of gElementTestData) {
+ checkMozIsTextFieldDefined(data[0], data[1]);
+}
+
+// Check if the method returns the correct value.
+var input = document.createElement('input');
+for (data of gInputTestData) {
+ input.type = data[0];
+ checkMozIsTextFieldValue(input, data[1]);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_novalidate_attribute.html b/dom/html/test/forms/test_novalidate_attribute.html
new file mode 100644
index 0000000000..dcea207838
--- /dev/null
+++ b/dom/html/test/forms/test_novalidate_attribute.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=556013
+-->
+<head>
+ <title>Test for Bug 556013</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556013">Mozilla Bug 556013</a>
+<p id="display"></p>
+<iframe style='width:50px; height: 50px;' name='t'></iframe>
+<div id="content">
+ <form target='t' action='data:text/html,' novalidate>
+ <input id='av' required>
+ <input id='a' type='submit'>
+ </form>
+ <form target='t' action='data:text/html,' novalidate>
+ <input id='bv' type='checkbox' required>
+ <button id='b' type='submit'></button>
+ </form>
+ <form target='t' action='data:text/html,' novalidate>
+ <input id='c' required>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 556013 **/
+
+/**
+ * novalidate should prevent form validation, thus not blocking form submission.
+ *
+ * NOTE: if the MozInvalidForm event doesn't get prevented default, the form
+ * submission will never be blocked and this test might be a false-positive but
+ * that should not be a problem. We will remove the check for MozInvalidForm
+ * event, see bug 587671.
+ */
+document.forms[0].addEventListener("submit", function(aEvent) {
+ ok(true, "novalidate has been correctly used for first form");
+ document.getElementById('b').click();
+}, {once: true});
+
+document.forms[1].addEventListener("submit", function(aEvent) {
+ ok(true, "novalidate has been correctly used for second form");
+ var c = document.getElementById('c');
+ c.focus();
+ synthesizeKey("KEY_Enter");
+}, {once: true});
+
+document.forms[2].addEventListener("submit", function(aEvent) {
+ ok(true, "novalidate has been correctly used for third form");
+ SimpleTest.executeSoon(SimpleTest.finish);
+}, {once: true});
+
+/**
+ * We have to be sure invalid events are not send too.
+ * They should be sent before the submit event so we can just create a test
+ * failure if we got one. All of them should be catched if sent.
+ * At worst, we got random green which isn't harmful.
+ */
+function invalidHandling(aEvent)
+{
+ aEvent.target.removeEventListener("invalid", invalidHandling);
+ ok(false, "invalid event should not be sent");
+}
+
+document.getElementById('av').addEventListener("invalid", invalidHandling);
+document.getElementById('bv').addEventListener("invalid", invalidHandling);
+document.getElementById('c').addEventListener("invalid", invalidHandling);
+
+SimpleTest.waitForExplicitFinish();
+
+// This is going to call all the tests (with a chain reaction).
+SimpleTest.waitForFocus(function() {
+ document.getElementById('a').click();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_option_disabled.html b/dom/html/test/forms/test_option_disabled.html
new file mode 100644
index 0000000000..421e4546be
--- /dev/null
+++ b/dom/html/test/forms/test_option_disabled.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=759666
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for HTMLOptionElement disabled attribute and pseudo-class</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=759666">Mozilla Bug 759666</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLOptionElement disabled attribute and pseudo-class **/
+
+var testCases = [
+ // Static checks.
+ { html: "<option></option>",
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<option disabled></option>",
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup><option></option></otpgroup>",
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup><option disabled></option></optgroup>",
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><option></option></optgroup>",
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>",
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>",
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ result: { attr: "", idl: true, pseudo: true } },
+
+ // Dynamic checks: changing disable value.
+ { html: "<option></option>",
+ modifier(c) { c.querySelector('option').disabled = true; },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<option disabled></option>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup><option></option></otpgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = true; },
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = false; },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><option></option></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('optgroup[disabled]').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('optgroup[disabled]').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = false; },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+
+ // Dynamic checks: moving option element.
+ { html: "<optgroup id='a'><option></option></optgroup><optgroup id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup id='a'><option disabled></option></optgroup><optgroup id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup id='a'><option></option></optgroup><optgroup disabled id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup disabled id='a'><option></option></optgroup><optgroup id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: null, idl: false, pseudo: false } },
+];
+
+var content = document.getElementById('content');
+
+testCases.forEach(function(testCase) {
+ var result = testCase.result;
+
+ content.innerHTML = testCase.html;
+
+ if (testCase.modifier !== undefined) {
+ testCase.modifier(content);
+ }
+
+ var option = content.querySelector('option');
+ is(option.getAttribute('disabled'), result.attr, "disabled content attribute value should be " + result.attr);
+ is(option.disabled, result.idl, "disabled idl attribute value should be " + result.idl);
+ is(option.matches(":disabled"), result.pseudo, ":disabled state should be " + result.pseudo);
+ is(option.matches(":enabled"), !result.pseudo, ":enabled state should be " + !result.pseudo);
+
+ content.innerHTML = "";
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_option_index_attribute.html b/dom/html/test/forms/test_option_index_attribute.html
new file mode 100644
index 0000000000..f15520e5e6
--- /dev/null
+++ b/dom/html/test/forms/test_option_index_attribute.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+See those bugs:
+https://bugzilla.mozilla.org/show_bug.cgi?id=720385
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for option.index</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=720385">Mozilla Bug 720385</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <datalist>
+ <option></option>
+ <option></option>
+ </datalist>
+ <select>
+ <option></option>
+ <foo>
+ <option></option>
+ <optgroup>
+ <option></option>
+ </optgroup>
+ <option></option>
+ </foo>
+ <option></option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 720385 **/
+
+var initialIndexes = [ 0, 0, 0, 1, 2, 3, 4 ];
+var options = document.getElementsByTagName('option');
+
+is(options.length, initialIndexes.length,
+ "Must have " + initialIndexes.length +" options");
+
+for (var i=0; i<options.length; ++i) {
+ is(options[i].index, initialIndexes[i], "test");
+}
+
+var o = document.createElement('option');
+is(o.index, 0, "option outside of a document have index=0");
+
+document.body.appendChild(o);
+is(o.index, 0, "option outside of a select have index=0");
+
+var datalist = document.getElementsByTagName('datalist')[0];
+
+datalist.appendChild(o);
+is(o.index, 0, "option outside of a select have index=0");
+
+datalist.removeChild(o);
+is(o.index, 0, "option outside of a select have index=0");
+
+var select = document.getElementsByTagName('select')[0];
+
+select.appendChild(o);
+is(o.index, 5, "option inside a select have an index");
+
+select.removeChild(select.options[0]);
+is(o.index, 4, "option inside a select have an index");
+
+select.insertBefore(o, select.options[0]);
+is(o.index, 0, "option inside a select have an index");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_option_text.html b/dom/html/test/forms/test_option_text.html
new file mode 100644
index 0000000000..3afe3e786a
--- /dev/null
+++ b/dom/html/test/forms/test_option_text.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>HTMLOptionElement.text</title>
+<link rel=author title=Ms2ger href="mailto:Ms2ger@gmail.com">
+<link rel=help href="http://www.whatwg.org/html/#dom-option-text">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createElement("font"))
+ .appendChild(document.createTextNode(" font "));
+ assert_equals(option.text, "font");
+}, "option.text should recurse");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElement("script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before after");
+}, "option.text should not recurse into HTML script elements");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before after");
+}, "option.text should not recurse into SVG script elements");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElementNS("http://www.w3.org/1998/Math/MathML", "script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before script after");
+}, "option.text should recurse into MathML script elements");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElementNS(null, "script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before script after");
+}, "option.text should recurse into null script elements");
+test(function() {
+ var option = document.createElement("option");
+ var span = option.appendChild(document.createElement("span"));
+ span.appendChild(document.createTextNode(" Some "));
+ span.appendChild(document.createElement("script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" Text "));
+ assert_equals(option.text, "Some Text");
+}, "option.text should work if a child of the option ends with a script");
+</script>
diff --git a/dom/html/test/forms/test_output_element.html b/dom/html/test/forms/test_output_element.html
new file mode 100644
index 0000000000..ab11443d83
--- /dev/null
+++ b/dom/html/test/forms/test_output_element.html
@@ -0,0 +1,182 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=346485
+-->
+<head>
+ <title>Test for Bug 346485</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ frameLoaded = function() {
+ is(frames.submit_frame.location.href, "about:blank",
+ "Blank frame loaded");
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=346485">Mozilla Bug 346485</a>
+<p id="display"></p>
+<iframe name="submit_frame" onload="frameLoaded()" style="visibility: hidden;"></iframe>
+<div id="content" style="display: none">
+ <form id='f' method='get' target='submit_frame' action='foo'>
+ <input name='a' id='a'>
+ <input name='b' id='b'>
+ <output id='o' for='a b' name='output-name'>tulip</output>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 346485 **/
+
+function checkNameAttribute(element)
+{
+ is(element.name, "output-name", "Output name IDL attribute is not correct");
+ is(element.getAttribute('name'), "output-name",
+ "Output name content attribute is not correct");
+}
+
+function checkValueAndDefaultValueIDLAttribute(element)
+{
+ is(element.value, element.textContent,
+ "The value IDL attribute should act like the textContent IDL attribute");
+
+ element.value = "foo";
+ is(element.value, "foo", "Value should be 'foo'");
+
+ is(element.defaultValue, "tulip", "Default defaultValue is 'tulip'");
+
+ element.defaultValue = "bar";
+ is(element.defaultValue, "bar", "defaultValue should be 'bar'");
+
+ // More complex situation.
+ element.textContent = 'foo';
+ var b = document.createElement('b');
+ b.textContent = 'bar'
+ element.appendChild(b);
+ is(element.value, element.textContent,
+ "The value IDL attribute should act like the textContent IDL attribute");
+}
+
+function checkValueModeFlag(element)
+{
+ /**
+ * The value mode flag is the flag used to know if value should represent the
+ * textContent or the default value.
+ */
+ // value mode flag should be 'value'
+ isnot(element.defaultValue, element.value,
+ "When value is set, defaultValue keeps its value");
+
+ var f = document.getElementById('f');
+ f.reset();
+ // value mode flag should be 'default'
+ is(element.defaultValue, element.value, "When reset, defaultValue=value");
+ is(element.textContent, element.defaultValue,
+ "textContent should contain the defaultValue");
+}
+
+function checkDescendantChanged(element)
+{
+ /**
+ * Whenever a descendant is changed if the value mode flag is value,
+ * the default value should be the textContent value.
+ */
+ element.defaultValue = 'tulip';
+ element.value = 'foo';
+
+ // set value mode flag to 'default'
+ var f = document.getElementById('f');
+ f.reset();
+
+ is(element.textContent, element.defaultValue,
+ "textContent should contain the defaultValue");
+ element.textContent = "bar";
+ is(element.textContent, element.defaultValue,
+ "textContent should contain the defaultValue");
+}
+
+function checkFormIDLAttribute(element)
+{
+ is(element.form, document.getElementById('f'),
+ "form IDL attribute is invalid");
+}
+
+function checkHtmlForIDLAttribute(element)
+{
+ is(String(element.htmlFor), 'a b',
+ "htmlFor IDL attribute should reflect the for content attribute");
+
+ // DOMTokenList is tested in another bug so we just test assignation
+ element.htmlFor.value = 'a b c';
+ is(String(element.htmlFor), 'a b c', "htmlFor should have changed");
+}
+
+function submitForm()
+{
+ // Setting the values for the submit.
+ document.getElementById('o').value = 'foo';
+ document.getElementById('a').value = 'afield';
+ document.getElementById('b').value = 'bfield';
+
+ frameLoaded = checkFormSubmission;
+
+ // This will call checkFormSubmission() which is going to call ST.finish().
+ document.getElementById('f').submit();
+}
+
+function checkFormSubmission()
+{
+ /**
+ * All elements values have been set just before the submission.
+ * The input elements values should be in the submit url but the ouput
+ * element value should not appear.
+ */
+
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?a=afield&b=bfield`,
+ "The output element value should not be submitted");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ reflectString({
+ element: document.createElement("output"),
+ attribute: "name",
+ });
+
+ var o = document.getElementsByTagName('output');
+ is(o.length, 1, "There should be one output element");
+
+ o = o[0];
+ ok(o instanceof HTMLOutputElement,
+ "The output should be instance of HTMLOutputElement");
+
+ o = document.getElementById('o');
+ ok(o instanceof HTMLOutputElement,
+ "The output should be instance of HTMLOutputElement");
+
+ is(o.type, "output", "Output type IDL attribute should be 'output'");
+
+ checkNameAttribute(o);
+
+ checkValueAndDefaultValueIDLAttribute(o);
+
+ checkValueModeFlag(o);
+
+ checkDescendantChanged(o);
+
+ checkFormIDLAttribute(o);
+
+ checkHtmlForIDLAttribute(o);
+
+ submitForm();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_pattern_attribute.html b/dom/html/test/forms/test_pattern_attribute.html
new file mode 100644
index 0000000000..71d79c1def
--- /dev/null
+++ b/dom/html/test/forms/test_pattern_attribute.html
@@ -0,0 +1,324 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345512
+-->
+<head>
+ <title>Test for Bug 345512</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input { background-color: rgb(0,0,0) !important; }
+ input:valid { background-color: rgb(0,255,0) !important; }
+ input:invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345512">Mozilla Bug 345512</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input id='i' pattern="tulip" oninvalid="invalidEventHandler(event);">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345512 **/
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be invalid");
+ gInvalid = true;
+}
+
+function completeValidityCheck(element, alwaysValid, isBarred)
+{
+ // Check when pattern matches.
+ if (element.type == 'email') {
+ element.pattern = ".*@bar.com";
+ element.value = "foo@bar.com";
+ } else if (element.type == 'url') {
+ element.pattern = "http://.*\\.com$";
+ element.value = "http://mozilla.com";
+ } else if (element.type == 'file') {
+ element.pattern = "foo";
+ SpecialPowers.wrap(element).mozSetFileArray([new File(["foo"], "foo")]);
+ } else {
+ element.pattern = "foo";
+ element.value = "foo";
+ }
+
+ checkValidPattern(element, true, isBarred);
+
+ // Check when pattern does not match.
+
+ if (element.type == 'email') {
+ element.pattern = ".*@bar.com";
+ element.value = "foo@foo.com";
+ } else if (element.type == 'url') {
+ element.pattern = "http://.*\\.com$";
+ element.value = "http://mozilla.org";
+ } else if (element.type == 'file') {
+ element.pattern = "foo";
+ SpecialPowers.wrap(element).mozSetFileArray([new File(["bar"], "bar")]);
+ } else {
+ element.pattern = "foo";
+ element.value = "bar";
+ }
+
+ if (!alwaysValid) {
+ checkInvalidPattern(element, true);
+ } else {
+ checkValidPattern(element, true, isBarred);
+ }
+}
+
+function checkValidPattern(element, completeCheck, isBarred)
+{
+ if (completeCheck) {
+ gInvalid = false;
+
+ ok(!element.validity.patternMismatch,
+ "Element should not suffer from pattern mismatch");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "Invalid event shouldn't have been thrown");
+ is(element.validationMessage, '',
+ "Validation message should be the empty string");
+ if (element.type != 'radio' && element.type != 'checkbox') {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)",
+ "The pseudo-class is not correctly applied");
+ }
+ } else {
+ ok(!element.validity.patternMismatch,
+ "Element should not suffer from pattern mismatch");
+ }
+}
+
+function checkInvalidPattern(element, completeCheck)
+{
+ if (completeCheck) {
+ gInvalid = false;
+
+ ok(element.validity.patternMismatch,
+ "Element should suffer from pattern mismatch");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+ ok(gInvalid, "Invalid event should have been thrown");
+ is(element.validationMessage,
+ "Please match the requested format.",
+ "Validation message is not valid");
+ } else {
+ ok(element.validity.patternMismatch,
+ "Element should suffer from pattern mismatch");
+ }
+
+ if (element.type != 'radio' && element.type != 'checkbox') {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ }
+}
+
+function checkSyntaxError(element)
+{
+ ok(!element.validity.patternMismatch,
+ "On SyntaxError, element should not suffer");
+}
+
+function checkPatternValidity(element)
+{
+ element.pattern = "foo";
+
+ element.value = '';
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkValidPattern(element);
+
+ element.value = "bar";
+ checkInvalidPattern(element);
+
+ element.value = "foobar";
+ checkInvalidPattern(element);
+
+ element.value = "foofoo";
+ checkInvalidPattern(element);
+
+ element.pattern = "foo\"bar";
+ element.value = "foo\"bar";
+ checkValidPattern(element);
+
+ element.value = 'foo"bar';
+ checkValidPattern(element);
+
+ element.pattern = "foo'bar";
+ element.value = "foo\'bar";
+ checkValidPattern(element);
+
+ element.pattern = "foo\\(bar";
+ element.value = "foo(bar";
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkInvalidPattern(element);
+
+ element.pattern = "foo\\)bar";
+ element.value = "foo)bar";
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkInvalidPattern(element);
+
+ // Check for 'i' flag disabled. Should be case sensitive.
+ element.value = "Foo";
+ checkInvalidPattern(element);
+
+ // We can't check for the 'g' flag because we only test, we don't execute.
+ // We can't check for the 'm' flag because .value shouldn't contain line breaks.
+
+ // We need '\\\\' because '\\' will produce '\\' and we want to escape the '\'
+ // for the regexp.
+ element.pattern = "foo\\\\bar";
+ element.value = "foo\\bar";
+ checkValidPattern(element);
+
+ // We may want to escape the ' in the pattern, but this is a SyntaxError
+ // when unicode flag is set.
+ element.pattern = "foo\\'bar";
+ element.value = "foo'bar";
+ checkSyntaxError(element);
+ element.value = "baz";
+ checkSyntaxError(element);
+
+ // We should check the pattern attribute do not pollute |RegExp.lastParen|.
+ is(RegExp.lastParen, "", "RegExp.lastParen should be the empty string");
+
+ element.pattern = "(foo)";
+ element.value = "foo";
+ checkValidPattern(element);
+ is(RegExp.lastParen, "", "RegExp.lastParen should be the empty string");
+
+ // That may sound weird but the empty string is a valid pattern value.
+ element.pattern = "";
+ element.value = "";
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkInvalidPattern(element);
+
+ // Checking some complex patterns. As we are using js regexp mechanism, these
+ // tests doesn't aim to test the regexp mechanism.
+ element.pattern = "\\d{2}\\s\\d{2}\\s\\d{4}"
+ element.value = "01 01 2010"
+ checkValidPattern(element);
+
+ element.value = "01/01/2010"
+ checkInvalidPattern(element);
+
+ element.pattern = "[0-9a-zA-Z]([\\-.\\w]*[0-9a-zA-Z_+])*@([0-9a-zA-Z][\\-\\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9}";
+ element.value = "foo@bar.com";
+ checkValidPattern(element);
+
+ element.value = "...@bar.com";
+ checkInvalidPattern(element);
+
+ element.pattern = "^(?:\\w{3,})$";
+ element.value = "foo";
+ checkValidPattern(element);
+
+ element.value = "f";
+ checkInvalidPattern(element);
+
+ // If @title is specified, it should be added in the validation message.
+ if (element.type == 'email') {
+ element.pattern = "foo@bar.com"
+ element.value = "bar@foo.com";
+ } else if (element.type == 'url') {
+ element.pattern = "http://mozilla.com";
+ element.value = "http://mozilla.org";
+ } else {
+ element.pattern = "foo";
+ element.value = "bar";
+ }
+ element.title = "this is an explanation of the regexp";
+ is(element.validationMessage,
+ "Please match the requested format: " + element.title + ".",
+ "Validation message is not valid");
+ element.title = "";
+ is(element.validationMessage,
+ "Please match the requested format.",
+ "Validation message is not valid");
+
+ element.pattern = "foo";
+ if (element.type == 'email') {
+ element.value = "bar@foo.com";
+ } else if (element.type == 'url') {
+ element.value = "http://mozilla.org";
+ } else {
+ element.value = "bar";
+ }
+ checkInvalidPattern(element);
+
+ element.removeAttribute('pattern');
+ checkValidPattern(element, true);
+
+ // Unicode pattern
+ for (var pattern of ["\\u{1F438}{2}", "\u{1F438}{2}",
+ "\\uD83D\\uDC38{2}", "\uD83D\uDC38{2}",
+ "\u{D83D}\u{DC38}{2}"]) {
+ element.pattern = pattern;
+
+ element.value = "\u{1F438}\u{1F438}";
+ checkValidPattern(element);
+
+ element.value = "\uD83D\uDC38\uD83D\uDC38";
+ checkValidPattern(element);
+
+ element.value = "\uD83D\uDC38\uDC38";
+ checkInvalidPattern(element);
+ }
+
+ element.pattern = "\\u{D83D}\\u{DC38}{2}";
+
+ element.value = "\u{1F438}\u{1F438}";
+ checkInvalidPattern(element);
+
+ element.value = "\uD83D\uDC38\uD83D\uDC38";
+ checkInvalidPattern(element);
+
+ element.value = "\uD83D\uDC38\uDC38";
+ checkInvalidPattern(element);
+}
+
+var input = document.getElementById('i');
+
+// |validTypes| are the types which accept @pattern
+// and |invalidTypes| are the ones which do not accept it.
+var validTypes = Array('text', 'password', 'search', 'tel', 'email', 'url');
+var barredTypes = Array('hidden', 'reset', 'button');
+var invalidTypes = Array('checkbox', 'radio', 'file', 'number', 'range', 'date',
+ 'time', 'color', 'submit', 'image', 'month', 'week',
+ 'datetime-local');
+
+for (type of validTypes) {
+ input.type = type;
+ completeValidityCheck(input, false);
+ checkPatternValidity(input);
+}
+
+for (type of barredTypes) {
+ input.type = type;
+ completeValidityCheck(input, true, true);
+}
+
+for (type of invalidTypes) {
+ input.type = type;
+ completeValidityCheck(input, true);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_preserving_metadata_between_reloads.html b/dom/html/test/forms/test_preserving_metadata_between_reloads.html
new file mode 100644
index 0000000000..07ca05f7ce
--- /dev/null
+++ b/dom/html/test/forms/test_preserving_metadata_between_reloads.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test preserving metadata between page reloads</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+<body>
+<p id="display"></p>
+<div id="content">
+ <iframe id="test-frame" width="800px" height="600px" srcdoc='
+ <html>
+ <body>
+ <h3>Bug 1635224: Preserve mLastValueChangeWasInteractive between reloads</h3>
+ <div>
+ <form>
+ <textarea id="maxlen-textarea" maxlength="2" rows="2" cols="10"></textarea><br/>
+ <input id="maxlen-inputtext" type="text" maxlength="2"><br/>
+ <textarea id="minlen-textarea" minlength="8" rows="2" cols="10"></textarea><br/>
+ <input id="minlen-inputtext" type="text" minlength="8"><br/>
+ </form>
+ </div>
+ </body>
+ </html>
+'></iframe>
+</div>
+
+<pre id="test">
+<script>
+ SimpleTest.waitForExplicitFinish()
+ const Ci = SpecialPowers.Ci;
+ const str = "aaaaa";
+
+ function afterLoad() {
+ SimpleTest.waitForFocus(async function () {
+ await SpecialPowers.pushPrefEnv({"set": [["editor.truncate_user_pastes", false]]});
+ var iframeDoc = $("test-frame").contentDocument;
+ var src = iframeDoc.getElementById("src");
+
+ function test(fieldId, callback) {
+ var field = iframeDoc.getElementById(fieldId);
+ field.focus();
+ SimpleTest.waitForClipboard(str,
+ function () {
+ SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(str);
+ },
+ function () {
+ synthesizeKey("v", { accelKey: true });
+ is(field.value, "aaaaa", "the value of " + fieldId + " was entered correctly");
+ is(field.checkValidity(), false, "the validity of " + fieldId + " should be false");
+ $("test-frame").contentWindow.location.reload();
+ is(field.value, "aaaaa", "the value of " + fieldId + " persisted correctly");
+ is(field.checkValidity(), false, "the validity of " + fieldId + " should be false after reload");
+ callback();
+ },
+ function () {
+ ok(false, "Failed to copy the string");
+ SimpleTest.finish();
+ }
+ );
+ }
+
+ function runNextTest() {
+ if (fieldIds.length) {
+ var currentFieldId = fieldIds.shift();
+ test(currentFieldId, runNextTest);
+ } else {
+ SimpleTest.finish();
+ }
+ }
+
+ var fieldIds = ["maxlen-textarea", "maxlen-inputtext", "minlen-textarea", "minlen-inputtext"];
+ runNextTest();
+ });
+ }
+ addLoadEvent(afterLoad);
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/html/test/forms/test_progress_element.html b/dom/html/test/forms/test_progress_element.html
new file mode 100644
index 0000000000..065adf94ea
--- /dev/null
+++ b/dom/html/test/forms/test_progress_element.html
@@ -0,0 +1,307 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=514437
+https://bugzilla.mozilla.org/show_bug.cgi?id=633913
+-->
+<head>
+ <title>Test for progress element content and layout</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514437">Mozilla Bug 514437</a>
+and
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=633913">Mozilla Bug 633913</a>
+<p id="display"></p>
+<iframe name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="visibility: hidden;">
+ <form id='f' method='get' target='submit_frame' action='foo'>
+ <progress id='p'></progress>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.expectAssertions(0, 1);
+
+/** Test for progress element content and layout **/
+
+function checkFormIDLAttribute(aElement)
+{
+ is("form" in aElement, false, "<progress> shouldn't have a form attribute");
+}
+
+function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL)
+{
+ var expectedValueForIDL = aNewValue;
+ var expectedValueForContent = String(aNewValue);
+
+ if (aExpectedValueForIDL !== undefined) {
+ expectedValueForIDL = aExpectedValueForIDL;
+ }
+
+ if (aNewValue != null) {
+ aElement.setAttribute(aAttribute, aNewValue);
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+
+ if (parseFloat(aNewValue) == aNewValue) {
+ aElement[aAttribute] = aNewValue;
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], parseFloat(expectedValueForIDL),
+ aAttribute + " IDL attribute should be " + parseFloat(expectedValueForIDL));
+ }
+ } else {
+ aElement.removeAttribute(aAttribute);
+ is(aElement.getAttribute(aAttribute), null,
+ aAttribute + " content attribute should be null");
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+ }
+}
+
+function checkValueAttribute()
+{
+ var tests = [
+ // value has to be a valid float, its default value is 0.0 otherwise.
+ [ null, 0.0 ],
+ [ 'fo', 0.0 ],
+ // If value < 0.0, 0.0 is used instead.
+ [ -1.0, 0.0 ],
+ // If value >= max, max is used instead (max default value is 1.0).
+ [ 2.0, 1.0 ],
+ [ 1.0, 0.5, 0.5 ],
+ [ 10.0, 5.0, 5.0 ],
+ [ 13.37, 13.37, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('max', test[2]);
+ }
+
+ checkAttribute(element, 'value', test[0], test[1]);
+
+ element.removeAttribute('max');
+ }
+}
+
+function checkMaxAttribute()
+{
+ var tests = [
+ // max default value is 1.0.
+ [ null, 1.0 ],
+ // If value <= 0.0, 1.0 is used instead.
+ [ 0.0, 1.0 ],
+ [ -1.0, 1.0 ],
+ // Regular reflection.
+ [ 0.5 ],
+ [ 1.0 ],
+ [ 2.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ checkAttribute(element, 'max', test[0], test[1]);
+ }
+}
+
+function checkPositionAttribute()
+{
+ function checkPositionValue(aElement, aValue, aMax, aExpected) {
+ if (aValue != null) {
+ aElement.setAttribute('value', aValue);
+ } else {
+ aElement.removeAttribute('value');
+ }
+
+ if (aMax != null) {
+ aElement.setAttribute('max', aMax);
+ } else {
+ aElement.removeAttribute('max');
+ }
+
+ is(aElement.position, aExpected, "position IDL attribute should be " + aExpected);
+ }
+
+ var tests = [
+ // value has to be defined (indeterminate state).
+ [ null, null, -1.0 ],
+ [ null, 1.0, -1.0 ],
+ // value has to be defined to a valid float (indeterminate state).
+ [ 'foo', 1.0, -1.0 ],
+ // If value < 0.0, 0.0 is used instead.
+ [ -1.0, 1.0, 0.0 ],
+ // If value >= max, max is used instead.
+ [ 2.0, 1.0, 1.0 ],
+ // If max isn't present, max is set to 1.0.
+ [ 1.0, null, 1.0 ],
+ // If max isn't a valid float, max is set to 1.0.
+ [ 1.0, 'foo', 1.0 ],
+ // If max isn't > 0, max is set to 1.0.
+ [ 1.0, -1.0, 1.0 ],
+ // A few simple and valid values.
+ [ 0.0, 1.0, 0.0 ],
+ [ 0.1, 1.0, 0.1/1.0 ],
+ [ 0.1, 2.0, 0.1/2.0 ],
+ [ 10, 50, 10/50 ],
+ // Values implying .position is a double.
+ [ 1.0, 3.0, 1.0/3.0 ],
+ [ 0.1, 0.7, 0.1/0.7 ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ checkPositionValue(element, test[0], test[1], test[2], test[3]);
+ }
+}
+
+function checkIndeterminatePseudoClass()
+{
+ function checkIndeterminate(aElement, aValue, aMax, aIndeterminate) {
+ if (aValue != null) {
+ aElement.setAttribute('value', aValue);
+ } else {
+ aElement.removeAttribute('value');
+ }
+
+ if (aMax != null) {
+ aElement.setAttribute('max', aMax);
+ } else {
+ aElement.removeAttribute('max');
+ }
+
+ is(aElement.matches("progress:indeterminate"), aIndeterminate,
+ "<progress> indeterminate state should be " + aIndeterminate);
+ }
+
+ var tests = [
+ // Indeterminate state: (value is undefined, or not a float)
+ // value has to be defined (indeterminate state).
+ [ null, null, true ],
+ [ null, 1.0, true ],
+ [ 'foo', 1.0, true ],
+ // Determined state:
+ [ -1.0, 1.0, false ],
+ [ 2.0, 1.0, false ],
+ [ 1.0, null, false ],
+ [ 1.0, 'foo', false ],
+ [ 1.0, -1.0, false ],
+ [ 0.0, 1.0, false ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ checkIndeterminate(element, test[0], test[1], test[2]);
+ }
+}
+
+function checkFormListedElement(aElement)
+{
+ is(document.forms[0].elements.length, 0, "the form should have no element");
+}
+
+function checkLabelable(aElement)
+{
+ var content = document.getElementById('content');
+ var label = document.createElement('label');
+
+ content.appendChild(label);
+ label.appendChild(aElement);
+ is(label.control, aElement, "progress should be labelable");
+
+ // Cleaning-up.
+ content.removeChild(label);
+ content.appendChild(aElement);
+}
+
+function checkNotResetableAndFormSubmission(aElement)
+{
+ // Creating an input element to check the submission worked.
+ var form = document.forms[0];
+ var input = document.createElement('input');
+
+ input.name = 'a';
+ input.value = 'tulip';
+ form.appendChild(input);
+
+ // Setting values.
+ aElement.value = 42.0;
+ aElement.max = 100.0;
+
+ document.getElementsByName('submit_frame')[0].addEventListener("load", function() {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?a=tulip`,
+ "The progress element value should not be submitted");
+
+ checkNotResetable();
+ }, {once: true});
+
+ form.submit();
+}
+
+function checkNotResetable()
+{
+ // Try to reset the form.
+ var form = document.forms[0];
+ var element = document.getElementById('p');
+
+ element.value = 3.0;
+ element.max = 42.0;
+
+ form.reset();
+
+ SimpleTest.executeSoon(function() {
+ is(element.value, 3.0, "progress.value should not have changed");
+ is(element.max, 42.0, "progress.max should not have changed");
+
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var p = document.getElementById('p');
+
+ok(p instanceof HTMLProgressElement,
+ "The progress element should be instance of HTMLProgressElement");
+is(p.constructor, HTMLProgressElement,
+ "The progress element constructor should be HTMLProgressElement");
+
+checkFormIDLAttribute(p);
+
+checkValueAttribute();
+
+checkMaxAttribute();
+
+checkPositionAttribute();
+
+checkIndeterminatePseudoClass();
+
+checkFormListedElement(p);
+
+checkLabelable(p);
+
+checkNotResetableAndFormSubmission(p);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_radio_in_label.html b/dom/html/test/forms/test_radio_in_label.html
new file mode 100644
index 0000000000..7e8a232cc3
--- /dev/null
+++ b/dom/html/test/forms/test_radio_in_label.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=229925
+-->
+<head>
+ <title>Test for Bug 229925</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=229925">Mozilla Bug 229925</a>
+<p id="display"></p>
+<form>
+ <label>
+ <span id="s1">LABEL</span>
+ <input type="radio" name="rdo" value="1" id="r1" onmousedown="document.body.appendChild(document.createTextNode('down'));">
+ <input type="radio" name="rdo" value="2" id="r2" checked="checked">
+ </label>
+</form>
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 229925 **/
+SimpleTest.waitForExplicitFinish();
+var r1 = document.getElementById("r1");
+var r2 = document.getElementById("r2");
+var s1 = document.getElementById("s1");
+startTest();
+function startTest() {
+ r1.click();
+ ok(r1.checked,
+ "The first radio input element should be checked by clicking the element");
+ r2.click();
+ ok(r2.checked,
+ "The second radio input element should be checked by clicking the element");
+ s1.click();
+ ok(r1.checked,
+ "The first radio input element should be checked by clicking other element");
+
+ r1.focus();
+ synthesizeKey("KEY_ArrowLeft");
+ ok(r2.checked,
+ "The second radio input element should be checked by key");
+ synthesizeKey("KEY_ArrowLeft");
+ ok(r1.checked,
+ "The first radio input element should be checked by key");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_radio_radionodelist.html b/dom/html/test/forms/test_radio_radionodelist.html
new file mode 100644
index 0000000000..8761c22b58
--- /dev/null
+++ b/dom/html/test/forms/test_radio_radionodelist.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=779723
+-->
+<head>
+ <title>Test for Bug 779723</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=779723">Mozilla Bug 779723</a>
+<p id="display"></p>
+<form>
+ <input type="checkbox" name="rdo" value="0" id="r0" checked="checked">
+ <input type="radio" name="rdo" id="r1">
+ <input type="radio" name="rdo" id="r2" value="2">
+</form>
+<script class="testbody" type="text/javascript">
+/** Test for Bug 779723 **/
+
+var rdoList = document.forms[0].elements.namedItem('rdo');
+is(rdoList.value, "", "The value attribute should be empty");
+
+document.getElementById('r2').checked = true;
+is(rdoList.value, "2", "The value attribute should be 2");
+
+document.getElementById('r1').checked = true;
+is(rdoList.value, "on", "The value attribute should be on");
+
+document.getElementById('r1').value = 1;
+is(rdoList.value, "1", "The value attribute should be 1");
+
+is(rdoList.value, document.getElementById('r1').value,
+ "The value attribute should be equal to the first checked radio input element's value");
+ok(!document.getElementById('r2').checked,
+ "The second radio input element should not be checked");
+
+rdoList.value = '2';
+is(rdoList.value, document.getElementById('r2').value,
+ "The value attribute should be equal to the second radio input element's value");
+ok(document.getElementById('r2').checked,
+ "The second radio input element should be checked");
+
+rdoList.value = '3';
+is(rdoList.value, document.getElementById('r2').value,
+ "The value attribute should be the second radio input element's value");
+ok(document.getElementById('r2').checked,
+ "The second radio input element should be checked");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_reportValidation_preventDefault.html b/dom/html/test/forms/test_reportValidation_preventDefault.html
new file mode 100644
index 0000000000..3f3b99d140
--- /dev/null
+++ b/dom/html/test/forms/test_reportValidation_preventDefault.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1088761
+-->
+<head>
+ <title>Test for Bug 1088761</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea, fieldset, button, select, output, object { background-color: rgb(0,0,0) !important; }
+ :valid { background-color: rgb(0,255,0) !important; }
+ :invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1088761">Mozilla Bug 1088761</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <fieldset id='f' oninvalid="invalidEventHandler(event, true);"></fieldset>
+ <input id='i' required oninvalid="invalidEventHandler(event, true);">
+ <button id='b' oninvalid="invalidEventHandler(event, true);"></button>
+ <select id='s' required oninvalid="invalidEventHandler(event, true);"></select>
+ <textarea id='t' required oninvalid="invalidEventHandler(event, true);"></textarea>
+ <output id='o' oninvalid="invalidEventHandler(event, true);"></output>
+ <object id='obj' oninvalid="invalidEventHandler(event, true);"></object>
+</div>
+<div id="content2" style="display: none">
+ <fieldset id='f2' oninvalid="invalidEventHandler(event, false);"></fieldset>
+ <input id='i2' required oninvalid="invalidEventHandler(event, false);">
+ <button id='b2' oninvalid="invalidEventHandler(event, false);"></button>
+ <select id='s2' required oninvalid="invalidEventHandler(event, false);"></select>
+ <textarea id='t2' required oninvalid="invalidEventHandler(event, false);"></textarea>
+ <output id='o2' oninvalid="invalidEventHandler(event, false);"></output>
+ <object id='obj2' oninvalid="invalidEventHandler(event, false);"></object>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1088761 **/
+
+var gInvalid = false;
+
+function invalidEventHandler(aEvent, isPreventDefault)
+{
+ if (isPreventDefault) {
+ aEvent.preventDefault();
+ }
+
+ is(aEvent.type, "invalid", "Invalid event type should be invalid");
+ ok(!aEvent.bubbles, "Invalid event should not bubble");
+ ok(aEvent.cancelable, "Invalid event should be cancelable");
+ gInvalid = true;
+}
+
+function checkReportValidityForInvalid(element)
+{
+ gInvalid = false;
+ ok(!element.reportValidity(), "reportValidity() should return false when the element is not valid");
+ ok(gInvalid, "Invalid event should have been handled");
+}
+
+function checkReportValidityForValid(element)
+{
+ gInvalid = false;
+ ok(element.reportValidity(), "reportValidity() should return true when the element is valid");
+ ok(!gInvalid, "Invalid event shouldn't have been handled");
+}
+
+checkReportValidityForInvalid(document.getElementById('i'));
+checkReportValidityForInvalid(document.getElementById('s'));
+checkReportValidityForInvalid(document.getElementById('t'));
+
+checkReportValidityForInvalid(document.getElementById('i2'));
+checkReportValidityForInvalid(document.getElementById('s2'));
+checkReportValidityForInvalid(document.getElementById('t2'));
+
+checkReportValidityForValid(document.getElementById('o'));
+checkReportValidityForValid(document.getElementById('obj'));
+checkReportValidityForValid(document.getElementById('f'));
+
+checkReportValidityForValid(document.getElementById('o2'));
+checkReportValidityForValid(document.getElementById('obj2'));
+checkReportValidityForValid(document.getElementById('f2'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_required_attribute.html b/dom/html/test/forms/test_required_attribute.html
new file mode 100644
index 0000000000..a95a5cc339
--- /dev/null
+++ b/dom/html/test/forms/test_required_attribute.html
@@ -0,0 +1,416 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345822
+-->
+<head>
+ <title>Test for Bug 345822</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345822">Mozilla Bug 345822</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345822 **/
+
+function checkNotSufferingFromBeingMissing(element, doNotApply)
+{
+ ok(!element.validity.valueMissing,
+ "Element should not suffer from value missing");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ is(element.validationMessage, "",
+ "Validation message should be the empty string");
+
+ if (doNotApply) {
+ ok(!element.matches(':valid'), ":valid should not apply");
+ ok(!element.matches(':invalid'), ":invalid should not apply");
+ } else {
+ ok(element.matches(':valid'), ":valid should apply");
+ ok(!element.matches(':invalid'), ":invalid should not apply");
+ }
+}
+
+function checkSufferingFromBeingMissing(element)
+{
+ ok(element.validity.valueMissing, "Element should suffer from value missing");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+
+ if (element.type == 'checkbox')
+ {
+ is(element.validationMessage,
+ "Please check this box if you want to proceed.",
+ "Validation message is wrong");
+ }
+ else if (element.type == 'radio')
+ {
+ is(element.validationMessage,
+ "Please select one of these options.",
+ "Validation message is wrong");
+ }
+ else if (element.type == 'file')
+ {
+ is(element.validationMessage,
+ "Please select a file.",
+ "Validation message is wrong");
+ }
+ else if (element.type == 'number')
+ {
+ is(element.validationMessage,
+ "Please enter a number.",
+ "Validation message is wrong");
+ }
+ else // text fields
+ {
+ is(element.validationMessage,
+ "Please fill out this field.",
+ "Validation message is wrong");
+ }
+
+ ok(!element.matches(':valid'), ":valid should apply");
+ ok(element.matches(':invalid'), ":invalid should not apply");
+}
+
+function checkTextareaRequiredValidity()
+{
+ var element = document.createElement('textarea');
+ document.forms[0].appendChild(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.readOnly = true;
+ checkNotSufferingFromBeingMissing(element, true);
+
+ element.readOnly = false;
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = 'foo';
+ checkNotSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ checkSufferingFromBeingMissing(element);
+
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.focus();
+ element.required = true;
+ SpecialPowers.wrap(element).value = 'foobar';
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ // TODO: for the moment, a textarea outside of a document is mutable.
+ SpecialPowers.wrap(element).value = ''; // To make -moz-ui-valid apply.
+ element.required = false;
+ document.forms[0].removeChild(element);
+ checkNotSufferingFromBeingMissing(element);
+}
+
+function checkInputRequiredNotApply(type, isBarred)
+{
+ var element = document.createElement('input');
+ element.type = type;
+ document.forms[0].appendChild(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element, isBarred);
+
+ element.required = true;
+ checkNotSufferingFromBeingMissing(element, isBarred);
+
+ element.required = false;
+
+ document.forms[0].removeChild(element);
+ checkNotSufferingFromBeingMissing(element, isBarred);
+}
+
+function checkInputRequiredValidity(type)
+{
+ var element = document.createElement('input');
+ element.type = type;
+ document.forms[0].appendChild(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.readOnly = true;
+ checkNotSufferingFromBeingMissing(element, true);
+
+ element.readOnly = false;
+ checkSufferingFromBeingMissing(element);
+
+ if (element.type == 'email') {
+ SpecialPowers.wrap(element).value = 'foo@bar.com';
+ } else if (element.type == 'url') {
+ SpecialPowers.wrap(element).value = 'http://mozilla.org/';
+ } else if (element.type == 'number') {
+ SpecialPowers.wrap(element).value = '42';
+ } else if (element.type == 'date') {
+ SpecialPowers.wrap(element).value = '2010-10-10';
+ } else if (element.type == 'time') {
+ SpecialPowers.wrap(element).value = '21:21';
+ // TODO: Bug 1864327. This test is wrong, and needs fixing properly.
+ // eslint-disable-next-line no-cond-assign
+ } else if (element.type = 'month') {
+ SpecialPowers.wrap(element).value = '2010-10';
+ } else {
+ SpecialPowers.wrap(element).value = 'foo';
+ }
+ checkNotSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ checkSufferingFromBeingMissing(element);
+
+ element.focus();
+ element.required = true;
+ SpecialPowers.wrap(element).value = 'foobar';
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ SpecialPowers.wrap(element).value = ''; // To make :-moz-ui-valid apply.
+ checkSufferingFromBeingMissing(element);
+ document.forms[0].removeChild(element);
+ // Removing the child changes nothing about whether it's valid
+ checkSufferingFromBeingMissing(element);
+}
+
+function checkInputRequiredValidityForCheckbox()
+{
+ var element = document.createElement('input');
+ element.type = 'checkbox';
+ document.forms[0].appendChild(element);
+
+ element.checked = false;
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.checked = true;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.checked = false;
+ checkSufferingFromBeingMissing(element);
+
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.focus();
+ element.required = true;
+ element.checked = true;
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ element.checked = false;
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ element.checked = false;
+ document.forms[0].removeChild(element);
+ checkSufferingFromBeingMissing(element);
+}
+
+function checkInputRequiredValidityForRadio()
+{
+ var element = document.createElement('input');
+ element.type = 'radio';
+ element.name = 'test'
+ document.forms[0].appendChild(element);
+
+ element.checked = false;
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.checked = true;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.checked = false;
+ checkSufferingFromBeingMissing(element);
+
+ // A required radio button should not suffer from value missing if another
+ // radio button from the same group is checked.
+ var element2 = document.createElement('input');
+ element2.type = 'radio';
+ element2.name = 'test';
+
+ element2.checked = true;
+ element2.required = false;
+ document.forms[0].appendChild(element2);
+
+ // Adding a checked radio should make required radio in the group not
+ // suffering from being missing.
+ checkNotSufferingFromBeingMissing(element);
+
+ element.checked = false;
+ element2.checked = false;
+ checkSufferingFromBeingMissing(element);
+
+ // The other radio button should not be disabled.
+ // A disabled checked radio button in the radio group
+ // is enough to not suffer from value missing.
+ element2.checked = true;
+ element2.disabled = true;
+ checkNotSufferingFromBeingMissing(element);
+
+ // If a radio button is not required but another radio button is required in
+ // the same group, the not required radio button should suffer from value
+ // missing.
+ element2.disabled = false;
+ element2.checked = false;
+ element.required = false;
+ element2.required = true;
+ checkSufferingFromBeingMissing(element);
+ checkSufferingFromBeingMissing(element2);
+
+ element.checked = true;
+ checkNotSufferingFromBeingMissing(element2);
+
+ // The checked radio is not in the group anymore, element2 should be invalid.
+ element.form.removeChild(element);
+ checkNotSufferingFromBeingMissing(element);
+ checkSufferingFromBeingMissing(element2);
+
+ element2.focus();
+ element2.required = true;
+ element2.checked = true;
+ element2.blur();
+ element2.form.reset();
+ checkSufferingFromBeingMissing(element2);
+
+ element2.required = true;
+ element2.checked = false;
+ element2.form.reportValidity();
+ checkSufferingFromBeingMissing(element2);
+
+ element2.form.reset();
+ checkSufferingFromBeingMissing(element2);
+
+ element2.required = true;
+ element2.checked = false;
+ document.forms[0].removeChild(element2);
+ checkSufferingFromBeingMissing(element2);
+}
+
+function checkInputRequiredValidityForFile()
+{
+ var element = document.createElement('input');
+ element.type = 'file'
+ document.forms[0].appendChild(element);
+
+ var file = new File([""], "345822_file");
+
+ SpecialPowers.wrap(element).value = "";
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).mozSetFileArray([file]);
+ checkNotSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = "";
+ checkSufferingFromBeingMissing(element);
+
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.focus();
+ SpecialPowers.wrap(element).mozSetFileArray([file]);
+ element.required = true;
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ SpecialPowers.wrap(element).value = '';
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ SpecialPowers.wrap(element).value = '';
+ document.forms[0].removeChild(element);
+ checkSufferingFromBeingMissing(element);
+}
+
+checkTextareaRequiredValidity();
+
+// The require attribute behavior depend of the input type.
+// First of all, checks for types that make the element barred from
+// constraint validation.
+var typeBarredFromConstraintValidation = ["hidden", "button", "reset"];
+for (type of typeBarredFromConstraintValidation) {
+ checkInputRequiredNotApply(type, true);
+}
+
+// Then, checks for the types which do not use the required attribute.
+var typeRequireNotApply = ['range', 'color', 'submit', 'image'];
+for (type of typeRequireNotApply) {
+ checkInputRequiredNotApply(type, false);
+}
+
+// Now, checking for all types which accept the required attribute.
+var typeRequireApply = ["text", "password", "search", "tel", "email", "url",
+ "number", "date", "time", "month", "week",
+ "datetime-local"];
+
+for (type of typeRequireApply) {
+ checkInputRequiredValidity(type);
+}
+
+checkInputRequiredValidityForCheckbox();
+checkInputRequiredValidityForRadio();
+checkInputRequiredValidityForFile();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_restore_form_elements.html b/dom/html/test/forms/test_restore_form_elements.html
new file mode 100644
index 0000000000..be22a29b7b
--- /dev/null
+++ b/dom/html/test/forms/test_restore_form_elements.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=737851
+-->
+<head>
+ <meta charset="utf-8">
+
+ <title>Test for Bug 737851</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=737851">Mozilla Bug 737851</a>
+
+<p id="display"></p>
+
+
+<div id="content">
+
+ <iframe id="frame" width="800px" height="600px" srcdoc='
+ <html>
+ <body style="display:none;">
+
+ <h3>Checking persistence of inputs through js inserts and moves</h3>
+ <div id="test">
+ <input id="a"/>
+ <input id="b"/>
+ <form id="form1">
+ <input id="c"/>
+ <input id="d"/>
+ </form>
+ <form id="form2">
+ <input id="radio1" type="radio" name="radio"/>
+ <input type="radio" name="radio"/>
+ <input type="radio" name="radio"/>
+ <input type="radio" name="radio"/>
+ </form>
+ <input id="e"/>
+ </div>
+
+ <h3>Bug 728798: checking persistence of inputs when forward-using @form</h3>
+ <div>
+ <input id="728798-a" form="728798-form" name="a"/>
+ <form id="728798-form">
+ <input id="728798-b" form="728798-form" name="b"/>
+ <input id="728798-c" name="c"/>
+ </form>
+ <input id="728798-d" form="728798-form" name="d"/>
+ </div>
+
+ </body>
+ </html>
+ '></iframe>
+
+</div>
+
+
+<pre id="test">
+<script type="text/javascript">
+
+var frameElem = document.getElementById("frame");
+var frame = frameElem.contentWindow;
+
+
+/* -- Main test run -- */
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ shuffle();
+ fill();
+ frameElem.addEventListener("load", function() {
+ shuffle();
+ checkAllFields();
+ SimpleTest.finish();
+ });
+ frame.location.reload();
+})
+
+
+/* -- Input fields js changes and moves -- */
+
+function shuffle() {
+ var framedoc = frame.document;
+
+ // Insert a button (toplevel)
+ var btn = framedoc.createElement("button");
+ var testdiv = framedoc.getElementById("test");
+ testdiv.insertBefore(btn, framedoc.getElementById("b"));
+
+ // Insert a dynamically generated input (in a form)
+ var newInput = framedoc.createElement("input");
+ newInput.setAttribute("id","c0");
+ var form1 = framedoc.getElementById("form1");
+ form1.insertBefore(newInput, form1.firstChild);
+
+ // Move an input around
+ var inputD = framedoc.getElementById("d");
+ var form2 = framedoc.getElementById("form2");
+ form2.insertBefore(inputD, form2.firstChild)
+
+ // Clone an existing input
+ var inputE2 = framedoc.getElementById("e").cloneNode(true);
+ inputE2.setAttribute("id","e2");
+ testdiv.appendChild(inputE2);
+}
+
+
+/* -- Input fields fill & check -- */
+
+/* Values entered in the input fields (by id) */
+
+var fieldValues = {
+ 'a':'simple input',
+ 'b':'moved by inserting a button before (no form)',
+ 'c0':'dynamically generated input',
+ 'c':'moved by inserting an input before (in a form)',
+ 'd':'moved from a form to another',
+ 'e':'the original',
+ 'e2':'the clone',
+ '728798-a':'before the form',
+ '728798-b':'from within the form',
+ '728798-c':'no form attribute in the form',
+ '728798-d':'after the form'
+}
+
+/* Fields for which the input is changed, and corresponding value
+ (clone and creation, same behaviour as webkit) */
+
+var changedFields = {
+ // dynamically generated input field not preserved
+ 'c0':'',
+ // cloned input field is restored with the value of the original
+ 'e2':fieldValues.e
+}
+
+/* Simulate user input by entering the values */
+
+function fill() {
+ for (id in fieldValues) {
+ frame.document.getElementById(id).value = fieldValues[id];
+ }
+ // an input is inserted before the radios (that may move the selected one by 1)
+ frame.document.getElementById('radio1').checked = true;
+}
+
+/* Check that all the fields are as they have been entered */
+
+function checkAllFields() {
+
+ for (id in fieldValues) {
+ var fieldValue = frame.document.getElementById(id).value;
+ if (changedFields[id] === undefined) {
+ is(fieldValue, fieldValues[id],
+ "Field "+id+" should be restored after reload");
+ } else {
+ is(fieldValue, changedFields[id],
+ "Field "+id+" normally gets a different value after reload");
+ }
+ }
+
+ ok(frame.document.getElementById('radio1').checked,
+ "Radio button radio1 should be restored after reload")
+
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_save_restore_custom_elements.html b/dom/html/test/forms/test_save_restore_custom_elements.html
new file mode 100644
index 0000000000..489ad0ca2f
--- /dev/null
+++ b/dom/html/test/forms/test_save_restore_custom_elements.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1556358
+-->
+
+<head>
+ <title>Test for Bug 1556358</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1556358">Mozilla Bug 1556358</a>
+ <p id="display"></p>
+ <div id="content">
+ <iframe src="save_restore_custom_elements_sample.html"></iframe>
+ </div>
+ <script type="application/javascript">
+ /** Test for Bug 1556358 **/
+
+ function formDataWith(...entries) {
+ const formData = new FormData();
+ for (let [key, value] of entries) {
+ formData.append(key, value);
+ }
+ return formData;
+ }
+
+ const states = [
+ "test state",
+ new File(["state"], "state.txt"),
+ formDataWith(["1", "state"], ["2", new Blob(["state_blob"])]),
+ null,
+ undefined,
+ ];
+ const values = [
+ "test value",
+ new File(["value"], "value.txt"),
+ formDataWith(["1", "value"], ["2", new Blob(["value_blob"])]),
+ "null state",
+ "both value and state",
+ ];
+
+ add_task(async () => {
+ const frame = document.querySelector("iframe");
+ const elementTags = ["c-e", "upgraded-ce"];
+
+ // Set the custom element values.
+ for (const tags of elementTags) {
+ [...frame.contentDocument.querySelectorAll(tags)]
+ .forEach((e, i) => {
+ e.set(states[i], values[i]);
+ });
+ }
+
+ await new Promise(resolve => {
+ frame.addEventListener("load", resolve);
+ frame.contentWindow.location.reload();
+ });
+
+ for (const tag of elementTags) {
+ // Retrieve the restored values.
+ const ceStates =
+ [...frame.contentDocument.querySelectorAll(tag)].map((e) => e.state);
+ is(ceStates.length, 5, "Should have 5 custom element states");
+
+ const [restored, original] = [ceStates, states];
+ is(restored[0], original[0], "Value should be restored");
+
+ const file = restored[1];
+ isnot(file, original[1], "Restored file object differs from original object.");
+ is(file.name, original[1].name, "File name should be restored");
+ is(await file.text(), await original[1].text(), "File text should be restored");
+
+ const formData = restored[2];
+ isnot(formData, original[2], "Restored formdata object differs from original object.");
+ is(formData.get("1"), original[2].get("1"), "Form data string should be restored");
+ is(await formData.get("2").text(), await original[2].get("2").text(), "Form data blob should be restored");
+
+ isnot(restored[3], original[3], "Null values don't get restored");
+ is(restored[3], undefined, "Null values don't get restored");
+
+ is(restored[4], "both value and state", "Undefined state should be set to value");
+ }
+ });
+ </script>
+</body>
+
+</html>
diff --git a/dom/html/test/forms/test_save_restore_radio_groups.html b/dom/html/test/forms/test_save_restore_radio_groups.html
new file mode 100644
index 0000000000..c5ef924a0e
--- /dev/null
+++ b/dom/html/test/forms/test_save_restore_radio_groups.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=350022
+-->
+<head>
+ <title>Test for Bug 350022</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=350022">Mozilla Bug 350022</a>
+<p id="display"></p>
+<div id="content"><!-- style="display: none">-->
+ <iframe src="save_restore_radio_groups.sjs"></iframe>
+ <iframe src="save_restore_radio_groups.sjs"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 350022 **/
+
+function checkRadioGroup(aFrame, aResults)
+{
+ var radios = frames[aFrame].document.getElementsByTagName('input');
+
+ is(radios.length, aResults.length,
+ "Radio group should have " + aResults.length + "elements");
+
+ for (var i=0; i<aResults.length; ++i) {
+ is(radios[i].checked, aResults[i],
+ "Radio checked state should be " + aResults[i]);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ /**
+ * We have two iframes each containing one radio button group.
+ * We are going to change the selected radio button in one group.
+ * Then, both iframes will be reloaded and the new groups will have another
+ * radio checked by default.
+ * For the first group (which had a selection change), nothing should change.
+ * For the second, the selected radio button should change.
+ */
+ checkRadioGroup(0, [true, false, false]);
+ checkRadioGroup(1, [true, false, false]);
+
+ frames[0].document.getElementsByTagName('input')[2].checked = true;
+ checkRadioGroup(0, [false, false, true]);
+
+ framesElts = document.getElementsByTagName('iframe');
+ framesElts[0].addEventListener("load", function() {
+ checkRadioGroup(0, [false, false, true]);
+
+ framesElts[1].addEventListener("load", function() {
+ checkRadioGroup(1, [false, true, false]);
+ SimpleTest.finish();
+ }, {once: true});
+
+ frames[1].location.reload();
+ }, {once: true});
+
+ frames[0].location.reload();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_change_event.html b/dom/html/test/forms/test_select_change_event.html
new file mode 100644
index 0000000000..ec3ed58c5e
--- /dev/null
+++ b/dom/html/test/forms/test_select_change_event.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265968
+-->
+<head>
+ <title>Test for Bug 1265968</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265968">Mozilla Bug 1265968</a>
+<p id="display"></p>
+<div id="content">
+ <select id="select" onchange="++selectChange;">
+ <option>one</option>
+ <option>two</option>
+ <option>three</option>
+ <option>four</option>
+ <option>five</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+ var select = document.getElementById("select");
+ var selectChange = 0;
+ var expectedChange = 0;
+
+ select.focus();
+ for (var i = 1; i < select.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ is(select.options[i].selected, true, "Option should be selected");
+ is(selectChange, ++expectedChange, "Down key should fire change event.");
+ }
+
+ // We are at the end of the list, going down should not fire change event.
+ synthesizeKey("KEY_ArrowDown");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ for (var i = select.length - 2; i >= 0; i--) {
+ synthesizeKey("KEY_ArrowUp");
+ is(select.options[i].selected, true, "Option should be selected");
+ is(selectChange, ++expectedChange, "Up key should fire change event.");
+ }
+
+ // We are at the top of the list, going up should not fire change event.
+ synthesizeKey("KEY_ArrowUp");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_input_change_event.html b/dom/html/test/forms/test_select_input_change_event.html
new file mode 100644
index 0000000000..fcf384e423
--- /dev/null
+++ b/dom/html/test/forms/test_select_input_change_event.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265968
+-->
+<head>
+ <title>Test for Bug 1024350</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1024350">Mozilla Bug 1024350</a>
+<p id="display"></p>
+<div id="content">
+ <select oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ </select>
+ <select oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ <option>two</option>
+ </select>
+ <select multiple size='1' oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ </select>
+ <select multiple oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ <option>two</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+ var selectSingleOneItem = document.getElementsByTagName('select')[0];
+ var selectSingle = document.getElementsByTagName('select')[1];
+ var selectMultipleOneItem = document.getElementsByTagName('select')[2];
+ var selectMultiple = document.getElementsByTagName('select')[3];
+
+ var selectChange = 0;
+ var selectInput = 0;
+ var expectedChange = 0;
+ var expectedInput = 0;
+
+ selectSingleOneItem.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+ selectSingle.focus();
+ for (var i = 1; i < selectSingle.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+
+ is(selectSingle.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Down key should fire input event.");
+ is(selectChange, ++expectedChange, "Down key should fire change event.");
+ }
+
+ // We are at the end of the list, going down should not fire change event.
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ for (var i = selectSingle.length - 2; i >= 0; i--) {
+ synthesizeKey("KEY_ArrowUp");
+
+ is(selectSingle.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Up key should fire input event.");
+ is(selectChange, ++expectedChange, "Up key should fire change event.");
+ }
+
+ // We are at the top of the list, going up should not fire change event.
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+ selectMultipleOneItem.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, ++expectedInput, "Down key should fire input event when reaching end of the list.");
+ is(selectChange, ++expectedChange, "Down key should fire change event when reaching end of the list.");
+
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+ selectMultiple.focus();
+ for (var i = 0; i < selectMultiple.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+
+ is(selectMultiple.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Down key should fire input event.");
+ is(selectChange, ++expectedChange, "Down key should fire change event.");
+ }
+
+ // We are at the end of the list, going down should not fire change event.
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ for (var i = selectMultiple.length - 2; i >= 0; i--) {
+ synthesizeKey("KEY_ArrowUp");
+
+ is(selectMultiple.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Up key should fire input event.");
+ is(selectChange, ++expectedChange, "Up key should fire change event.");
+ }
+
+ // We are at the top of the list, going up should not fire change event.
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_selectedOptions.html b/dom/html/test/forms/test_select_selectedOptions.html
new file mode 100644
index 0000000000..745e0ba4f3
--- /dev/null
+++ b/dom/html/test/forms/test_select_selectedOptions.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596681
+-->
+<head>
+ <title>Test for HTMLSelectElement.selectedOptions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596681">Mozilla Bug 596681</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLSelectElement's selectedOptions attribute.
+ *
+ * selectedOptions is a live list of the options that have selectedness of true
+ * (not the selected content attribute).
+ *
+ * See http://www.whatwg.org/html/#dom-select-selectedoptions
+ **/
+
+function checkSelectedOptions(size, elements)
+{
+ is(selectedOptions.length, size,
+ "select should have " + size + " selected options");
+ for (let i = 0; i < size; ++i) {
+ ok(selectedOptions[i], "selected option is valid");
+ if (selectedOptions[i]) {
+ is(selectedOptions[i].value, elements[i].value, "selected options are correct");
+ }
+ }
+}
+
+let select = document.createElement("select");
+document.body.appendChild(select);
+let selectedOptions = select.selectedOptions;
+
+ok("selectedOptions" in select,
+ "select element should have a selectedOptions IDL attribute");
+
+ok(select.selectedOptions instanceof HTMLCollection,
+ "selectedOptions should be an HTMLCollection instance");
+
+let option1 = document.createElement("option");
+let option2 = document.createElement("option");
+let option3 = document.createElement("option");
+option1.id = "option1";
+option1.value = "option1";
+option2.value = "option2";
+option3.value = "option3";
+
+checkSelectedOptions(0, null);
+
+select.add(option1, null);
+is(selectedOptions.namedItem("option1").value, "option1", "named getter works");
+checkSelectedOptions(1, [option1]);
+
+select.add(option2, null);
+checkSelectedOptions(1, [option1]);
+
+select.options[1].selected = true;
+checkSelectedOptions(1, [option2]);
+
+select.multiple = true;
+checkSelectedOptions(1, [option2]);
+
+select.options[0].selected = true;
+checkSelectedOptions(2, [option1, option2]);
+
+option1.selected = false;
+// Usinig selected directly on the option should work.
+checkSelectedOptions(1, [option2]);
+
+select.remove(1);
+select.add(option2, 0);
+select.options[0].selected = true;
+select.options[1].selected = true;
+// Should be in tree order.
+checkSelectedOptions(2, [option2, option1]);
+
+select.add(option3, null);
+checkSelectedOptions(2, [option2, option1]);
+
+select.options[2].selected = true;
+checkSelectedOptions(3, [option2, option1, option3]);
+
+select.length = 0;
+option1.selected = false;
+option2.selected = false;
+option3.selected = false;
+var optgroup1 = document.createElement("optgroup");
+optgroup1.appendChild(option1);
+optgroup1.appendChild(option2);
+select.add(optgroup1)
+var optgroup2 = document.createElement("optgroup");
+optgroup2.appendChild(option3);
+select.add(optgroup2);
+
+checkSelectedOptions(0, null);
+
+option2.selected = true;
+checkSelectedOptions(1, [option2]);
+
+option3.selected = true;
+checkSelectedOptions(2, [option2, option3]);
+
+optgroup1.removeChild(option2);
+checkSelectedOptions(1, [option3]);
+
+document.body.removeChild(select);
+option1.selected = true;
+checkSelectedOptions(2, [option1, option3]);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_validation.html b/dom/html/test/forms/test_select_validation.html
new file mode 100644
index 0000000000..6d02aa0746
--- /dev/null
+++ b/dom/html/test/forms/test_select_validation.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=942321
+-->
+<head>
+ <title>Test for Bug 942321</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942321">Mozilla Bug 942321</a>
+<p id="display"></p>
+<form id="form" href="">
+ <select required id="testselect">
+ <option id="placeholder" value="" selected>placeholder</option>
+ <option value="test" id="actualvalue">test</option>
+ <select>
+ <input type="submit" />
+</form>
+<script class="testbody" type="text/javascript">
+/** Test for Bug 942321 **/
+var option = document.getElementById("actualvalue");
+option.selected = true;
+is(form.checkValidity(), true, "Select is required and should be valid");
+
+var placeholder = document.getElementById("placeholder");
+placeholder.selected = true;
+is(form.checkValidity(), false, "Select is required and should be invalid");
+
+placeholder.value = "not-invalid-anymore";
+is(form.checkValidity(), true, "Select is required and should be valid when option's value is changed by javascript");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_set_range_text.html b/dom/html/test/forms/test_set_range_text.html
new file mode 100644
index 0000000000..f85014ae77
--- /dev/null
+++ b/dom/html/test/forms/test_set_range_text.html
@@ -0,0 +1,242 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=850364
+-->
+<head>
+<title>Tests for Bug 850364 && Bug 918940</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>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850364">Mozilla Bug 850364</a>
+<p id="display"></p>
+<div id="content">
+
+<!-- "SetRangeText() supported types"-->
+<input type="text" id="input_text"></input>
+<input type="search" id="input_search"></input>
+<input type="url" id="input_url"></input>
+<input type="tel" id="input_tel"></input>
+<input type="password" id="input_password"></input>
+<textarea id="input_textarea"></textarea>
+
+<!-- "SetRangeText() non-supported types" -->
+<input type="button" id="input_button"></input>
+<input type="submit" id="input_submit"></input>
+<input type="image" id="input_image"></input>
+<input type="reset" id="input_reset"></input>
+<input type="radio" id="input_radio"></input>
+<input type="checkbox" id="input_checkbox"></input>
+<input type="range" id="input_range"></input>
+<input type="file" id="input_file"></input>
+<input type="email" id="input_email"></input>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Tests for Bug 850364 && Bug 918940**/
+
+ var SupportedTypes = ["text", "search", "url", "tel", "password", "textarea"];
+ var NonSupportedTypes = ["button", "submit", "image", "reset", "radio",
+ "checkbox", "range", "file", "email"];
+
+ SimpleTest.waitForExplicitFinish();
+
+ function TestInputs() {
+
+ var opThrows, elem, i, msg;
+
+ //Non-supported types should throw
+ for (i = 0; i < NonSupportedTypes.length; ++i) {
+ opThrows = false;
+ msg = "input_" + NonSupportedTypes[i];
+ elem = document.getElementById(msg);
+ elem.focus();
+ try {
+ elem.setRangeText("abc");
+ } catch (ex) {
+ opThrows = true;
+ }
+ ok(opThrows, msg + " should throw InvalidStateError");
+ }
+
+ var numOfSelectCalls = 0, expectedNumOfSelectCalls = 0;
+ //Supported types should not throw
+ for (i = 0; i < SupportedTypes.length; ++i) {
+ opThrows = false;
+ msg = "input_" + SupportedTypes[i];
+ elem = document.getElementById(msg);
+ elem.focus();
+ try {
+ elem.setRangeText("abc");
+ } catch (ex) {
+ opThrows = true;
+ }
+ is(opThrows, false, msg + " should not throw InvalidStateError");
+
+ elem.addEventListener("select", function (aEvent) {
+ ok(true, "select event should be fired for " + aEvent.target.id);
+ if (++numOfSelectCalls == expectedNumOfSelectCalls) {
+ SimpleTest.finish();
+ } else if (numOfSelectCalls > expectedNumOfSelectCalls) {
+ ok(false, "Too many select events were fired");
+ }
+ });
+
+ elem.addEventListener("input", function (aEvent) {
+ ok(false, "input event should NOT be fired for " + + aEvent.target.id);
+ });
+
+ var test = " setRange(replacement), shrink";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(1, 6);
+ elem.setRangeText("xyz");
+ is(elem.value, "0xyz6789ABCDEF", msg + test);
+ is(elem.selectionStart, 1, msg + test);
+ is(elem.selectionEnd, 4, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0mnk6789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 2;
+
+ test = " setRange(replacement), expand";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(1, 2);
+ elem.setRangeText("xyz");
+ is(elem.value, "0xyz23456789ABCDEF", msg + test);
+ is(elem.selectionStart, 1, msg + test);
+ is(elem.selectionEnd, 4, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0mnk23456789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 2;
+
+ test = " setRange(replacement) pure insertion at start";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(0, 0);
+ elem.setRangeText("xyz");
+ is(elem.value, "xyz0123456789ABCDEF", msg + test);
+ is(elem.selectionStart, 0, msg + test);
+ is(elem.selectionEnd, 0, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "mnkxyz0123456789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 1;
+
+ test = " setRange(replacement) pure insertion in the middle";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(4, 4);
+ elem.setRangeText("xyz");
+ is(elem.value, "0123xyz456789ABCDEF", msg + test);
+ is(elem.selectionStart, 4, msg + test);
+ is(elem.selectionEnd, 4, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0123mnkxyz456789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 1;
+
+ test = " setRange(replacement) pure insertion at the end";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(16, 16);
+ elem.setRangeText("xyz");
+ is(elem.value, "0123456789ABCDEFxyz", msg + test);
+ is(elem.selectionStart, 16, msg + test);
+ is(elem.selectionEnd, 16, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0123456789ABCDEFmnkxyz", msg + test);
+
+ //test SetRange(replacement, start, end, mode) with start > end
+ try {
+ elem.setRangeText("abc", 20, 4);
+ } catch (ex) {
+ opThrows = (ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR);
+ }
+ is(opThrows, true, msg + " should throw IndexSizeError");
+
+ //test SelectionMode 'select'
+ elem.value = "0123456789ABCDEF";
+ elem.setRangeText("xyz", 4, 9, "select");
+ is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\"");
+ is(elem.selectionStart, 4, msg + ".selectionStart == 4, with \"select\"");
+ is(elem.selectionEnd, 7, msg + ".selectionEnd == 7, with \"select\"");
+ expectedNumOfSelectCalls += 1;
+
+ elem.setRangeText("pqm", 6, 25, "select");
+ is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\"");
+ is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"select\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"select\"");
+ expectedNumOfSelectCalls += 1;
+
+ //test SelectionMode 'start'
+ elem.value = "0123456789ABCDEF";
+ elem.setRangeText("xyz", 4, 9, "start");
+ is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\"");
+ is(elem.selectionStart, 4, msg + ".selectionStart == 4, with \"start\"");
+ is(elem.selectionEnd, 4, msg + ".selectionEnd == 4, with \"start\"");
+ expectedNumOfSelectCalls += 1;
+
+ elem.setRangeText("pqm", 6, 25, "start");
+ is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\"");
+ is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"start\"");
+ is(elem.selectionEnd, 6, msg + ".selectionEnd == 6, with \"start\"");
+ expectedNumOfSelectCalls += 1;
+
+ //test SelectionMode 'end'
+ elem.value = "0123456789ABCDEF";
+ elem.setRangeText("xyz", 4, 9, "end");
+ is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\"");
+ is(elem.selectionStart, 7, msg + ".selectionStart == 7, with \"end\"");
+ is(elem.selectionEnd, 7, msg + ".selectionEnd == 7, with \"end\"");
+ expectedNumOfSelectCalls += 1;
+
+ elem.setRangeText("pqm", 6, 25, "end");
+ is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\"");
+ is(elem.selectionStart, 9, msg + ".selectionStart == 9, with \"end\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"end\"");
+ expectedNumOfSelectCalls += 1;
+
+ //test SelectionMode 'preserve' (default)
+
+ //subcase: selection{Start|End} > end
+ elem.value = "0123456789";
+ elem.setSelectionRange(6, 9);
+ elem.setRangeText("Z", 1, 2, "preserve");
+ is(elem.value, "0Z23456789", msg + ".value == \"0Z23456789\"");
+ is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"preserve\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"preserve\"");
+ expectedNumOfSelectCalls += 1;
+
+ //subcase: selection{Start|End} < end
+ elem.value = "0123456789";
+ elem.setSelectionRange(4, 5);
+ elem.setRangeText("QRST", 2, 9, "preserve");
+ is(elem.value, "01QRST9", msg + ".value == \"01QRST9\"");
+ is(elem.selectionStart, 2, msg + ".selectionStart == 2, with \"preserve\"");
+ is(elem.selectionEnd, 6, msg + ".selectionEnd == 6, with \"preserve\"");
+ expectedNumOfSelectCalls += 2;
+
+ //subcase: selectionStart > end, selectionEnd < end
+ elem.value = "0123456789";
+ elem.setSelectionRange(8, 4);
+ elem.setRangeText("QRST", 1, 5);
+ is(elem.value, "0QRST56789", msg + ".value == \"0QRST56789\"");
+ is(elem.selectionStart, 1, msg + ".selectionStart == 1, with \"default\"");
+ is(elem.selectionEnd, 5, msg + ".selectionEnd == 5, with \"default\"");
+ expectedNumOfSelectCalls += 2;
+
+ //subcase: selectionStart < end, selectionEnd > end
+ elem.value = "0123456789";
+ elem.setSelectionRange(4, 9);
+ elem.setRangeText("QRST", 2, 6);
+ is(elem.value, "01QRST6789", msg + ".value == \"01QRST6789\"");
+ is(elem.selectionStart, 2, msg + ".selectionStart == 2, with \"default\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"default\"");
+ expectedNumOfSelectCalls += 2;
+ }
+ }
+
+ addLoadEvent(TestInputs);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_step_attribute.html b/dom/html/test/forms/test_step_attribute.html
new file mode 100644
index 0000000000..f0af250c06
--- /dev/null
+++ b/dom/html/test/forms/test_step_attribute.html
@@ -0,0 +1,1060 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635553
+-->
+<head>
+ <title>Test for Bug 635553</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635553 **/
+
+var data = [
+ { type: 'hidden', apply: false },
+ { type: 'text', apply: false },
+ { type: 'search', apply: false },
+ { type: 'tel', apply: false },
+ { type: 'url', apply: false },
+ { type: 'email', apply: false },
+ { type: 'password', apply: false },
+ { type: 'date', apply: true },
+ { type: 'month', apply: true },
+ { type: 'week', apply: true },
+ { type: 'time', apply: true },
+ { type: 'datetime-local', apply: true },
+ { type: 'number', apply: true },
+ { type: 'range', apply: true },
+ { type: 'color', apply: false },
+ { type: 'checkbox', apply: false },
+ { type: 'radio', apply: false },
+ { type: 'file', apply: false },
+ { type: 'submit', apply: false },
+ { type: 'image', apply: false },
+ { type: 'reset', apply: false },
+ { type: 'button', apply: false },
+];
+
+function getFreshElement(type) {
+ var elmt = document.createElement('input');
+ elmt.type = type;
+ return elmt;
+}
+
+function checkValidity(aElement, aValidity, aApply, aData)
+{
+ aValidity = aApply ? aValidity : true;
+
+ is(aElement.validity.valid, aValidity,
+ "element validity should be " + aValidity);
+ is(aElement.validity.stepMismatch, !aValidity,
+ "element step mismatch status should be " + !aValidity);
+
+ if (aValidity) {
+ is(aElement.validationMessage, "", "There should be no validation message.");
+ } else {
+ if (aElement.validity.rangeUnderflow) {
+ var underflowMsg =
+ (aElement.type == "date" || aElement.type == "time") ?
+ ("Please select a value that is no earlier than " + aElement.min + ".") :
+ ("Please select a value that is no less than " + aElement.min + ".");
+ is(aElement.validationMessage, underflowMsg,
+ "Checking range underflow validation message.");
+ } else if (aData.low == aData.high) {
+ is(aElement.validationMessage, "Please select a valid value. " +
+ "The nearest valid value is " + aData.low + ".",
+ "There should be a validation message.");
+ } else {
+ is(aElement.validationMessage, "Please select a valid value. " +
+ "The two nearest valid values are " + aData.low + " and " + aData.high + ".",
+ "There should be a validation message.");
+ }
+ }
+
+ is(aElement.matches(":valid"), aElement.willValidate && aValidity,
+ (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply");
+ is(aElement.matches(":invalid"), aElement.willValidate && !aValidity,
+ (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply");
+}
+
+for (var test of data) {
+ var input = getFreshElement(test.type);
+ var apply = test.apply;
+
+ if (test.todo) {
+ todo_is(input.type, test.type, test.type + " isn't implemented yet");
+ continue;
+ }
+
+ // The element should be valid, there should be no step mismatch.
+ checkValidity(input, true, apply);
+
+ // Checks to do for all types that support step:
+ // - check for @step=0,
+ // - check for @step behind removed,
+ // - check for @step being 'any' with different case variations.
+ switch (input.type) {
+ case 'text':
+ case 'hidden':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'radio':
+ case 'checkbox':
+ case 'reset':
+ case 'button':
+ case 'submit':
+ case 'image':
+ case 'color':
+ input.value = '0';
+ checkValidity(input, true, apply);
+ break;
+ case 'url':
+ input.value = 'http://mozilla.org';
+ checkValidity(input, true, apply);
+ break;
+ case 'email':
+ input.value = 'foo@bar.com';
+ checkValidity(input, true, apply);
+ break;
+ case 'file':
+ var file = new File([''], '635499_file');
+
+ SpecialPowers.wrap(input).mozSetFileArray([file]);
+ checkValidity(input, true, apply);
+
+ break;
+ case 'date':
+ // For date, the step is calulated on the timestamp since 1970-01-01
+ // which mean that for all dates prior to the epoch, this timestamp is < 0
+ // and the behavior might differ, therefore we have to test for these cases.
+
+ // When step is invalid, every date is valid
+ input.step = 0;
+ input.value = '2012-07-05';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-01-01';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1969-12-12';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-01-01';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-12-12';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2013-02-03';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid date, there is a step base.
+ input.min = '2008-02-28';
+ input.step = '2';
+ input.value = '2008-03-01';
+ checkValidity(input, true, apply);
+
+ input.value = '2008-02-29';
+ checkValidity(input, false, apply, { low: "2008-02-28", high: "2008-03-01" });
+
+ input.min = '2008-02-27';
+ input.value = '2008-02-28';
+ checkValidity(input, false, apply, { low: "2008-02-27", high: "2008-02-29" });
+
+ input.min = '2009-02-27';
+ input.value = '2009-02-28';
+ checkValidity(input, false, apply, { low: "2009-02-27", high: "2009-03-01" });
+
+ input.min = '2009-02-01';
+ input.step = '1.1';
+ input.value = '2009-02-02';
+ checkValidity(input, true, apply);
+
+ // Without any step attribute the date is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-01-01';
+ input.step = '366';
+ input.value = '1951-01-01';
+ checkValidity(input, false, apply, { low: "1950-01-01", high: "1951-01-02" });
+
+ input.min = '1951-01-01';
+ input.step = '365';
+ input.value = '1952-01-01';
+ checkValidity(input, true, apply);
+
+ input.step = '0.9';
+ input.value = '1951-01-02';
+ is(input.step, '0.9', "check that step value is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = '0.4';
+ input.value = '1951-01-02';
+ is(input.step, '0.4', "check that step value is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = '1.5';
+ input.value = '1951-01-02';
+ is(input.step, '1.5', "check that step value is unchanged");
+ checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-03" });
+
+ input.value = '1951-01-08';
+ checkValidity(input, false, apply, { low: "1951-01-07", high: "1951-01-09" });
+
+ input.step = '3000';
+ input.min= '1968-01-01';
+ input.value = '1968-05-12';
+ checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
+
+ input.value = '1971-01-01';
+ checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
+
+ input.value = '1991-01-01';
+ checkValidity(input, false, apply, { low: "1984-06-05", high: "1992-08-22" });
+
+ input.value = '1984-06-05';
+ checkValidity(input, true, apply);
+
+ input.value = '1992-08-22';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-01-01';
+ input.value = '1991-01-01';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-01-02';
+ checkValidity(input, false, apply, { low: "1991-01-01", high: "1991-01-03" });
+
+ input.value = '1991-01-03';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-12-20';
+ input.value = '1969-12-20';
+ checkValidity(input, true, apply);
+
+ input.value = '1969-12-21';
+ checkValidity(input, false, apply, { low: "1969-12-20", high: "1969-12-22" });
+
+ input.value = '1969-12-22';
+ checkValidity(input, true, apply);
+
+ break;
+ case 'number':
+ // When step=0, the allowed step is 1.
+ input.step = '0';
+ input.value = '1.2';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '0';
+ checkValidity(input, true, apply);
+
+ // When step is NaN, the allowed step value is 1.
+ input.step = 'foo';
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '1.5';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ // When step is negative, the allowed step value is 1.
+ input.step = '-0.1';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ // When step is missing, the allowed step value is 1.
+ input.removeAttribute('step');
+ input.value = '1.5';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ // When step is 'any', all values are fine wrt to step.
+ input.step = 'any';
+ checkValidity(input, true, apply);
+
+ input.step = 'aNy';
+ input.value = '1337';
+ checkValidity(input, true, apply);
+
+ input.step = 'AnY';
+ input.value = '0.1';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '-13.37';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid float, there is a step base.
+ input.min = '1';
+ input.step = '2';
+ input.value = '3';
+ checkValidity(input, true, apply);
+
+ input.value = '2';
+ checkValidity(input, false, apply, { low: 1, high: 3 });
+
+ input.removeAttribute('step'); // step = 1
+ input.min = '0.5';
+ input.value = '5.5';
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ checkValidity(input, false, apply, { low: 0.5, high: 1.5 });
+
+ input.min = '-0.1';
+ input.step = '1';
+ input.value = '0.9';
+ checkValidity(input, true, apply);
+
+ input.value = '0.1';
+ checkValidity(input, false, apply, { low: -0.1, high: 0.9 });
+
+ // When min is set to NaN, there is no step base (step base=0 actually).
+ input.min = 'foo';
+ input.step = '1';
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ checkValidity(input, false, apply, { low: 0, high: 1 });
+
+ input.min = '';
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ checkValidity(input, false, apply, { low: 0, high: 1 });
+
+ input.removeAttribute('min');
+
+ // If value isn't a number, the element isn't invalid.
+ input.value = '';
+ checkValidity(input, true, apply);
+
+ // Regular situations.
+ input.step = '2';
+ input.value = '1.5';
+ checkValidity(input, false, apply, { low: 0, high: 2 });
+
+ input.value = '42.0';
+ checkValidity(input, true, apply);
+
+ input.step = '0.1';
+ input.value = '-0.1';
+ checkValidity(input, true, apply);
+
+ input.step = '2';
+ input.removeAttribute('min');
+ input.max = '10';
+ input.value = '-9';
+ checkValidity(input, false, apply, {low: -10, high: -8});
+
+ // If there is a value defined but no min, the step base is the value.
+ input = getFreshElement(test.type);
+ input.setAttribute('value', '1');
+ input.step = 2;
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ checkValidity(input, false, apply, {low: 1, high: 3});
+
+ // Should also work with defaultValue.
+ input = getFreshElement(test.type);
+ input.defaultValue = 1;
+ input.step = 2;
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ checkValidity(input, false, apply, {low: 1, high: 3});
+
+ // Rounding issues.
+ input = getFreshElement(test.type);
+ input.min = 0.1;
+ input.step = 0.2;
+ input.value = 0.3;
+ checkValidity(input, true, apply);
+
+ // Check that when the higher value is higher than max, we don't show it.
+ input = getFreshElement(test.type);
+ input.step = '2';
+ input.min = '1';
+ input.max = '10.9';
+ input.value = '10';
+
+ is(input.validationMessage, "Please select a valid value. " +
+ "The nearest valid value is 9.",
+ "The validation message should not include the higher value.");
+ break;
+ case 'range':
+ // Range is special in that it clamps to valid values, so it is much
+ // rarer for it to be invalid.
+
+ // When step=0, the allowed value step is 1.
+ input.step = '0';
+ input.value = '1.2';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '0';
+ is(input.value, '0', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ // When step is NaN, the allowed step value is 1.
+ input.step = 'foo';
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '1.5';
+ is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // When step is negative, the allowed step value is 1.
+ input.step = '-0.1';
+ is(input.value, '2', "check that the value still coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ // When step is missing, the allowed step value is 1.
+ input.removeAttribute('step');
+ input.value = '1.5';
+ is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ // When step is 'any', all values are fine wrt to step.
+ input.step = 'any';
+ checkValidity(input, true, apply);
+
+ input.step = 'aNy';
+ input.value = '97';
+ is(input.value, '97', "check that the value for step=aNy is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = 'AnY';
+ input.value = '0.1';
+ is(input.value, '0.1', "check that a positive fractional value with step=AnY is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.min = -100;
+ input.value = '-13.37';
+ is(input.value, '-13.37', "check that a negative fractional value with step=ANY is unchanged");
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid float, there is a step base.
+ input.min = '1'; // the step base
+ input.step = '2';
+ input.value = '3';
+ is(input.value, '3', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '2';
+ is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '1.99';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step'); // step = 1
+ input.min = '0.5'; // step base
+ input.value = '5.5';
+ is(input.value, '5.5', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1.5', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.min = '-0.1'; // step base
+ input.step = '1';
+ input.value = '0.9';
+ is(input.value, '0.9', "the value should be a valid step");
+ checkValidity(input, true, apply);
+
+ input.value = '0.1';
+ is(input.value, '-0.1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // When min is set to NaN, the step base is the value.
+ input.min = 'foo';
+ input.step = '1';
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.min = '';
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('min');
+
+ // Test when the value isn't a number
+ input.value = '';
+ is(input.value, '50', "value be should default to the value midway between the minimum (0) and the maximum (100)");
+ checkValidity(input, true, apply);
+
+ // Regular situations.
+ input.step = '2';
+ input.value = '1.5';
+ is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '42.0';
+ is(input.value, '42.0', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.step = '0.1';
+ input.value = '-0.1';
+ is(input.value, '0', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.step = '2';
+ input.removeAttribute('min');
+ input.max = '10';
+ input.value = '-9';
+ is(input.value, '0', "check the value is clamped to the minimum's default of zero");
+ checkValidity(input, true, apply);
+
+ // If @value is defined but not @min, the step base is @value.
+ input = getFreshElement(test.type);
+ input.setAttribute('value', '1');
+ input.step = 2;
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ is(input.value, '3', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // Should also work with defaultValue.
+ input = getFreshElement(test.type);
+ input.defaultValue = 1;
+ input.step = 2;
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ is(input.value, '3', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // Check contrived error case where there are no valid steps in range:
+ // No @min, so the step base is the default minimum, zero, the valid
+ // range is 0-1, -1 gets clamped to zero.
+ input = getFreshElement(test.type);
+ input.step = '3';
+ input.max = '1';
+ input.defaultValue = '-1';
+ is(input.value, '0', "the value should have been clamped to the default minimum, zero");
+ checkValidity(input, false, apply, {low: -1, high: -1});
+
+ // Check that when the closest of the two steps that the value is between
+ // is greater than the maximum we sanitize to the lower step.
+ input = getFreshElement(test.type);
+ input.step = '2';
+ input.min = '1';
+ input.max = '10.9';
+ input.value = '10.8'; // closest step in 11, but 11 > maximum
+ is(input.value, '9', "check that the value coincides with a step");
+
+ // The way that step base is defined, the converse (the value not being
+ // on a step, and the nearest step being a value that would be underflow)
+ // is not possible, so nothing to test there.
+
+ is(input.validationMessage, "",
+ "The validation message should be empty.");
+ break;
+ case 'time':
+ // Tests invalid step values. That defaults to step = 1 minute (60).
+ var values = [ '0', '-1', 'foo', 'any', 'ANY', 'aNy' ];
+ for (var value of values) {
+ input.step = value;
+ input.value = '19:06:00';
+ checkValidity(input, true, apply);
+ input.value = '19:06:51';
+ if (value.toLowerCase() != 'any') {
+ checkValidity(input, false, apply, {low: '19:06', high: '19:07'});
+ } else {
+ checkValidity(input, true, apply);
+ }
+ }
+
+ // No step means that we use the default step value.
+ input.removeAttribute('step');
+ input.value = '19:06:00';
+ checkValidity(input, true, apply);
+ input.value = '19:06:51';
+ checkValidity(input, false, apply, {low: '19:06', high: '19:07'});
+
+ var tests = [
+ // With step=1, we allow values by the second.
+ { step: '1', value: '19:11:01', min: '00:00', result: true },
+ { step: '1', value: '19:11:01.001', min: '00:00', result: false,
+ low: '19:11:01', high: '19:11:02' },
+ { step: '1', value: '19:11:01.1', min: '00:00', result: false,
+ low: '19:11:01', high: '19:11:02' },
+ // When step >= 86400000, only the minimum value is valid.
+ // This is actually @value if there is no @min.
+ { step: '86400000', value: '00:00', result: true },
+ { step: '86400000', value: '00:01', result: true },
+ { step: '86400000', value: '00:00', min: '00:01', result: false },
+ { step: '86400000', value: '00:01', min: '00:00', result: false,
+ low: '00:00', high: '00:00' },
+ // When step < 1, it should just work.
+ { step: '0.1', value: '15:05:05.1', min: '00:00', result: true },
+ { step: '0.1', value: '15:05:05.101', min: '00:00', result: false,
+ low: '15:05:05.100', high: '15:05:05.200' },
+ { step: '0.2', value: '15:05:05.2', min: '00:00', result: true },
+ { step: '0.2', value: '15:05:05.1', min: '00:00', result: false,
+ low: '15:05:05', high: '15:05:05.200' },
+ { step: '0.01', value: '15:05:05.01', min: '00:00', result: true },
+ { step: '0.01', value: '15:05:05.011', min: '00:00', result: false,
+ low: '15:05:05.010', high: '15:05:05.020' },
+ { step: '0.02', value: '15:05:05.02', min: '00:00', result: true },
+ { step: '0.02', value: '15:05:05.01', min: '00:00', result: false,
+ low: '15:05:05', high: '15:05:05.020' },
+ { step: '0.002', value: '15:05:05.002', min: '00:00', result: true },
+ { step: '0.002', value: '15:05:05.001', min: '00:00', result: false,
+ low: '15:05:05', high: '15:05:05.002' },
+ // When step<=0.001, any value is allowed.
+ { step: '0.001', value: '15:05:05.001', min: '00:00', result: true },
+ { step: '0.001', value: '15:05:05', min: '00:00', result: true },
+ { step: '0.000001', value: '15:05:05', min: '00:00', result: true },
+ // This value has conversion to double issues.
+ { step: '0.0000001', value: '15:05:05', min: '00:00', result: true },
+ // Some random values.
+ { step: '100', value: '15:06:40', min: '00:00', result: true },
+ { step: '100', value: '15:05:05.010', min: '00:00', result: false,
+ low: '15:05', high: '15:06:40' },
+ { step: '3600', value: '15:00', min: '00:00', result: true },
+ { step: '3600', value: '15:14', min: '00:00', result: false,
+ low: '15:00', high: '16:00' },
+ { step: '7200', value: '14:00', min: '00:00', result: true },
+ { step: '7200', value: '15:14', min: '00:00', result: false,
+ low: '14:00', high: '16:00' },
+ { step: '7260', value: '14:07', min: '00:00', result: true },
+ { step: '7260', value: '15:14', min: '00:00', result: false,
+ low: '14:07', high: '16:08' },
+ ];
+
+ var type = test.type;
+ for (var test of tests) {
+ var input = getFreshElement(type);
+ input.step = test.step;
+ input.setAttribute('value', test.value);
+ if (test.min !== undefined) {
+ input.min = test.min;
+ }
+
+ if (test.todo) {
+ todo(input.validity.valid, test.result,
+ "This test should fail for the moment because of precission issues");
+ continue;
+ }
+
+ if (test.result) {
+ checkValidity(input, true, apply);
+ } else {
+ checkValidity(input, false, apply,
+ { low: test.low, high: test.high });
+ }
+ }
+
+ break;
+ case 'month':
+ // When step is invalid, every date is valid
+ input.step = 0;
+ input.value = '2016-07';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-01';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1970-01';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-01';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-12';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2013-02';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid month, there is a step base.
+ input.min = '2000-01';
+ input.step = '2';
+ input.value = '2000-03';
+ checkValidity(input, true, apply);
+
+ input.value = '2000-02';
+ checkValidity(input, false, apply, { low: "2000-01", high: "2000-03" });
+
+ input.min = '2012-12';
+ input.value = '2013-01';
+ checkValidity(input, false, apply, { low: "2012-12", high: "2013-02" });
+
+ input.min = '2010-10';
+ input.value = '2010-11';
+ checkValidity(input, false, apply, { low: "2010-10", high: "2010-12" });
+
+ input.min = '2010-01';
+ input.step = '1.1';
+ input.value = '2010-02';
+ checkValidity(input, true, apply);
+
+ input.min = '2010-05';
+ input.step = '1.9';
+ input.value = '2010-06';
+ checkValidity(input, false, apply, { low: "2010-05", high: "2010-07" });
+
+ // Without any step attribute the date is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-01';
+ input.step = '13';
+ input.value = '1951-01';
+ checkValidity(input, false, apply, { low: "1950-01", high: "1951-02" });
+
+ input.min = '1951-01';
+ input.step = '12';
+ input.value = '1952-01';
+ checkValidity(input, true, apply);
+
+ input.step = '0.9';
+ input.value = '1951-02';
+ checkValidity(input, true, apply);
+
+ input.step = '1.5';
+ input.value = '1951-04';
+ checkValidity(input, false, apply, { low: "1951-03", high: "1951-05" });
+
+ input.value = '1951-08';
+ checkValidity(input, false, apply, { low: "1951-07", high: "1951-09" });
+
+ input.step = '300';
+ input.min= '1968-01';
+ input.value = '1968-05';
+ checkValidity(input, false, apply, { low: "1968-01", high: "1993-01" });
+
+ input.value = '1971-01';
+ checkValidity(input, false, apply, { low: "1968-01", high: "1993-01" });
+
+ input.value = '1994-01';
+ checkValidity(input, false, apply, { low: "1993-01", high: "2018-01" });
+
+ input.value = '2018-01';
+ checkValidity(input, true, apply);
+
+ input.value = '2043-01';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-01';
+ input.value = '1991-01';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-02';
+ checkValidity(input, false, apply, { low: "1991-01", high: "1991-03" });
+
+ input.value = '1991-03';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-12';
+ input.value = '1969-12';
+ checkValidity(input, true, apply);
+
+ input.value = '1970-01';
+ checkValidity(input, false, apply, { low: "1969-12", high: "1970-02" });
+
+ input.value = '1970-02';
+ checkValidity(input, true, apply);
+
+ break;
+ case 'week':
+ // When step is invalid, every week is valid
+ input.step = 0;
+ input.value = '2016-W30';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-W01';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1970-W01';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-W01';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-W52';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2013-W10';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid week, there is a step base.
+ input.min = '2000-W01';
+ input.step = '2';
+ input.value = '2000-W03';
+ checkValidity(input, true, apply);
+
+ input.value = '2000-W02';
+ checkValidity(input, false, apply, { low: "2000-W01", high: "2000-W03" });
+
+ input.min = '2012-W52';
+ input.value = '2013-W01';
+ checkValidity(input, false, apply, { low: "2012-W52", high: "2013-W02" });
+
+ input.min = '2010-W01';
+ input.step = '1.1';
+ input.value = '2010-W02';
+ checkValidity(input, true, apply);
+
+ input.min = '2010-W05';
+ input.step = '1.9';
+ input.value = '2010-W06';
+ checkValidity(input, false, apply, { low: "2010-W05", high: "2010-W07" });
+
+ // Without any step attribute the week is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-W01';
+ input.step = '53';
+ input.value = '1951-W01';
+ checkValidity(input, false, apply, { low: "1950-W01", high: "1951-W02" });
+
+ input.min = '1951-W01';
+ input.step = '52';
+ input.value = '1952-W01';
+ checkValidity(input, true, apply);
+
+ input.step = '0.9';
+ input.value = '1951-W02';
+ checkValidity(input, true, apply);
+
+ input.step = '1.5';
+ input.value = '1951-W04';
+ checkValidity(input, false, apply, { low: "1951-W03", high: "1951-W05" });
+
+ input.value = '1951-W20';
+ checkValidity(input, false, apply, { low: "1951-W19", high: "1951-W21" });
+
+ input.step = '300';
+ input.min= '1968-W01';
+ input.value = '1968-W05';
+ checkValidity(input, false, apply, { low: "1968-W01", high: "1973-W40" });
+
+ input.value = '1971-W01';
+ checkValidity(input, false, apply, { low: "1968-W01", high: "1973-W40" });
+
+ input.value = '1975-W01';
+ checkValidity(input, false, apply, { low: "1973-W40", high: "1979-W27" });
+
+ input.value = '1985-W14';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-W01';
+ input.value = '1991-W01';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-W02';
+ checkValidity(input, false, apply, { low: "1991-W01", high: "1991-W03" });
+
+ input.value = '1991-W03';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-W52';
+ input.value = '1969-W52';
+ checkValidity(input, true, apply);
+
+ input.value = '1970-W01';
+ checkValidity(input, false, apply, { low: "1969-W52", high: "1970-W02" });
+
+ input.value = '1970-W02';
+ checkValidity(input, true, apply);
+
+ break;
+ case 'datetime-local':
+ // When step is invalid, every datetime is valid
+ input.step = 0;
+ input.value = '2017-02-06T12:00';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-01-01T00:00';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1969-12-12 00:10';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-01-01T12:00';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-12-12T12:00';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2017-01-01 12:00';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid datetime, there is a step base.
+ input.min = '2017-01-01T00:00:00';
+ input.step = '2';
+ input.value = '2017-01-01T00:00:02';
+ checkValidity(input, true, apply);
+
+ input.value = '2017-01-01T00:00:03';
+ checkValidity(input, false, apply,
+ { low: "2017-01-01T00:00:02", high: "2017-01-01T00:00:04" });
+
+ input.min = '2017-01-01T00:00:05';
+ input.value = '2017-01-01T00:00:08';
+ checkValidity(input, false, apply,
+ { low: "2017-01-01T00:00:07", high: "2017-01-01T00:00:09" });
+
+ input.min = '2000-01-01T00:00';
+ input.step = '120';
+ input.value = '2000-01-01T00:02';
+ checkValidity(input, true, apply);
+
+ // Without any step attribute the datetime is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-01-01T00:00';
+ input.step = '129600'; // 1.5 day
+ input.value = '1950-01-02T00:00';
+ checkValidity(input, false, apply,
+ { low: "1950-01-01T00:00", high: "1950-01-02T12:00" });
+
+ input.step = '259200'; // 3 days
+ input.value = '1950-01-04T12:00';
+ checkValidity(input, false, apply,
+ { low: "1950-01-04T00:00", high: "1950-01-07T00:00" });
+
+ input.value = '1950-01-10T00:00';
+ checkValidity(input, true, apply);
+
+ input.step = '0.5'; // half a second
+ input.value = '1950-01-01T00:00:00.123';
+ checkValidity(input, false, apply,
+ { low: "1950-01-01T00:00", high: "1950-01-01T00:00:00.500" });
+
+ input.value = '2000-01-01T12:30:30.600';
+ checkValidity(input, false, apply,
+ { low: "2000-01-01T12:30:30.500", high: "2000-01-01T12:30:31" });
+
+ input.value = '1950-01-05T00:00:00.500';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-01-01T12:00';
+ input.value = '1991-01-01T12:00';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-01-01T12:00:03';
+ checkValidity(input, false, apply,
+ { low: "1991-01-01T12:00:02.100", high: "1991-01-01T12:00:04.200" });
+
+ input.value = '1991-01-01T12:00:06.3';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-12-20T10:00:05';
+ input.value = '1969-12-20T10:00:05';
+ checkValidity(input, true, apply);
+
+ input.value = '1969-12-20T10:00:08';
+ checkValidity(input, false, apply,
+ { low: "1969-12-20T10:00:07.100", high: "1969-12-20T10:00:09.200" });
+
+ input.value = '1969-12-20T10:00:09.200';
+ checkValidity(input, true, apply);
+
+ break;
+ default:
+ ok(false, "Implement the tests for <input type='" + test.type + " >");
+ break;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_stepup_stepdown.html b/dom/html/test/forms/test_stepup_stepdown.html
new file mode 100644
index 0000000000..8ad7fbfeee
--- /dev/null
+++ b/dom/html/test/forms/test_stepup_stepdown.html
@@ -0,0 +1,1137 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=636627
+-->
+<head>
+ <title>Test for Bug 636627</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636627">Mozilla Bug 636627</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 636627 **/
+
+/**
+ * This test is testing stepDown() and stepUp().
+ */
+
+function checkPresence()
+{
+ var input = document.createElement('input');
+ is('stepDown' in input, true, 'stepDown() should be an input function');
+ is('stepUp' in input, true, 'stepUp() should be an input function');
+}
+
+function checkAvailability()
+{
+ var testData =
+ [
+ ["text", false],
+ ["password", false],
+ ["search", false],
+ ["telephone", false],
+ ["email", false],
+ ["url", false],
+ ["hidden", false],
+ ["checkbox", false],
+ ["radio", false],
+ ["file", false],
+ ["submit", false],
+ ["image", false],
+ ["reset", false],
+ ["button", false],
+ ["number", true],
+ ["range", true],
+ ["date", true],
+ ["time", true],
+ ["month", true],
+ ["week", true],
+ ["datetime-local", true],
+ ["color", false],
+ ];
+
+ var element = document.createElement("input");
+ element.setAttribute('value', '0');
+
+ for (data of testData) {
+ var exceptionCaught = false;
+ element.type = data[0];
+ try {
+ element.stepDown();
+ } catch (e) {
+ exceptionCaught = true;
+ }
+ is(exceptionCaught, !data[1], "stepDown() availability is not correct");
+
+ exceptionCaught = false;
+ try {
+ element.stepUp();
+ } catch (e) {
+ exceptionCaught = true;
+ }
+ is(exceptionCaught, !data[1], "stepUp() availability is not correct");
+ }
+}
+
+function checkStepDown()
+{
+ // This testData is very similar to the one in checkStepUp with some changes
+ // relative to stepDown.
+ var testData = [
+ /* Initial value | step | min | max | stepDown arg | final value | exception */
+ { type: 'number', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '0', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '0', false ],
+ [ '9', null, null, null, 9, '0', false ],
+ [ '1', null, null, null, -1, '2', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '0', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '0.5', false ],
+ [ '1', '0.25', null, null, 4, '0', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '0', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '0', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '0', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '0', false ],
+ [ '1', null, '-10', null, null, '0', false ],
+ [ '1', null, '0', null, null, '0', false ],
+ [ '1', null, '10', null, null, '1', false ],
+ [ '1', null, '2', null, null, '1', false ],
+ [ '1', null, '1', null, null, '1', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '0', false ],
+ [ '1', null, null, '10', null, '0', false ],
+ [ '1', null, null, '0', null, '0', false ],
+ [ '1', null, null, '-10', null, '-10', false ],
+ [ '1', null, null, '1', null, '0', false ],
+ [ '5', null, null, '3', '3', '2', false ],
+ [ '5', '2', '-6', '3', '2', '2', false ],
+ [ '-3', '5', '-10', '-3', null, '-5', false ],
+ // Step mismatch.
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '3', '2', '-2', null, null, '2', false ],
+ [ '3', '2', '-2', null, '2', '0', false ],
+ [ '3', '2', '-2', null, '-2', '6', false ],
+ [ '1', '2', '-6', null, null, '0', false ],
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '1', '3', '-6', null, null, '0', false ],
+ [ '2', '3', '-6', null, null, '0', false ],
+ [ '2', '3', '1', null, null, '1', false ],
+ [ '5', '3', '1', null, null, '4', false ],
+ [ '3', '2', '-6', null, null, '2', false ],
+ [ '5', '2', '-6', null, null, '4', false ],
+ [ '6', '2', '1', null, null, '5', false ],
+ [ '8', '3', '1', null, null, '7', false ],
+ [ '9', '2', '-10', null, null, '8', false ],
+ [ '7', '3', '-10', null, null, '5', false ],
+ [ '-2', '3', '-10', null, null, '-4', false ],
+ // Clamping.
+ [ '0', '2', '-1', null, null, '-1', false ],
+ [ '10', '2', '0', '4', '10', '0', false ],
+ [ '10', '2', '0', '4', '5', '0', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '-1', false ],
+ [ '', '2', null, null, null, '-2', false ],
+ [ '', '2', '3', null, null, '3', false ],
+ [ '', null, '3', null, null, '3', false ],
+ [ '', '2', '3', '8', null, '3', false ],
+ [ '', null, '-10', '10', null, '-1', false ],
+ [ '', '3', '-10', '10', null, '-1', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '-1', false ],
+ ]},
+ { type: 'range', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '0', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '0', false ],
+ [ '9', null, null, null, 9, '0', false ],
+ [ '1', null, null, null, -1, '2', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '0', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '0.5', false ],
+ [ '1', '0.25', null, null, 4, '0', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '0', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '0', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '0', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '0', false ],
+ [ '1', null, '-10', null, null, '0', false ],
+ [ '1', null, '0', null, null, '0', false ],
+ [ '1', null, '10', null, null, '10', false ],
+ [ '1', null, '2', null, null, '2', false ],
+ [ '1', null, '1', null, null, '1', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '0', false ],
+ [ '1', null, null, '10', null, '0', false ],
+ [ '1', null, null, '0', null, '0', false ],
+ [ '1', null, null, '-10', null, '0', false ],
+ [ '1', null, null, '1', null, '0', false ],
+ [ '5', null, null, '3', '3', '0', false ],
+ [ '5', '2', '-6', '3', '2', '-2', false ],
+ [ '-3', '5', '-10', '-3', null, '-10', false ],
+ // Step mismatch.
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '3', '2', '-2', null, null, '2', false ],
+ [ '3', '2', '-2', null, '2', '0', false ],
+ [ '3', '2', '-2', null, '-2', '8', false ],
+ [ '1', '2', '-6', null, null, '0', false ],
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '1', '3', '-6', null, null, '-3', false ],
+ [ '2', '3', '-6', null, null, '0', false ],
+ [ '2', '3', '1', null, null, '1', false ],
+ [ '5', '3', '1', null, null, '1', false ],
+ [ '3', '2', '-6', null, null, '2', false ],
+ [ '5', '2', '-6', null, null, '4', false ],
+ [ '6', '2', '1', null, null, '5', false ],
+ [ '8', '3', '1', null, null, '4', false ],
+ [ '9', '2', '-10', null, null, '8', false ],
+ [ '7', '3', '-10', null, null, '5', false ],
+ [ '-2', '3', '-10', null, null, '-4', false ],
+ // Clamping.
+ [ '0', '2', '-1', null, null, '-1', false ],
+ [ '10', '2', '0', '4', '10', '0', false ],
+ [ '10', '2', '0', '4', '5', '0', false ],
+ // value = "" (default will be 50).
+ [ '', null, null, null, null, '49', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '1', false ],
+ ]},
+ { type: 'date', data: [
+ // Regular case.
+ [ '2012-07-09', null, null, null, null, '2012-07-08', false ],
+ // Argument testing.
+ [ '2012-07-09', null, null, null, 1, '2012-07-08', false ],
+ [ '2012-07-09', null, null, null, 5, '2012-07-04', false ],
+ [ '2012-07-09', null, null, null, -1, '2012-07-10', false ],
+ [ '2012-07-09', null, null, null, 0, '2012-07-09', false ],
+ // Month/Year wrapping.
+ [ '2012-08-01', null, null, null, 1, '2012-07-31', false ],
+ [ '1969-01-02', null, null, null, 4, '1968-12-29', false ],
+ [ '1969-01-01', null, null, null, -365, '1970-01-01', false ],
+ [ '2012-02-29', null, null, null, -1, '2012-03-01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2012-01-02', null, null, null, 1.1, '2012-01-01', false ],
+ [ '2012-01-02', null, null, null, 1.9, '2012-01-01', false ],
+ // With step values.
+ [ '2012-01-03', '0.5', null, null, null, '2012-01-02', false ],
+ [ '2012-01-02', '0.5', null, null, null, '2012-01-01', false ],
+ [ '2012-01-01', '2', null, null, null, '2011-12-30', false ],
+ [ '2012-01-02', '0.25',null, null, 4, '2011-12-29', false ],
+ [ '2012-01-15', '1.1', '2012-01-01', null, 1, '2012-01-14', false ],
+ [ '2012-01-12', '1.1', '2012-01-01', null, 2, '2012-01-10', false ],
+ [ '2012-01-23', '1.1', '2012-01-01', null, 10, '2012-01-13', false ],
+ [ '2012-01-23', '1.1', '2012-01-01', null, 11, '2012-01-12', false ],
+ [ '1968-01-12', '1.1', '1968-01-01', null, 8, '1968-01-04', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2012-01-02', '0', null, null, null, '2012-01-01', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2012-01-02', '-1', null, null, null, '2012-01-01', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2012-01-02', 'foo', null, null, null, '2012-01-01', false ],
+ // Min values testing.
+ [ '2012-01-03', '1', 'foo', null, 2, '2012-01-01', false ],
+ [ '2012-01-02', '1', '2012-01-01', null, null, '2012-01-01', false ],
+ [ '2012-01-01', '1', '2012-01-01', null, null, '2012-01-01', false ],
+ [ '2012-01-01', '1', '2012-01-10', null, 1, '2012-01-01', false ],
+ [ '2012-01-05', '3', '2012-01-01', null, null, '2012-01-04', false ],
+ [ '1969-01-01', '5', '1969-01-01', '1969-01-02', null, '1969-01-01', false ],
+ // Max values testing.
+ [ '2012-01-02', '1', null, 'foo', null, '2012-01-01', false ],
+ [ '2012-01-02', null, null, '2012-01-05', null, '2012-01-01', false ],
+ [ '2012-01-03', null, null, '2012-01-03', null, '2012-01-02', false ],
+ [ '2012-01-07', null, null, '2012-01-04', 4, '2012-01-03', false ],
+ [ '2012-01-07', '2', null, '2012-01-04', 3, '2012-01-01', false ],
+ // Step mismatch.
+ [ '2012-01-04', '2', '2012-01-01', null, null, '2012-01-03', false ],
+ [ '2012-01-06', '2', '2012-01-01', null, 2, '2012-01-03', false ],
+ [ '2012-01-05', '2', '2012-01-04', '2012-01-08', null, '2012-01-04', false ],
+ [ '1970-01-04', '2', null, null, null, '1970-01-02', false ],
+ [ '1970-01-09', '3', null, null, null, '1970-01-06', false ],
+ // Clamping.
+ [ '2012-05-01', null, null, '2012-01-05', null, '2012-01-05', false ],
+ [ '1970-01-05', '2', '1970-01-02', '1970-01-05', null, '1970-01-04', false ],
+ [ '1970-01-01', '5', '1970-01-02', '1970-01-09', 10, '1970-01-01', false ],
+ [ '1970-01-07', '5', '1969-12-27', '1970-01-06', 2, '1970-01-01', false ],
+ [ '1970-03-08', '3', '1970-02-01', '1970-02-07', 15, '1970-02-01', false ],
+ [ '1970-01-10', '3', '1970-01-01', '1970-01-06', 2, '1970-01-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1969-12-31', false ],
+ // With step = 'any'.
+ [ '2012-01-01', 'any', null, null, 1, null, true ],
+ [ '2012-01-01', 'ANY', null, null, 1, null, true ],
+ [ '2012-01-01', 'AnY', null, null, 1, null, true ],
+ [ '2012-01-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'time', data: [
+ // Regular case.
+ [ '16:39', null, null, null, null, '16:38', false ],
+ // Argument testing.
+ [ '16:40', null, null, null, 1, '16:39', false ],
+ [ '16:40', null, null, null, 5, '16:35', false ],
+ [ '16:40', null, null, null, -1, '16:41', false ],
+ [ '16:40', null, null, null, 0, '16:40', false ],
+ // hour/minutes/seconds wrapping.
+ [ '05:00', null, null, null, null, '04:59', false ],
+ [ '05:00:00', 1, null, null, null, '04:59:59', false ],
+ [ '05:00:00', 0.1, null, null, null, '04:59:59.900', false ],
+ [ '05:00:00', 0.01, null, null, null, '04:59:59.990', false ],
+ [ '05:00:00', 0.001, null, null, null, '04:59:59.999', false ],
+ // stepDown() on '00:00' gives '23:59'.
+ [ '00:00', null, null, null, 1, '23:59', false ],
+ [ '00:00', null, null, null, 3, '23:57', false ],
+ // Some random step values..
+ [ '16:56', '0.5', null, null, null, '16:55:59.500', false ],
+ [ '16:56', '2', null, null, null, '16:55:58', false ],
+ [ '16:56', '0.25',null, null, 4, '16:55:59', false ],
+ [ '16:57', '1.1', '16:00', null, 1, '16:56:59.900', false ],
+ [ '16:57', '1.1', '16:00', null, 2, '16:56:58.800', false ],
+ [ '16:57', '1.1', '16:00', null, 10, '16:56:50', false ],
+ [ '16:57', '1.1', '16:00', null, 11, '16:56:48.900', false ],
+ [ '16:57', '1.1', '16:00', null, 8, '16:56:52.200', false ],
+ // Invalid @step, means that we use the default value.
+ [ '17:01', '0', null, null, null, '17:00', false ],
+ [ '17:01', '-1', null, null, null, '17:00', false ],
+ [ '17:01', 'foo', null, null, null, '17:00', false ],
+ // Min values testing.
+ [ '17:02', '60', 'foo', null, 2, '17:00', false ],
+ [ '17:10', '60', '17:09', null, null, '17:09', false ],
+ [ '17:10', '60', '17:10', null, null, '17:10', false ],
+ [ '17:10', '60', '17:30', null, 1, '17:10', false ],
+ [ '17:10', '180', '17:05', null, null, '17:08', false ],
+ [ '17:10', '300', '17:10', '17:11', null, '17:10', false ],
+ // Max values testing.
+ [ '17:15', '60', null, 'foo', null, '17:14', false ],
+ [ '17:15', null, null, '17:20', null, '17:14', false ],
+ [ '17:15', null, null, '17:15', null, '17:14', false ],
+ [ '17:15', null, null, '17:13', 4, '17:11', false ],
+ [ '17:15', '120', null, '17:13', 3, '17:09', false ],
+ // Step mismatch.
+ [ '17:19', '120', '17:10', null, null, '17:18', false ],
+ [ '17:19', '120', '17:10', null, 2, '17:16', false ],
+ [ '17:19', '120', '17:18', '17:25', null, '17:18', false ],
+ [ '17:19', '120', null, null, null, '17:17', false ],
+ [ '17:19', '180', null, null, null, '17:16', false ],
+ // Clamping.
+ [ '17:22', null, null, '17:11', null, '17:11', false ],
+ [ '17:22', '120', '17:20', '17:22', null, '17:20', false ],
+ [ '17:22', '300', '17:12', '17:20', 10, '17:12', false ],
+ [ '17:22', '300', '17:18', '17:20', 2, '17:18', false ],
+ [ '17:22', '180', '17:00', '17:20', 15, '17:00', false ],
+ [ '17:22', '180', '17:10', '17:20', 2, '17:16', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '23:59', false ],
+ // With step = 'any'.
+ [ '17:26', 'any', null, null, 1, null, true ],
+ [ '17:26', 'ANY', null, null, 1, null, true ],
+ [ '17:26', 'AnY', null, null, 1, null, true ],
+ [ '17:26', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'month', data: [
+ // Regular case.
+ [ '2016-08', null, null, null, null, '2016-07', false ],
+ // Argument testing.
+ [ '2016-08', null, null, null, 1, '2016-07', false ],
+ [ '2016-08', null, null, null, 5, '2016-03', false ],
+ [ '2016-08', null, null, null, -1, '2016-09', false ],
+ [ '2016-08', null, null, null, 0, '2016-08', false ],
+ // Month/Year wrapping.
+ [ '2016-01', null, null, null, 1, '2015-12', false ],
+ [ '1969-02', null, null, null, 4, '1968-10', false ],
+ [ '1969-01', null, null, null, -12, '1970-01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-08', null, null, null, 1.1, '2016-07', false ],
+ [ '2016-01', null, null, null, 1.9, '2015-12', false ],
+ // With step values.
+ [ '2016-03', '0.5', null, null, null, '2016-02', false ],
+ [ '2016-03', '2', null, null, null, '2016-01', false ],
+ [ '2016-03', '0.25',null, null, 4, '2015-11', false ],
+ [ '2016-12', '1.1', '2016-01', null, 1, '2016-11', false ],
+ [ '2016-12', '1.1', '2016-01', null, 2, '2016-10', false ],
+ [ '2016-12', '1.1', '2016-01', null, 10, '2016-02', false ],
+ [ '2016-12', '1.1', '2016-01', null, 12, '2016-01', false ],
+ [ '1968-12', '1.1', '1968-01', null, 8, '1968-04', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-02', '0', null, null, null, '2016-01', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-02', '-1', null, null, null, '2016-01', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-02', 'foo', null, null, null, '2016-01', false ],
+ // Min values testing.
+ [ '2016-03', '1', 'foo', null, 2, '2016-01', false ],
+ [ '2016-02', '1', '2016-01', null, null, '2016-01', false ],
+ [ '2016-01', '1', '2016-01', null, null, '2016-01', false ],
+ [ '2016-01', '1', '2016-01', null, 1, '2016-01', false ],
+ [ '2016-05', '3', '2016-01', null, null, '2016-04', false ],
+ [ '1969-01', '5', '1969-01', '1969-02', null, '1969-01', false ],
+ // Max values testing.
+ [ '2016-02', '1', null, 'foo', null, '2016-01', false ],
+ [ '2016-02', null, null, '2016-05', null, '2016-01', false ],
+ [ '2016-03', null, null, '2016-03', null, '2016-02', false ],
+ [ '2016-07', null, null, '2016-04', 4, '2016-03', false ],
+ [ '2016-07', '2', null, '2016-04', 3, '2016-01', false ],
+ // Step mismatch.
+ [ '2016-04', '2', '2016-01', null, null, '2016-03', false ],
+ [ '2016-06', '2', '2016-01', null, 2, '2016-03', false ],
+ [ '2016-05', '2', '2016-04', '2016-08', null, '2016-04', false ],
+ [ '1970-04', '2', null, null, null, '1970-02', false ],
+ [ '1970-09', '3', null, null, null, '1970-06', false ],
+ // Clamping.
+ [ '2016-05', null, null, '2016-01', null, '2016-01', false ],
+ [ '1970-05', '2', '1970-02', '1970-05', null, '1970-04', false ],
+ [ '1970-01', '5', '1970-02', '1970-09', 10, '1970-01', false ],
+ [ '1970-07', '5', '1969-12', '1970-10', 2, '1969-12', false ],
+ [ '1970-08', '3', '1970-01', '1970-07', 15, '1970-01', false ],
+ [ '1970-10', '3', '1970-01', '1970-06', 2, '1970-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1969-12', false ],
+ // With step = 'any'.
+ [ '2016-01', 'any', null, null, 1, null, true ],
+ [ '2016-01', 'ANY', null, null, 1, null, true ],
+ [ '2016-01', 'AnY', null, null, 1, null, true ],
+ [ '2016-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'week', data: [
+ // Regular case.
+ [ '2016-W40', null, null, null, null, '2016-W39', false ],
+ // Argument testing.
+ [ '2016-W40', null, null, null, 1, '2016-W39', false ],
+ [ '2016-W40', null, null, null, 5, '2016-W35', false ],
+ [ '2016-W40', null, null, null, -1, '2016-W41', false ],
+ [ '2016-W40', null, null, null, 0, '2016-W40', false ],
+ // Week/Year wrapping.
+ [ '2016-W01', null, null, null, 1, '2015-W53', false ],
+ [ '1969-W02', null, null, null, 4, '1968-W50', false ],
+ [ '1969-W01', null, null, null, -52, '1970-W01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-W40', null, null, null, 1.1, '2016-W39', false ],
+ [ '2016-W01', null, null, null, 1.9, '2015-W53', false ],
+ // With step values.
+ [ '2016-W03', '0.5', null, null, null, '2016-W02', false ],
+ [ '2016-W03', '2', null, null, null, '2016-W01', false ],
+ [ '2016-W03', '0.25', null, null, 4, '2015-W52', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 1, '2016-W51', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 2, '2016-W50', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 10, '2016-W42', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 52, '2016-W01', false ],
+ [ '1968-W52', '1.1', '1968-W01', null, 8, '1968-W44', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-W02', '0', null, null, null, '2016-W01', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-W02', '-1', null, null, null, '2016-W01', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-W02', 'foo', null, null, null, '2016-W01', false ],
+ // Min values testing.
+ [ '2016-W03', '1', 'foo', null, 2, '2016-W01', false ],
+ [ '2016-W02', '1', '2016-01', null, null, '2016-W01', false ],
+ [ '2016-W01', '1', '2016-W01', null, null, '2016-W01', false ],
+ [ '2016-W01', '1', '2016-W01', null, 1, '2016-W01', false ],
+ [ '2016-W05', '3', '2016-W01', null, null, '2016-W04', false ],
+ [ '1969-W01', '5', '1969-W01', '1969-W02', null, '1969-W01', false ],
+ // Max values testing.
+ [ '2016-W02', '1', null, 'foo', null, '2016-W01', false ],
+ [ '2016-W02', null, null, '2016-W05', null, '2016-W01', false ],
+ [ '2016-W03', null, null, '2016-W03', null, '2016-W02', false ],
+ [ '2016-W07', null, null, '2016-W04', 4, '2016-W03', false ],
+ [ '2016-W07', '2', null, '2016-W04', 3, '2016-W01', false ],
+ // Step mismatch.
+ [ '2016-W04', '2', '2016-W01', null, null, '2016-W03', false ],
+ [ '2016-W06', '2', '2016-W01', null, 2, '2016-W03', false ],
+ [ '2016-W05', '2', '2016-W04', '2016-W08', null, '2016-W04', false ],
+ [ '1970-W04', '2', null, null, null, '1970-W02', false ],
+ [ '1970-W09', '3', null, null, null, '1970-W06', false ],
+ // Clamping.
+ [ '2016-W05', null, null, '2016-W01', null, '2016-W01', false ],
+ [ '1970-W05', '2', '1970-W02', '1970-W05', null, '1970-W04', false ],
+ [ '1970-W01', '5', '1970-W02', '1970-W09', 10, '1970-W01', false ],
+ [ '1970-W07', '5', '1969-W52', '1970-W10', 2, '1969-W52', false ],
+ [ '1970-W08', '3', '1970-W01', '1970-W07', 15, '1970-W01', false ],
+ [ '1970-W10', '3', '1970-W01', '1970-W06', 2, '1970-W04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-W01', false ],
+ // With step = 'any'.
+ [ '2016-W01', 'any', null, null, 1, null, true ],
+ [ '2016-W01', 'ANY', null, null, 1, null, true ],
+ [ '2016-W01', 'AnY', null, null, 1, null, true ],
+ [ '2016-W01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'datetime-local', data: [
+ // Regular case.
+ [ '2017-02-07T09:30', null, null, null, null, '2017-02-07T09:29', false ],
+ // Argument testing.
+ [ '2017-02-07T09:30', null, null, null, 1, '2017-02-07T09:29', false ],
+ [ '2017-02-07T09:30', null, null, null, 5, '2017-02-07T09:25', false ],
+ [ '2017-02-07T09:30', null, null, null, -1, '2017-02-07T09:31', false ],
+ [ '2017-02-07T09:30', null, null, null, 0, '2017-02-07T09:30', false ],
+ // hour/minutes/seconds wrapping.
+ [ '2000-01-01T05:00', null, null, null, null, '2000-01-01T04:59', false ],
+ [ '2000-01-01T05:00:00', 1, null, null, null, '2000-01-01T04:59:59', false ],
+ [ '2000-01-01T05:00:00', 0.1, null, null, null, '2000-01-01T04:59:59.900', false ],
+ [ '2000-01-01T05:00:00', 0.01, null, null, null, '2000-01-01T04:59:59.990', false ],
+ [ '2000-01-01T05:00:00', 0.001, null, null, null, '2000-01-01T04:59:59.999', false ],
+ // month/year wrapping.
+ [ '2012-08-01T12:00', null, null, null, 1440, '2012-07-31T12:00', false ],
+ [ '1969-01-02T12:00', null, null, null, 5760, '1968-12-29T12:00', false ],
+ [ '1969-12-31T00:00', null, null, null, -1440, '1970-01-01T00:00', false ],
+ [ '2012-02-29T00:00', null, null, null, -1440, '2012-03-01T00:00', false ],
+ // stepDown() on '00:00' gives '23:59'.
+ [ '2017-02-07T00:00', null, null, null, 1, '2017-02-06T23:59', false ],
+ [ '2017-02-07T00:00', null, null, null, 3, '2017-02-06T23:57', false ],
+ // Some random step values..
+ [ '2017-02-07T16:07', '0.5', null, null, null, '2017-02-07T16:06:59.500', false ],
+ [ '2017-02-07T16:07', '2', null, null, null, '2017-02-07T16:06:58', false ],
+ [ '2017-02-07T16:07', '0.25', null, null, 4, '2017-02-07T16:06:59', false ],
+ [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 1, '2017-02-07T16:06:59.100', false ],
+ [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 2, '2017-02-07T16:06:58', false ],
+ [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 10, '2017-02-07T16:06:49.200', false ],
+ [ '2017-02-07T16:07', '129600', '2017-02-01T00:00', null, 2, '2017-02-05T12:00', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T10:15', '0', null, null, null, '2017-02-07T10:14', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T10:15', '-1', null, null, null, '2017-02-07T10:14', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2017-02-07T10:15', 'foo', null, null, null, '2017-02-07T10:14', false ],
+ // Min values testing.
+ [ '2012-02-02T17:02', '60', 'foo', null, 2, '2012-02-02T17:00', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:09', null, null, '2012-02-02T17:09', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:10', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:10', false ],
+ [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:08', false ],
+ [ '2012-02-03T20:05', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ],
+ [ '2012-02-03T18:00', '129600', '2012-02-01T00:00', null, null, '2012-02-02T12:00', false ],
+ // Max values testing.
+ [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:14', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:14', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:14', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:11', false ],
+ [ '2012-02-02T17:15', '120', null, '2012-02-02T17:13', 3, '2012-02-02T17:09', false ],
+ [ '2012-02-03T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-02T20:05', false ],
+ [ '2012-02-03T18:00', '129600', null, '2012-02-03T20:00', null, '2012-02-02T06:00', false ],
+ // Step mismatch.
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:18', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:16', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:18', false ],
+ [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:17', false ],
+ [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:16', false ],
+ [ '2017-02-07T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-06T17:19', false ],
+ // Clamping.
+ [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:11', false ],
+ [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:20', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:12', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:18', false ],
+ [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 15, '2017-02-07T15:00', false ],
+ [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 2, '2017-02-07T17:00', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1969-12-31T23:59', false ],
+ // With step = 'any'.
+ [ '2017-02-07T15:20', 'any', null, null, 1, null, true ],
+ [ '2017-02-07T15:20', 'ANY', null, null, 1, null, true ],
+ [ '2017-02-07T15:20', 'AnY', null, null, 1, null, true ],
+ [ '2017-02-07T15:20', 'aNy', null, null, 1, null, true ],
+ ]},
+ ];
+
+ for (var test of testData) {
+ for (var data of test.data) {
+ var element = document.createElement("input");
+ element.type = test.type;
+
+ if (data[1] != null) {
+ element.step = data[1];
+ }
+
+ if (data[2] != null) {
+ element.min = data[2];
+ }
+
+ if (data[3] != null) {
+ element.max = data[3];
+ }
+
+ // Set 'value' last for type=range, because the final sanitized value
+ // after setting 'step', 'min' and 'max' can be affected by the order in
+ // which those attributes are set. Setting 'value' last makes it simpler
+ // to reason about what the final value should be.
+ if (data[0] != null) {
+ element.setAttribute('value', data[0]);
+ }
+
+ var exceptionCaught = false;
+ try {
+ if (data[4] != null) {
+ element.stepDown(data[4]);
+ } else {
+ element.stepDown();
+ }
+
+ is(element.value, data[5], "The value for type=" + test.type + " should be " + data[5]);
+ } catch (e) {
+ exceptionCaught = true;
+ is(element.value, data[0], e.name + "The value should not have changed");
+ is(e.name, 'InvalidStateError',
+ "It should be a InvalidStateError exception.");
+ } finally {
+ is(exceptionCaught, data[6], "exception status should be " + data[6]);
+ }
+ }
+ }
+}
+
+function checkStepUp()
+{
+ // This testData is very similar to the one in checkStepDown with some changes
+ // relative to stepUp.
+ var testData = [
+ /* Initial value | step | min | max | stepUp arg | final value | exception */
+ { type: 'number', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '2', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '2', false ],
+ [ '9', null, null, null, 9, '18', false ],
+ [ '1', null, null, null, -1, '0', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '2', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '1.5', false ],
+ [ '1', '0.25', null, null, 4, '2', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '2', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '2', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '2', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '2', false ],
+ [ '1', null, '-10', null, null, '2', false ],
+ [ '1', null, '0', null, null, '2', false ],
+ [ '1', null, '10', null, null, '10', false ],
+ [ '1', null, '2', null, null, '2', false ],
+ [ '1', null, '1', null, null, '2', false ],
+ [ '0', null, '4', null, '5', '5', false ],
+ [ '0', '2', '5', null, '3', '5', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '2', false ],
+ [ '1', null, null, '10', null, '2', false ],
+ [ '1', null, null, '0', null, '1', false ],
+ [ '1', null, null, '-10', null, '1', false ],
+ [ '1', null, null, '1', null, '1', false ],
+ [ '-3', '5', '-10', '-3', null, '-3', false ],
+ // Step mismatch.
+ [ '1', '2', '0', null, null, '2', false ],
+ [ '1', '2', '0', null, '2', '4', false ],
+ [ '8', '2', null, '9', null, '8', false ],
+ [ '-3', '2', '-6', null, null, '-2', false ],
+ [ '9', '3', '-10', null, null, '11', false ],
+ [ '7', '3', '-10', null, null, '8', false ],
+ [ '7', '3', '5', null, null, '8', false ],
+ [ '9', '4', '3', null, null, '11', false ],
+ [ '-2', '3', '-6', null, null, '0', false ],
+ [ '7', '3', '6', null, null, '9', false ],
+ // Clamping.
+ [ '1', '2', '0', '3', null, '2', false ],
+ [ '0', '5', '1', '8', '10', '6', false ],
+ [ '-9', '3', '-8', '-1', '5', '-2', false ],
+ [ '-9', '3', '8', '15', '15', '14', false ],
+ [ '-1', '3', '-1', '4', '3', '2', false ],
+ [ '-3', '2', '-6', '-2', null, '-2', false ],
+ [ '-3', '2', '-6', '-1', null, '-2', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1', false ],
+ [ '', null, null, null, null, '1', false ],
+ [ '', '2', null, null, null, '2', false ],
+ [ '', '2', '3', null, null, '3', false ],
+ [ '', null, '3', null, null, '3', false ],
+ [ '', '2', '3', '8', null, '3', false ],
+ [ '', null, '-10', '10', null, '1', false ],
+ [ '', '3', '-10', '10', null, '2', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '3', false ],
+ ]},
+ { type: 'range', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '2', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '2', false ],
+ [ '9', null, null, null, 9, '18', false ],
+ [ '1', null, null, null, -1, '0', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '2', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '1.5', false ],
+ [ '1', '0.25', null, null, 4, '2', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '2', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '2', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '2', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '2', false ],
+ [ '1', null, '-10', null, null, '2', false ],
+ [ '1', null, '0', null, null, '2', false ],
+ [ '1', null, '10', null, null, '11', false ],
+ [ '1', null, '2', null, null, '3', false ],
+ [ '1', null, '1', null, null, '2', false ],
+ [ '0', null, '4', null, '5', '9', false ],
+ [ '0', '2', '5', null, '3', '11', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '2', false ],
+ [ '1', null, null, '10', null, '2', false ],
+ [ '1', null, null, '0', null, '0', false ],
+ [ '1', null, null, '-10', null, '0', false ],
+ [ '1', null, null, '1', null, '1', false ],
+ [ '-3', '5', '-10', '-3', null, '-5', false ],
+ // Step mismatch.
+ [ '1', '2', '0', null, null, '4', false ],
+ [ '1', '2', '0', null, '2', '6', false ],
+ [ '8', '2', null, '9', null, '8', false ],
+ [ '-3', '2', '-6', null, null, '0', false ],
+ [ '9', '3', '-10', null, null, '11', false ],
+ [ '7', '3', '-10', null, null, '11', false ],
+ [ '7', '3', '5', null, null, '11', false ],
+ [ '9', '4', '3', null, null, '15', false ],
+ [ '-2', '3', '-6', null, null, '0', false ],
+ [ '7', '3', '6', null, null, '9', false ],
+ // Clamping.
+ [ '1', '2', '0', '3', null, '2', false ],
+ [ '0', '5', '1', '8', '10', '6', false ],
+ [ '-9', '3', '-8', '-1', '5', '-2', false ],
+ [ '-9', '3', '8', '15', '15', '14', false ],
+ [ '-1', '3', '-1', '4', '3', '2', false ],
+ [ '-3', '2', '-6', '-2', null, '-2', false ],
+ [ '-3', '2', '-6', '-1', null, '-2', false ],
+ // value = "" (default will be 50).
+ [ '', null, null, null, null, '51', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '3', false ],
+ ]},
+ { type: 'date', data: [
+ // Regular case.
+ [ '2012-07-09', null, null, null, null, '2012-07-10', false ],
+ // Argument testing.
+ [ '2012-07-09', null, null, null, 1, '2012-07-10', false ],
+ [ '2012-07-09', null, null, null, 9, '2012-07-18', false ],
+ [ '2012-07-09', null, null, null, -1, '2012-07-08', false ],
+ [ '2012-07-09', null, null, null, 0, '2012-07-09', false ],
+ // Month/Year wrapping.
+ [ '2012-07-31', null, null, null, 1, '2012-08-01', false ],
+ [ '1968-12-29', null, null, null, 4, '1969-01-02', false ],
+ [ '1970-01-01', null, null, null, -365, '1969-01-01', false ],
+ [ '2012-03-01', null, null, null, -1, '2012-02-29', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2012-01-01', null, null, null, 1.1, '2012-01-02', false ],
+ [ '2012-01-01', null, null, null, 1.9, '2012-01-02', false ],
+ // With step values.
+ [ '2012-01-01', '0.5', null, null, null, '2012-01-02', false ],
+ [ '2012-01-01', '2', null, null, null, '2012-01-03', false ],
+ [ '2012-01-01', '0.25', null, null, 4, '2012-01-05', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 1, '2012-01-02', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 2, '2012-01-03', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 10, '2012-01-11', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 11, '2012-01-12', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2012-01-01', '0', null, null, null, '2012-01-02', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2012-01-01', '-1', null, null, null, '2012-01-02', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2012-01-01', 'foo', null, null, null, '2012-01-02', false ],
+ // Min values testing.
+ [ '2012-01-01', '1', 'foo', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2011-12-01', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2012-01-02', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2012-01-01', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2012-01-04', null, 4, '2012-01-05', false ],
+ [ '2012-01-01', '2', '2012-01-04', null, 3, '2012-01-06', false ],
+ // Max values testing.
+ [ '2012-01-01', '1', null, 'foo', 2, '2012-01-03', false ],
+ [ '2012-01-01', '1', null, '2012-01-10', 1, '2012-01-02', false ],
+ [ '2012-01-02', null, null, '2012-01-01', null, '2012-01-02', false ],
+ [ '2012-01-02', null, null, '2012-01-02', null, '2012-01-02', false ],
+ [ '1969-01-02', '5', '1969-01-01', '1969-01-02', null, '1969-01-02', false ],
+ // Step mismatch.
+ [ '2012-01-02', '2', '2012-01-01', null, null, '2012-01-03', false ],
+ [ '2012-01-02', '2', '2012-01-01', null, 2, '2012-01-05', false ],
+ [ '2012-01-05', '2', '2012-01-01', '2012-01-06', null, '2012-01-05', false ],
+ [ '1970-01-02', '2', null, null, null, '1970-01-04', false ],
+ [ '1970-01-05', '3', null, null, null, '1970-01-08', false ],
+ [ '1970-01-03', '3', null, null, null, '1970-01-06', false ],
+ [ '1970-01-03', '3', '1970-01-02', null, null, '1970-01-05', false ],
+ // Clamping.
+ [ '2012-01-01', null, '2012-01-31', null, null, '2012-01-31', false ],
+ [ '1970-01-02', '2', '1970-01-01', '1970-01-04', null, '1970-01-03', false ],
+ [ '1970-01-01', '5', '1970-01-02', '1970-01-09', 10, '1970-01-07', false ],
+ [ '1969-12-28', '5', '1969-12-29', '1970-01-06', 3, '1970-01-03', false ],
+ [ '1970-01-01', '3', '1970-02-01', '1970-02-07', 15, '1970-02-07', false ],
+ [ '1970-01-01', '3', '1970-01-01', '1970-01-06', 2, '1970-01-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-01-02', false ],
+ // With step = 'any'.
+ [ '2012-01-01', 'any', null, null, 1, null, true ],
+ [ '2012-01-01', 'ANY', null, null, 1, null, true ],
+ [ '2012-01-01', 'AnY', null, null, 1, null, true ],
+ [ '2012-01-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'time', data: [
+ // Regular case.
+ [ '16:39', null, null, null, null, '16:40', false ],
+ // Argument testing.
+ [ '16:40', null, null, null, 1, '16:41', false ],
+ [ '16:40', null, null, null, 5, '16:45', false ],
+ [ '16:40', null, null, null, -1, '16:39', false ],
+ [ '16:40', null, null, null, 0, '16:40', false ],
+ // hour/minutes/seconds wrapping.
+ [ '04:59', null, null, null, null, '05:00', false ],
+ [ '04:59:59', 1, null, null, null, '05:00', false ],
+ [ '04:59:59.900', 0.1, null, null, null, '05:00', false ],
+ [ '04:59:59.990', 0.01, null, null, null, '05:00', false ],
+ [ '04:59:59.999', 0.001, null, null, null, '05:00', false ],
+ // stepUp() on '23:59' gives '00:00'.
+ [ '23:59', null, null, null, 1, '00:00', false ],
+ [ '23:59', null, null, null, 3, '00:02', false ],
+ // Some random step values..
+ [ '16:56', '0.5', null, null, null, '16:56:00.500', false ],
+ [ '16:56', '2', null, null, null, '16:56:02', false ],
+ [ '16:56', '0.25',null, null, 4, '16:56:01', false ],
+ [ '16:57', '1.1', '16:00', null, 1, '16:57:01', false ],
+ [ '16:57', '1.1', '16:00', null, 2, '16:57:02.100', false ],
+ [ '16:57', '1.1', '16:00', null, 10, '16:57:10.900', false ],
+ [ '16:57', '1.1', '16:00', null, 11, '16:57:12', false ],
+ [ '16:57', '1.1', '16:00', null, 8, '16:57:08.700', false ],
+ // Invalid @step, means that we use the default value.
+ [ '17:01', '0', null, null, null, '17:02', false ],
+ [ '17:01', '-1', null, null, null, '17:02', false ],
+ [ '17:01', 'foo', null, null, null, '17:02', false ],
+ // Min values testing.
+ [ '17:02', '60', 'foo', null, 2, '17:04', false ],
+ [ '17:10', '60', '17:09', null, null, '17:11', false ],
+ [ '17:10', '60', '17:10', null, null, '17:11', false ],
+ [ '17:10', '60', '17:30', null, 1, '17:30', false ],
+ [ '17:10', '180', '17:05', null, null, '17:11', false ],
+ [ '17:10', '300', '17:10', '17:11', null,'17:10', false ],
+ // Max values testing.
+ [ '17:15', '60', null, 'foo', null, '17:16', false ],
+ [ '17:15', null, null, '17:20', null, '17:16', false ],
+ [ '17:15', null, null, '17:15', null, '17:15', false ],
+ [ '17:15', null, null, '17:13', 4, '17:15', false ],
+ [ '17:15', '120', null, '17:13', 3, '17:15', false ],
+ // Step mismatch.
+ [ '17:19', '120', '17:10', null, null, '17:20', false ],
+ [ '17:19', '120', '17:10', null, 2, '17:22', false ],
+ [ '17:19', '120', '17:18', '17:25', null, '17:20', false ],
+ [ '17:19', '120', null, null, null, '17:21', false ],
+ [ '17:19', '180', null, null, null, '17:22', false ],
+ // Clamping.
+ [ '17:22', null, null, '17:11', null, '17:22', false ],
+ [ '17:22', '120', '17:20', '17:22', null, '17:22', false ],
+ [ '17:22', '300', '17:12', '17:20', 10, '17:22', false ],
+ [ '17:22', '300', '17:18', '17:20', 2, '17:22', false ],
+ [ '17:22', '180', '17:00', '17:20', 15, '17:22', false ],
+ [ '17:22', '180', '17:10', '17:20', 2, '17:22', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '00:01', false ],
+ // With step = 'any'.
+ [ '17:26', 'any', null, null, 1, null, true ],
+ [ '17:26', 'ANY', null, null, 1, null, true ],
+ [ '17:26', 'AnY', null, null, 1, null, true ],
+ [ '17:26', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'month', data: [
+ // Regular case.
+ [ '2016-08', null, null, null, null, '2016-09', false ],
+ // Argument testing.
+ [ '2016-08', null, null, null, 1, '2016-09', false ],
+ [ '2016-08', null, null, null, 9, '2017-05', false ],
+ [ '2016-08', null, null, null, -1, '2016-07', false ],
+ [ '2016-08', null, null, null, 0, '2016-08', false ],
+ // Month/Year wrapping.
+ [ '2015-12', null, null, null, 1, '2016-01', false ],
+ [ '1968-12', null, null, null, 4, '1969-04', false ],
+ [ '1970-01', null, null, null, -12, '1969-01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-01', null, null, null, 1.1, '2016-02', false ],
+ [ '2016-01', null, null, null, 1.9, '2016-02', false ],
+ // With step values.
+ [ '2016-01', '0.5', null, null, null, '2016-02', false ],
+ [ '2016-01', '2', null, null, null, '2016-03', false ],
+ [ '2016-01', '0.25', null, null, 4, '2016-05', false ],
+ [ '2016-01', '1.1', '2016-01', null, 1, '2016-02', false ],
+ [ '2016-01', '1.1', '2016-01', null, 2, '2016-03', false ],
+ [ '2016-01', '1.1', '2016-01', null, 10, '2016-11', false ],
+ [ '2016-01', '1.1', '2016-01', null, 11, '2016-12', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-01', '0', null, null, null, '2016-02', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-01', '-1', null, null, null, '2016-02', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-01', 'foo', null, null, null, '2016-02', false ],
+ // Min values testing.
+ [ '2016-01', '1', 'foo', null, null, '2016-02', false ],
+ [ '2016-01', null, '2015-12', null, null, '2016-02', false ],
+ [ '2016-01', null, '2016-02', null, null, '2016-02', false ],
+ [ '2016-01', null, '2016-01', null, null, '2016-02', false ],
+ [ '2016-01', null, '2016-04', null, 4, '2016-05', false ],
+ [ '2016-01', '2', '2016-04', null, 3, '2016-06', false ],
+ // Max values testing.
+ [ '2016-01', '1', null, 'foo', 2, '2016-03', false ],
+ [ '2016-01', '1', null, '2016-02', 1, '2016-02', false ],
+ [ '2016-02', null, null, '2016-01', null, '2016-02', false ],
+ [ '2016-02', null, null, '2016-02', null, '2016-02', false ],
+ [ '1969-02', '5', '1969-01', '1969-02', null, '1969-02', false ],
+ // Step mismatch.
+ [ '2016-02', '2', '2016-01', null, null, '2016-03', false ],
+ [ '2016-02', '2', '2016-01', null, 2, '2016-05', false ],
+ [ '2016-05', '2', '2016-01', '2016-06', null, '2016-05', false ],
+ [ '1970-02', '2', null, null, null, '1970-04', false ],
+ [ '1970-05', '3', null, null, null, '1970-08', false ],
+ [ '1970-03', '3', null, null, null, '1970-06', false ],
+ [ '1970-03', '3', '1970-02', null, null, '1970-05', false ],
+ // Clamping.
+ [ '2016-01', null, '2016-12', null, null, '2016-12', false ],
+ [ '1970-02', '2', '1970-01', '1970-04', null, '1970-03', false ],
+ [ '1970-01', '5', '1970-02', '1970-09', 10, '1970-07', false ],
+ [ '1969-11', '5', '1969-12', '1970-06', 3, '1970-05', false ],
+ [ '1970-01', '3', '1970-02', '1971-07', 15, '1971-05', false ],
+ [ '1970-01', '3', '1970-01', '1970-06', 2, '1970-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-02', false ],
+ // With step = 'any'.
+ [ '2016-01', 'any', null, null, 1, null, true ],
+ [ '2016-01', 'ANY', null, null, 1, null, true ],
+ [ '2016-01', 'AnY', null, null, 1, null, true ],
+ [ '2016-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'week', data: [
+ // Regular case.
+ [ '2016-W40', null, null, null, null, '2016-W41', false ],
+ // Argument testing.
+ [ '2016-W40', null, null, null, 1, '2016-W41', false ],
+ [ '2016-W40', null, null, null, 20, '2017-W08', false ],
+ [ '2016-W40', null, null, null, -1, '2016-W39', false ],
+ [ '2016-W40', null, null, null, 0, '2016-W40', false ],
+ // Week/Year wrapping.
+ [ '2015-W53', null, null, null, 1, '2016-W01', false ],
+ [ '1968-W52', null, null, null, 4, '1969-W04', false ],
+ [ '1970-W01', null, null, null, -52, '1969-W01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-W01', null, null, null, 1.1, '2016-W02', false ],
+ [ '2016-W01', null, null, null, 1.9, '2016-W02', false ],
+ // With step values.
+ [ '2016-W01', '0.5', null, null, null, '2016-W02', false ],
+ [ '2016-W01', '2', null, null, null, '2016-W03', false ],
+ [ '2016-W01', '0.25', null, null, 4, '2016-W05', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 1, '2016-W02', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 2, '2016-W03', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 10, '2016-W11', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 20, '2016-W21', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-W01', '0', null, null, null, '2016-W02', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-W01', '-1', null, null, null, '2016-W02', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-W01', 'foo', null, null, null, '2016-W02', false ],
+ // Min values testing.
+ [ '2016-W01', '1', 'foo', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2015-W53', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2016-W02', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2016-W01', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2016-W04', null, 4, '2016-W05', false ],
+ [ '2016-W01', '2', '2016-W04', null, 3, '2016-W06', false ],
+ // Max values testing.
+ [ '2016-W01', '1', null, 'foo', 2, '2016-W03', false ],
+ [ '2016-W01', '1', null, '2016-W02', 1, '2016-W02', false ],
+ [ '2016-W02', null, null, '2016-W01', null, '2016-W02', false ],
+ [ '2016-W02', null, null, '2016-W02', null, '2016-W02', false ],
+ [ '1969-W02', '5', '1969-W01', '1969-W02', null, '1969-W02', false ],
+ // Step mismatch.
+ [ '2016-W02', '2', '2016-W01', null, null, '2016-W03', false ],
+ [ '2016-W02', '2', '2016-W01', null, 2, '2016-W05', false ],
+ [ '2016-W05', '2', '2016-W01', '2016-W06', null, '2016-W05', false ],
+ [ '1970-W02', '2', null, null, null, '1970-W04', false ],
+ [ '1970-W05', '3', null, null, null, '1970-W08', false ],
+ [ '1970-W03', '3', null, null, null, '1970-W06', false ],
+ [ '1970-W03', '3', '1970-W02', null, null, '1970-W05', false ],
+ // Clamping.
+ [ '2016-W01', null, '2016-W52', null, null, '2016-W52', false ],
+ [ '1970-W02', '2', '1970-W01', '1970-W04', null, '1970-W03', false ],
+ [ '1970-W01', '5', '1970-W02', '1970-W09', 10, '1970-W07', false ],
+ [ '1969-W50', '5', '1969-W52', '1970-W06', 3, '1970-W05', false ],
+ [ '1970-W01', '3', '1970-W02', '1971-W07', 15, '1970-W44', false ],
+ [ '1970-W01', '3', '1970-W01', '1970-W06', 2, '1970-W04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-W02', false ],
+ // With step = 'any'.
+ [ '2016-W01', 'any', null, null, 1, null, true ],
+ [ '2016-W01', 'ANY', null, null, 1, null, true ],
+ [ '2016-W01', 'AnY', null, null, 1, null, true ],
+ [ '2016-W01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'datetime-local', data: [
+ // Regular case.
+ [ '2017-02-07T17:09', null, null, null, null, '2017-02-07T17:10', false ],
+ // Argument testing.
+ [ '2017-02-07T17:10', null, null, null, 1, '2017-02-07T17:11', false ],
+ [ '2017-02-07T17:10', null, null, null, 5, '2017-02-07T17:15', false ],
+ [ '2017-02-07T17:10', null, null, null, -1, '2017-02-07T17:09', false ],
+ [ '2017-02-07T17:10', null, null, null, 0, '2017-02-07T17:10', false ],
+ // hour/minutes/seconds wrapping.
+ [ '2000-01-01T04:59', null, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59', 1, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59.900', 0.1, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59.990', 0.01, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59.999', 0.001, null, null, null, '2000-01-01T05:00', false ],
+ // month/year wrapping.
+ [ '2012-07-31T12:00', null, null, null, 1440, '2012-08-01T12:00', false ],
+ [ '1968-12-29T12:00', null, null, null, 5760, '1969-01-02T12:00', false ],
+ [ '1970-01-01T00:00', null, null, null, -1440, '1969-12-31T00:00', false ],
+ [ '2012-03-01T00:00', null, null, null, -1440, '2012-02-29T00:00', false ],
+ // stepUp() on '23:59' gives '00:00'.
+ [ '2017-02-07T23:59', null, null, null, 1, '2017-02-08T00:00', false ],
+ [ '2017-02-07T23:59', null, null, null, 3, '2017-02-08T00:02', false ],
+ // Some random step values..
+ [ '2017-02-07T17:40', '0.5', null, null, null, '2017-02-07T17:40:00.500', false ],
+ [ '2017-02-07T17:40', '2', null, null, null, '2017-02-07T17:40:02', false ],
+ [ '2017-02-07T17:40', '0.25', null, null, 4, '2017-02-07T17:40:01', false ],
+ [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 1, '2017-02-07T17:40:00.200', false ],
+ [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 2, '2017-02-07T17:40:01.300', false ],
+ [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 10, '2017-02-07T17:40:10.100', false ],
+ [ '2017-02-07T17:40', '129600', '2017-02-01T00:00', null, 2, '2017-02-10T00:00', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T17:39', '0', null, null, null, '2017-02-07T17:40', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T17:39', '-1', null, null, null, '2017-02-07T17:40', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2017-02-07T17:39', 'foo', null, null, null, '2017-02-07T17:40', false ],
+ // Min values testing.
+ [ '2012-02-02T17:00', '60', 'foo', null, 2, '2012-02-02T17:02', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:11', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:30', false ],
+ [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:11', false ],
+ [ '2012-02-02T17:10', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ],
+ [ '2012-02-02T17:10', '129600', '2012-02-01T00:00', null, null, '2012-02-04T00:00', false ],
+ // Max values testing.
+ [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:16', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:16', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:15', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:15', false ],
+ [ '2012-02-02T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-03T20:05', false ],
+ [ '2012-02-02T18:00', '129600', null, '2012-02-04T20:00', null, '2012-02-04T06:00', false ],
+ // Step mismatch.
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:20', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:20', false ],
+ [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:21', false ],
+ [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:22', false ],
+ [ '2017-02-03T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-04T17:19', false ],
+ // Clamping.
+ [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:22', false ],
+ [ '2017-02-06T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:20', 15, '2017-02-06T19:50', false ],
+ [ '2017-02-06T17:22', '600', '2017-02-02T17:10', '2017-02-07T17:20', 2, '2017-02-06T17:40', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-01-01T00:01', false ],
+ // With step = 'any'.
+ [ '2017-02-07T17:30', 'any', null, null, 1, null, true ],
+ [ '2017-02-07T17:30', 'ANY', null, null, 1, null, true ],
+ [ '2017-02-07T17:30', 'AnY', null, null, 1, null, true ],
+ [ '2017-02-07T17:30', 'aNy', null, null, 1, null, true ],
+ ]},
+ ];
+
+ for (var test of testData) {
+ for (var data of test.data) {
+ var element = document.createElement("input");
+ element.type = test.type;
+
+ if (data[1] != null) {
+ element.step = data[1];
+ }
+
+ if (data[2] != null) {
+ element.min = data[2];
+ }
+
+ if (data[3] != null) {
+ element.max = data[3];
+ }
+
+ // Set 'value' last for type=range, because the final sanitized value
+ // after setting 'step', 'min' and 'max' can be affected by the order in
+ // which those attributes are set. Setting 'value' last makes it simpler
+ // to reason about what the final value should be.
+ if (data[0] != null) {
+ element.setAttribute('value', data[0]);
+ }
+
+ var exceptionCaught = false;
+ try {
+ if (data[4] != null) {
+ element.stepUp(data[4]);
+ } else {
+ element.stepUp();
+ }
+
+ is(element.value, data[5], "The value for type=" + test.type + " should be " + data[5]);
+ } catch (e) {
+ exceptionCaught = true;
+ is(element.value, data[0], e.name + "The value should not have changed");
+ is(e.name, 'InvalidStateError',
+ "It should be a InvalidStateError exception.");
+ } finally {
+ is(exceptionCaught, data[6], "exception status should be " + data[6]);
+ }
+ }
+ }
+}
+
+checkPresence();
+checkAvailability();
+
+checkStepDown();
+checkStepUp();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_submit_invalid_file.html b/dom/html/test/forms/test_submit_invalid_file.html
new file mode 100644
index 0000000000..68b5e44877
--- /dev/null
+++ b/dom/html/test/forms/test_submit_invalid_file.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=702949
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test invalid file submission</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=702949">Mozilla Bug 702949</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form action='http://mochi.test:8888/chrome/dom/html/test/forms/submit_invalid_file.sjs' method='post' target='result'
+ enctype='multipart/form-data'>
+ <input type='file' name='file'>
+ </form>
+ <iframe name='result'></iframe>
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+ /*
+ * Test invalid file submission by submitting a file that has been deleted
+ * from the file system before the form has been submitted.
+ * The form submission triggers a sjs file that shows its output in a frame.
+ * That means the test might time out if it fails.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function() {
+ var { FileUtils } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ var i = document.getElementsByTagName('input')[0];
+
+ var file = FileUtils.getDir("TmpD", []);
+ file.append("testfile");
+ file.createUnique(SpecialPowers.Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ SpecialPowers.wrap(i).value = file.path;
+ file.remove(/* recursive = */ false);
+
+ document.getElementsByName('result')[0].addEventListener('load', function() {
+ is(window.frames[0].document.body.textContent, "SUCCESS");
+ SimpleTest.finish();
+ });
+ document.forms[0].submit();
+ });
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_textarea_attributes_reflection.html b/dom/html/test/forms/test_textarea_attributes_reflection.html
new file mode 100644
index 0000000000..925f97e751
--- /dev/null
+++ b/dom/html/test/forms/test_textarea_attributes_reflection.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLTextAreaElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLTextAreaElement attributes reflection **/
+
+// .autofocus
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "autofocus",
+});
+
+//.cols
+reflectUnsignedInt({
+ element: document.createElement("textarea"),
+ attribute: "cols",
+ nonZero: true,
+ defaultValue: 20,
+ fallback: true,
+});
+
+//.dirname
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "dirName"
+})
+
+// .disabled
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "disabled",
+});
+
+// TODO: form (HTMLFormElement)
+
+// .maxLength
+reflectInt({
+ element: document.createElement("textarea"),
+ attribute: "maxLength",
+ nonNegative: true,
+});
+
+// .name
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "name",
+ otherValues: [ "isindex", "_charset_" ],
+});
+
+// .placeholder
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "placeholder",
+ otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ],
+});
+
+// .readOnly
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "readOnly",
+});
+
+// .required
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "required",
+});
+
+// .rows
+reflectUnsignedInt({
+ element: document.createElement("textarea"),
+ attribute: "rows",
+ nonZero: true,
+ defaultValue: 2,
+ fallback: true,
+});
+
+// .wrap
+// TODO: make it an enumerated attributes limited to only known values, bug 670869.
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "wrap",
+ otherValues: [ "soft", "hard" ],
+});
+
+// .type doesn't reflect a content attribute.
+// .defaultValue doesn't reflect a content attribute.
+// .value doesn't reflect a content attribute.
+// .textLength doesn't reflect a content attribute.
+// .willValidate doesn't reflect a content attribute.
+// .validity doesn't reflect a content attribute.
+// .validationMessage doesn't reflect a content attribute.
+// .labels doesn't reflect a content attribute.
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_validation.html b/dom/html/test/forms/test_validation.html
new file mode 100644
index 0000000000..666d4a45c0
--- /dev/null
+++ b/dom/html/test/forms/test_validation.html
@@ -0,0 +1,343 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345624
+-->
+<head>
+ <title>Test for Bug 345624</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea, fieldset, button, select, output, object { background-color: rgb(0,0,0) !important; }
+ :valid { background-color: rgb(0,255,0) !important; }
+ :invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <fieldset id='f'></fieldset>
+ <input id='i' oninvalid="invalidEventHandler(event);">
+ <button id='b' oninvalid="invalidEventHandler(event);"></button>
+ <select id='s' oninvalid="invalidEventHandler(event);"></select>
+ <textarea id='t' oninvalid="invalidEventHandler(event);"></textarea>
+ <output id='o' oninvalid="invalidEventHandler(event);"></output>
+ <object id='obj'></object>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345624 **/
+
+var gInvalid = false;
+
+function invalidEventHandler(aEvent)
+{
+ function checkInvalidEvent(event)
+ {
+ is(event.type, "invalid", "Invalid event type should be invalid");
+ ok(!event.bubbles, "Invalid event should not bubble");
+ ok(event.cancelable, "Invalid event should be cancelable");
+ }
+
+ checkInvalidEvent(aEvent);
+
+ gInvalid = true;
+}
+
+function checkConstraintValidationAPIExist(element)
+{
+ ok('willValidate' in element, "willValidate is not available in the DOM");
+ ok('validationMessage' in element, "validationMessage is not available in the DOM");
+ ok('validity' in element, "validity is not available in the DOM");
+
+ if ('validity' in element) {
+ validity = element.validity;
+ ok('valueMissing' in validity, "validity.valueMissing is not available in the DOM");
+ ok('typeMismatch' in validity, "validity.typeMismatch is not available in the DOM");
+ ok('badInput' in validity, "validity.badInput is not available in the DOM");
+ ok('patternMismatch' in validity, "validity.patternMismatch is not available in the DOM");
+ ok('tooLong' in validity, "validity.tooLong is not available in the DOM");
+ ok('rangeUnderflow' in validity, "validity.rangeUnderflow is not available in the DOM");
+ ok('rangeOverflow' in validity, "validity.rangeOverflow is not available in the DOM");
+ ok('stepMismatch' in validity, "validity.stepMismatch is not available in the DOM");
+ ok('customError' in validity, "validity.customError is not available in the DOM");
+ ok('valid' in validity, "validity.valid is not available in the DOM");
+ }
+}
+
+function checkConstraintValidationAPIDefaultValues(element)
+{
+ // Not checking willValidate because the default value depends of the element
+
+ is(element.validationMessage, "", "validationMessage default value should be empty string");
+
+ ok(!element.validity.valueMissing, "The element should not suffer from a constraint validation");
+ ok(!element.validity.typeMismatch, "The element should not suffer from a constraint validation");
+ ok(!element.validity.badInput, "The element should not suffer from a constraint validation");
+ ok(!element.validity.patternMismatch, "The element should not suffer from a constraint validation");
+ ok(!element.validity.tooLong, "The element should not suffer from a constraint validation");
+ ok(!element.validity.rangeUnderflow, "The element should not suffer from a constraint validation");
+ ok(!element.validity.rangeOverflow, "The element should not suffer from a constraint validation");
+ ok(!element.validity.stepMismatch, "The element should not suffer from a constraint validation");
+ ok(!element.validity.customError, "The element should not suffer from a constraint validation");
+ ok(element.validity.valid, "The element should be valid by default");
+
+ ok(element.checkValidity(), "The element should be valid by default");
+}
+
+function checkDefaultPseudoClass()
+{
+ is(window.getComputedStyle(document.getElementById('f'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid should apply");
+
+ is(window.getComputedStyle(document.getElementById('o'))
+ .getPropertyValue('background-color'), "rgb(0, 0, 0)",
+ "Nor :valid and :invalid should apply");
+
+ is(window.getComputedStyle(document.getElementById('obj'))
+ .getPropertyValue('background-color'), "rgb(0, 0, 0)",
+ "Nor :valid and :invalid should apply");
+
+ is(window.getComputedStyle(document.getElementById('s'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+
+ is(window.getComputedStyle(document.getElementById('i'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+
+ is(window.getComputedStyle(document.getElementById('t'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+
+ is(window.getComputedStyle(document.getElementById('b'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+}
+
+function checkSpecificWillValidate()
+{
+ // fieldset, output, object (TODO) and select elements
+ ok(!document.getElementById('f').willValidate, "Fielset element should be barred from constraint validation");
+ ok(!document.getElementById('obj').willValidate, "Object element should be barred from constraint validation");
+ ok(!document.getElementById('o').willValidate, "Output element should be barred from constraint validation");
+ ok(document.getElementById('s').willValidate, "Select element should not be barred from constraint validation");
+
+ // input element
+ i = document.getElementById('i');
+ i.type = "hidden";
+ ok(!i.willValidate, "Hidden state input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.type = "reset";
+ ok(!i.willValidate, "Reset button state input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.type = "button";
+ ok(!i.willValidate, "Button state input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.type = "image";
+ ok(i.willValidate, "Image state input should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid and :invalid should apply");
+ i.type = "submit";
+ ok(i.willValidate, "Submit state input should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid and :invalid should apply");
+ i.type = "number";
+ ok(i.willValidate, "Number state input should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ i.type = "";
+ i.readOnly = 'true';
+ ok(!i.willValidate, "Readonly input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.removeAttribute('readOnly');
+ ok(i.willValidate, "Default input element should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+
+ // button element
+ b = document.getElementById('b');
+ b.type = "reset";
+ ok(!b.willValidate, "Reset state button should be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ b.type = "button";
+ ok(!b.willValidate, "Button state button should be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ b.type = "submit";
+ ok(b.willValidate, "Submit state button should not be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid and :invalid should apply");
+ b.type = "";
+ ok(b.willValidate, "Default button element should not be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+
+ // textarea element
+ t = document.getElementById('t');
+ t.readOnly = true;
+ ok(!t.willValidate, "Readonly textarea should be barred from constraint validation");
+ is(window.getComputedStyle(t).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ t.removeAttribute('readOnly');
+ ok(t.willValidate, "Default textarea element should not be barred from constraint validation");
+ is(window.getComputedStyle(t).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+
+ // TODO: PROGRESS
+ // TODO: METER
+}
+
+function checkCommonWillValidate(element)
+{
+ // Not checking the default value because it has been checked previously.
+
+ element.disabled = true;
+ ok(!element.willValidate, "Disabled element should be barred from constraint validation");
+
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+
+ element.removeAttribute('disabled');
+
+ // TODO: If an element has a datalist element ancestor, it is barred from constraint validation.
+}
+
+function checkCustomError(element, isBarred)
+{
+ element.setCustomValidity("message");
+ if (!isBarred) {
+ is(element.validationMessage, "message",
+ "When the element has a custom validity message, validation message should return it");
+ } else {
+ is(element.validationMessage, "",
+ "An element barred from constraint validation can't have a validation message");
+ }
+ ok(element.validity.customError, "The element should suffer from a custom error");
+ ok(!element.validity.valid, "The element should not be valid with a custom error");
+
+ if (element.tagName == "FIELDSET") {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred ? "rgb(0, 255, 0)" : "rgb(255, 0, 0)",
+ ":invalid pseudo-classs should apply to " + element.tagName);
+ }
+ else {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred ? "rgb(0, 0, 0)" : "rgb(255, 0, 0)",
+ ":invalid pseudo-classs should apply to " + element.tagName);
+ }
+
+ element.setCustomValidity("");
+ is(element.validationMessage, "", "The element should not have a validation message when reseted");
+ ok(!element.validity.customError, "The element should not suffer anymore from a custom error");
+ ok(element.validity.valid, "The element should now be valid");
+
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred && element.tagName != "FIELDSET" ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)",
+ ":valid pseudo-classs should apply");
+}
+
+function checkCheckValidity(element)
+{
+ element.setCustomValidity("message");
+ ok(!element.checkValidity(), "checkValidity() should return false when the element is not valid");
+
+ ok(gInvalid, "Invalid event should have been handled");
+
+ gInvalid = false;
+ element.setCustomValidity("");
+
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "Invalid event should not have been handled");
+}
+
+function checkValidityStateObjectAliveWithoutElement(element)
+{
+ // We are creating a temporary element and getting it's ValidityState object.
+ // Then, we make sure it is removed by the garbage collector and we check the
+ // ValidityState default values (it should not crash).
+
+ var v = document.createElement(element).validity;
+ SpecialPowers.gc();
+
+ ok(!v.valueMissing,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.typeMismatch,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.badInput,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.patternMismatch,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.tooLong,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.rangeUnderflow,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.rangeOverflow,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.stepMismatch,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.customError,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(v.valid, "When the element is not alive, it should be valid");
+}
+
+checkConstraintValidationAPIExist(document.getElementById('f'));
+checkConstraintValidationAPIExist(document.getElementById('i'));
+checkConstraintValidationAPIExist(document.getElementById('b'));
+checkConstraintValidationAPIExist(document.getElementById('s'));
+checkConstraintValidationAPIExist(document.getElementById('t'));
+checkConstraintValidationAPIExist(document.getElementById('o'));
+checkConstraintValidationAPIExist(document.getElementById('obj'));
+
+checkConstraintValidationAPIDefaultValues(document.getElementById('f'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('i'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('b'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('s'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('t'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('o'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('obj'));
+
+checkDefaultPseudoClass();
+
+checkSpecificWillValidate();
+
+// Not checking button, fieldset, output and object
+// because they are always barred from constraint validation.
+checkCommonWillValidate(document.getElementById('i'));
+checkCommonWillValidate(document.getElementById('s'));
+checkCommonWillValidate(document.getElementById('t'));
+
+checkCustomError(document.getElementById('i'), false);
+checkCustomError(document.getElementById('s'), false);
+checkCustomError(document.getElementById('t'), false);
+checkCustomError(document.getElementById('o'), true);
+checkCustomError(document.getElementById('b'), false);
+checkCustomError(document.getElementById('f'), true);
+checkCustomError(document.getElementById('obj'), true);
+
+// Not checking button, fieldset, output and object
+// because they are always barred from constraint validation.
+checkCheckValidity(document.getElementById('i'));
+checkCheckValidity(document.getElementById('s'));
+checkCheckValidity(document.getElementById('t'));
+
+checkValidityStateObjectAliveWithoutElement("fieldset");
+checkValidityStateObjectAliveWithoutElement("input");
+checkValidityStateObjectAliveWithoutElement("button");
+checkValidityStateObjectAliveWithoutElement("select");
+checkValidityStateObjectAliveWithoutElement("textarea");
+checkValidityStateObjectAliveWithoutElement("output");
+checkValidityStateObjectAliveWithoutElement("object");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_validation_not_in_doc.html b/dom/html/test/forms/test_validation_not_in_doc.html
new file mode 100644
index 0000000000..1500c60869
--- /dev/null
+++ b/dom/html/test/forms/test_validation_not_in_doc.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for constraint validation of form controls not in documents</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var input = document.createElement('input');
+ input.required = true;
+ assert_false(input.checkValidity());
+}, "Should validate input not in document");
+
+test(function() {
+ var textarea = document.createElement('textarea');
+ textarea.required = true;
+ assert_false(textarea.checkValidity());
+}, "Should validate textarea not in document");
+</script>
diff --git a/dom/html/test/forms/test_valueasdate_attribute.html b/dom/html/test/forms/test_valueasdate_attribute.html
new file mode 100644
index 0000000000..9055879a85
--- /dev/null
+++ b/dom/html/test/forms/test_valueasdate_attribute.html
@@ -0,0 +1,751 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=769370
+-->
+<head>
+ <title>Test for input.valueAsDate</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=769370">Mozilla Bug 769370</a>
+<iframe name="testFrame" style="display: none"></iframe>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 769370**/
+
+/**
+ * This test is checking .valueAsDate.
+ */
+
+var element = document.createElement("input");
+
+var validTypes =
+[
+ ["text", false],
+ ["password", false],
+ ["search", false],
+ ["tel", false],
+ ["email", false],
+ ["url", false],
+ ["hidden", false],
+ ["checkbox", false],
+ ["radio", false],
+ ["file", false],
+ ["submit", false],
+ ["image", false],
+ ["reset", false],
+ ["button", false],
+ ["number", false],
+ ["range", false],
+ ["date", true],
+ ["time", true],
+ ["color", false],
+ ["month", true],
+ ["week", true],
+ ["datetime-local", true],
+];
+
+function checkAvailability()
+{
+ for (let data of validTypes) {
+ var exceptionCatched = false;
+ element.type = data[0];
+ try {
+ element.valueAsDate;
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, false,
+ "valueAsDate shouldn't throw exception on getting");
+
+ exceptionCatched = false;
+ try {
+ element.valueAsDate = new Date();
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, !data[1], "valueAsDate for " + data[0] +
+ " availability is not correct");
+ }
+}
+
+function checkGarbageValues()
+{
+ for (let type of validTypes) {
+ if (!type[1]) {
+ continue;
+ }
+ type = type[0];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = type;
+
+ inputElement.value = "test";
+ inputElement.valueAsDate = null;
+ is(inputElement.value, "", "valueAsDate should set the value to the empty string");
+
+ inputElement.value = "test";
+ inputElement.valueAsDate = undefined;
+ is(inputElement.value, "", "valueAsDate should set the value to the empty string");
+
+ inputElement.value = "test";
+ inputElement.valueAsDate = new Date(NaN);
+ is(inputElement.value, "", "valueAsDate should set the value to the empty string");
+
+ var illegalValues = [
+ "foobar", 42, {}, function() { return 42; }, function() { return Date(); }
+ ];
+
+ for (let value of illegalValues) {
+ try {
+ var caught = false;
+ inputElement.valueAsDate = value;
+ } catch(e) {
+ is(e.name, "TypeError", "Exception should be 'TypeError'.");
+ caught = true;
+ }
+ ok(caught, "Assigning " + value + " to .valueAsDate should throw");
+ }
+ }
+}
+
+function checkDateGet()
+{
+ var validData =
+ [
+ [ "2012-07-12", 1342051200000 ],
+ [ "1970-01-01", 0 ],
+ [ "1970-01-02", 86400000 ],
+ [ "1969-12-31", -86400000 ],
+ [ "0311-01-31", -52350451200000 ],
+ [ "275760-09-13", 8640000000000000 ],
+ [ "0001-01-01", -62135596800000 ],
+ [ "2012-02-29", 1330473600000 ],
+ [ "2011-02-28", 1298851200000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invaliddate" ],
+ [ "-001-12-31" ],
+ [ "901-12-31" ],
+ [ "1901-13-31" ],
+ [ "1901-12-32" ],
+ [ "1901-00-12" ],
+ [ "1901-01-00" ],
+ [ "1900-02-29" ],
+ [ "0000-01-01" ],
+ [ "" ],
+ // This date is valid for the input element, but is out of
+ // the date object range. In this case, on getting valueAsDate,
+ // a Date object will be created, but it will have a NaN internal value,
+ // and will return the string "Invalid Date".
+ [ "275760-09-14", true ],
+ ];
+
+ element.type = "date";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this date");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid date");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid date");
+ }
+ }
+}
+
+function checkDateSet()
+{
+ var testData =
+ [
+ [ 1342051200000, "2012-07-12" ],
+ [ 0, "1970-01-01" ],
+ // Maximum valid date (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13" ],
+ // Minimum valid date (limited by the input element minimum valid value).
+ [ -62135596800000 , "0001-01-01" ],
+ [ 1330473600000, "2012-02-29" ],
+ [ 1298851200000, "2011-02-28" ],
+ // "Values must be truncated to valid dates"
+ [ 42.1234, "1970-01-01" ],
+ [ 123.123456789123, "1970-01-01" ],
+ [ 1e-1, "1970-01-01" ],
+ [ 1298851200010, "2011-02-28" ],
+ [ -1, "1969-12-31" ],
+ [ -86400000, "1969-12-31" ],
+ [ 86400000, "1970-01-02" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding date string is the empty string
+ [ -62135596800001, "" ],
+ // Invalid dates.
+ ];
+
+ element.type = "date";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to "
+ + data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+function checkTimeGet()
+{
+ var tests = [
+ // Some invalid values to begin.
+ { value: "", result: null },
+ { value: "foobar", result: null },
+ { value: "00:", result: null },
+ { value: "24:00", result: null },
+ { value: "00:99", result: null },
+ { value: "00:00:", result: null },
+ { value: "00:00:99", result: null },
+ { value: "00:00:00:", result: null },
+ { value: "00:00:00.", result: null },
+ { value: "00:00:00.0000", result: null },
+ // Some simple valid values.
+ { value: "00:00", result: { time: 0, hours: 0, minutes: 0, seconds: 0, ms: 0 } },
+ { value: "00:01", result: { time: 60000, hours: 0, minutes: 1, seconds: 0, ms: 0 } },
+ { value: "01:00", result: { time: 3600000, hours: 1, minutes: 0, seconds: 0, ms: 0 } },
+ { value: "01:01", result: { time: 3660000, hours: 1, minutes: 1, seconds: 0, ms: 0 } },
+ { value: "13:37", result: { time: 49020000, hours: 13, minutes: 37, seconds: 0, ms: 0 } },
+ // Valid values including seconds.
+ { value: "00:00:01", result: { time: 1000, hours: 0, minutes: 0, seconds: 1, ms: 0 } },
+ { value: "13:37:42", result: { time: 49062000, hours: 13, minutes: 37, seconds: 42, ms: 0 } },
+ // Valid values including seconds fractions.
+ { value: "00:00:00.001", result: { time: 1, hours: 0, minutes: 0, seconds: 0, ms: 1 } },
+ { value: "00:00:00.123", result: { time: 123, hours: 0, minutes: 0, seconds: 0, ms: 123 } },
+ { value: "00:00:00.100", result: { time: 100, hours: 0, minutes: 0, seconds: 0, ms: 100 } },
+ { value: "00:00:00.000", result: { time: 0, hours: 0, minutes: 0, seconds: 0, ms: 0 } },
+ { value: "20:17:31.142", result: { time: 73051142, hours: 20, minutes: 17, seconds: 31, ms: 142 } },
+ // Highest possible value.
+ { value: "23:59:59.999", result: { time: 86399999, hours: 23, minutes: 59, seconds: 59, ms: 999 } },
+ // Some values with one or two digits for the fraction of seconds.
+ { value: "00:00:00.1", result: { time: 100, hours: 0, minutes: 0, seconds: 0, ms: 100 } },
+ { value: "00:00:00.14", result: { time: 140, hours: 0, minutes: 0, seconds: 0, ms: 140 } },
+ { value: "13:37:42.7", result: { time: 49062700, hours: 13, minutes: 37, seconds: 42, ms: 700 } },
+ { value: "23:31:12.23", result: { time: 84672230, hours: 23, minutes: 31, seconds: 12, ms: 230 } },
+ ];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = 'time';
+
+ for (let test of tests) {
+ inputElement.value = test.value;
+ if (test.result === null) {
+ is(inputElement.valueAsDate, null, "element.valueAsDate should return null");
+ } else {
+ var date = inputElement.valueAsDate;
+ isnot(date, null, "element.valueAsDate should not be null");
+
+ is(date.getTime(), test.result.time);
+ is(date.getUTCHours(), test.result.hours);
+ is(date.getUTCMinutes(), test.result.minutes);
+ is(date.getUTCSeconds(), test.result.seconds);
+ is(date.getUTCMilliseconds(), test.result.ms);
+ }
+ }
+}
+
+function checkTimeSet()
+{
+ var tests = [
+ // Simple tests.
+ { value: 0, result: "00:00" },
+ { value: 1, result: "00:00:00.001" },
+ { value: 100, result: "00:00:00.100" },
+ { value: 1000, result: "00:00:01" },
+ { value: 60000, result: "00:01" },
+ { value: 3600000, result: "01:00" },
+ { value: 83622234, result: "23:13:42.234" },
+ // Some edge cases.
+ { value: 86400000, result: "00:00" },
+ { value: 86400001, result: "00:00:00.001" },
+ { value: 170022234, result: "23:13:42.234" },
+ { value: 432000000, result: "00:00" },
+ { value: -1, result: "23:59:59.999" },
+ { value: -86400000, result: "00:00" },
+ { value: -86400001, result: "23:59:59.999" },
+ { value: -56789, result: "23:59:03.211" },
+ { value: 0.9, result: "00:00" },
+ ];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = 'time';
+
+ for (let test of tests) {
+ inputElement.valueAsDate = new Date(test.value);
+ is(inputElement.value, test.result,
+ "element.value should have been changed by setting valueAsDate");
+ }
+}
+
+function checkWithBustedPrototype()
+{
+ for (let type of validTypes) {
+ if (!type[1]) {
+ continue;
+ }
+
+ type = type[0];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = type;
+
+ var backupPrototype = {};
+ backupPrototype.getUTCFullYear = Date.prototype.getUTCFullYear;
+ backupPrototype.getUTCMonth = Date.prototype.getUTCMonth;
+ backupPrototype.getUTCDate = Date.prototype.getUTCDate;
+ backupPrototype.getTime = Date.prototype.getTime;
+ backupPrototype.setUTCFullYear = Date.prototype.setUTCFullYear;
+
+ Date.prototype.getUTCFullYear = function() { return {}; };
+ Date.prototype.getUTCMonth = function() { return {}; };
+ Date.prototype.getUTCDate = function() { return {}; };
+ Date.prototype.getTime = function() { return {}; };
+ Date.prototype.setUTCFullYear = function(y,m,d) { };
+
+ inputElement.valueAsDate = new Date();
+
+ isnot(inputElement.valueAsDate, null, ".valueAsDate should not return null");
+ // The object returned by element.valueAsDate should return a Date object
+ // with the same prototype:
+ is(inputElement.valueAsDate.getUTCFullYear, Date.prototype.getUTCFullYear,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCMonth, Date.prototype.getUTCMonth,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCDate, Date.prototype.getUTCDate,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getTime, Date.prototype.getTime,
+ "prototype is the same");
+ is(inputElement.valueAsDate.setUTCFullYear, Date.prototype.setUTCFullYear,
+ "prototype is the same");
+
+ // However the Date should have the correct information.
+ // Skip type=month for now, since .valueAsNumber returns number of months
+ // and not milliseconds.
+ if (type != "month") {
+ var witnessDate = new Date(inputElement.valueAsNumber);
+ is(inputElement.valueAsDate.valueOf(), witnessDate.valueOf(), "correct Date");
+ }
+
+ // Same test as above but using NaN instead of {}.
+
+ Date.prototype.getUTCFullYear = function() { return NaN; };
+ Date.prototype.getUTCMonth = function() { return NaN; };
+ Date.prototype.getUTCDate = function() { return NaN; };
+ Date.prototype.getTime = function() { return NaN; };
+ Date.prototype.setUTCFullYear = function(y,m,d) { };
+
+ inputElement.valueAsDate = new Date();
+
+ isnot(inputElement.valueAsDate, null, ".valueAsDate should not return null");
+ // The object returned by element.valueAsDate should return a Date object
+ // with the same prototype:
+ is(inputElement.valueAsDate.getUTCFullYear, Date.prototype.getUTCFullYear,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCMonth, Date.prototype.getUTCMonth,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCDate, Date.prototype.getUTCDate,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getTime, Date.prototype.getTime,
+ "prototype is the same");
+ is(inputElement.valueAsDate.setUTCFullYear, Date.prototype.setUTCFullYear,
+ "prototype is the same");
+
+ // However the Date should have the correct information.
+ // Skip type=month for now, since .valueAsNumber returns number of months
+ // and not milliseconds.
+ if (type != "month") {
+ var witnessDate = new Date(inputElement.valueAsNumber);
+ is(inputElement.valueAsDate.valueOf(), witnessDate.valueOf(), "correct Date");
+ }
+
+ Date.prototype.getUTCFullYear = backupPrototype.getUTCFullYear;
+ Date.prototype.getUTCMonth = backupPrototype.getUTCMonth;
+ Date.prototype.getUTCDate = backupPrototype.getUTCDate;
+ Date.prototype.getTime = backupPrototype.getTime;
+ Date.prototype.setUTCFullYear = backupPrototype.setUTCFullYear;
+ }
+}
+
+function checkMonthGet()
+{
+ var validData =
+ [
+ [ "2016-07", 1467331200000 ],
+ [ "1970-01", 0 ],
+ [ "1970-02", 2678400000 ],
+ [ "1969-12", -2678400000 ],
+ [ "0001-01", -62135596800000 ],
+ [ "275760-09", 8639998963200000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invalidmonth" ],
+ [ "0000-01" ],
+ [ "2016-00" ],
+ [ "123-01" ],
+ [ "2017-13" ],
+ [ "" ],
+ // This month is valid for the input element, but is out of
+ // the date object range. In this case, on getting valueAsDate,
+ // a Date object will be created, but it will have a NaN internal value,
+ // and will return the string "Invalid Date".
+ [ "275760-10", true ],
+ ];
+
+ element.type = "month";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this month");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid month");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid month");
+ }
+ }
+}
+
+function checkMonthSet()
+{
+ var testData =
+ [
+ [ 1342051200000, "2012-07" ],
+ [ 0, "1970-01" ],
+ // Maximum valid month (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09" ],
+ // Minimum valid month (limited by the input element minimum valid value).
+ [ -62135596800000 , "0001-01" ],
+ [ 1330473600000, "2012-02" ],
+ [ 1298851200000, "2011-02" ],
+ // "Values must be truncated to valid months"
+ [ 42.1234, "1970-01" ],
+ [ 123.123456789123, "1970-01" ],
+ [ 1e-1, "1970-01" ],
+ [ 1298851200010, "2011-02" ],
+ [ -1, "1969-12" ],
+ [ -86400000, "1969-12" ],
+ [ 86400000, "1970-01" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding month string is the empty string
+ [ -62135596800001, "" ],
+ ];
+
+ element.type = "month";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to "
+ + data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+function checkWeekGet()
+{
+ var validData =
+ [
+ // Common years starting on different days of week.
+ [ "2007-W01", Date.UTC(2007, 0, 1) ], // Mon
+ [ "2013-W01", Date.UTC(2012, 11, 31) ], // Tue
+ [ "2014-W01", Date.UTC(2013, 11, 30) ], // Wed
+ [ "2015-W01", Date.UTC(2014, 11, 29) ], // Thu
+ [ "2010-W01", Date.UTC(2010, 0, 4) ], // Fri
+ [ "2011-W01", Date.UTC(2011, 0, 3) ], // Sat
+ [ "2017-W01", Date.UTC(2017, 0, 2) ], // Sun
+ // Common years ending on different days of week.
+ [ "2007-W52", Date.UTC(2007, 11, 24) ], // Mon
+ [ "2013-W52", Date.UTC(2013, 11, 23) ], // Tue
+ [ "2014-W52", Date.UTC(2014, 11, 22) ], // Wed
+ [ "2015-W53", Date.UTC(2015, 11, 28) ], // Thu
+ [ "2010-W52", Date.UTC(2010, 11, 27) ], // Fri
+ [ "2011-W52", Date.UTC(2011, 11, 26) ], // Sat
+ [ "2017-W52", Date.UTC(2017, 11, 25) ], // Sun
+ // Leap years starting on different days of week.
+ [ "1996-W01", Date.UTC(1996, 0, 1) ], // Mon
+ [ "2008-W01", Date.UTC(2007, 11, 31) ], // Tue
+ [ "2020-W01", Date.UTC(2019, 11, 30) ], // Wed
+ [ "2004-W01", Date.UTC(2003, 11, 29) ], // Thu
+ [ "2016-W01", Date.UTC(2016, 0, 4) ], // Fri
+ [ "2000-W01", Date.UTC(2000, 0, 3) ], // Sat
+ [ "2012-W01", Date.UTC(2012, 0, 2) ], // Sun
+ // Leap years ending on different days of week.
+ [ "2012-W52", Date.UTC(2012, 11, 24) ], // Mon
+ [ "2024-W52", Date.UTC(2024, 11, 23) ], // Tue
+ [ "1980-W52", Date.UTC(1980, 11, 22) ], // Wed
+ [ "1992-W53", Date.UTC(1992, 11, 28) ], // Thu
+ [ "2004-W53", Date.UTC(2004, 11, 27) ], // Fri
+ [ "1988-W52", Date.UTC(1988, 11, 26) ], // Sat
+ [ "2000-W52", Date.UTC(2000, 11, 25) ], // Sun
+ // Other normal cases.
+ [ "2016-W36", 1473033600000 ],
+ [ "1969-W52", -864000000 ],
+ [ "1970-W01", -259200000 ],
+ [ "275760-W37", 8639999568000000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invalidweek" ],
+ [ "0000-W01" ],
+ [ "2016-W00" ],
+ [ "123-W01" ],
+ [ "2016-W53" ],
+ [ "" ],
+ // This week is valid for the input element, but is out of
+ // the date object range. In this case, on getting valueAsDate,
+ // a Date object will be created, but it will have a NaN internal value,
+ // and will return the string "Invalid Date".
+ [ "275760-W38", true ],
+ ];
+
+ element.type = "week";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this week");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid week");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid week");
+ }
+ }
+}
+
+function checkWeekSet()
+{
+ var testData =
+ [
+ // Common years starting on different days of week.
+ [ Date.UTC(2007, 0, 1), "2007-W01" ], // Mon
+ [ Date.UTC(2013, 0, 1), "2013-W01" ], // Tue
+ [ Date.UTC(2014, 0, 1), "2014-W01" ], // Wed
+ [ Date.UTC(2015, 0, 1), "2015-W01" ], // Thu
+ [ Date.UTC(2010, 0, 1), "2009-W53" ], // Fri
+ [ Date.UTC(2011, 0, 1), "2010-W52" ], // Sat
+ [ Date.UTC(2017, 0, 1), "2016-W52" ], // Sun
+ // Common years ending on different days of week.
+ [ Date.UTC(2007, 11, 31), "2008-W01" ], // Mon
+ [ Date.UTC(2013, 11, 31), "2014-W01" ], // Tue
+ [ Date.UTC(2014, 11, 31), "2015-W01" ], // Wed
+ [ Date.UTC(2015, 11, 31), "2015-W53" ], // Thu
+ [ Date.UTC(2010, 11, 31), "2010-W52" ], // Fri
+ [ Date.UTC(2011, 11, 31), "2011-W52" ], // Sat
+ [ Date.UTC(2017, 11, 31), "2017-W52" ], // Sun
+ // Leap years starting on different days of week.
+ [ Date.UTC(1996, 0, 1), "1996-W01" ], // Mon
+ [ Date.UTC(2008, 0, 1), "2008-W01" ], // Tue
+ [ Date.UTC(2020, 0, 1), "2020-W01" ], // Wed
+ [ Date.UTC(2004, 0, 1), "2004-W01" ], // Thu
+ [ Date.UTC(2016, 0, 1), "2015-W53" ], // Fri
+ [ Date.UTC(2000, 0, 1), "1999-W52" ], // Sat
+ [ Date.UTC(2012, 0, 1), "2011-W52" ], // Sun
+ // Leap years ending on different days of week.
+ [ Date.UTC(2012, 11, 31), "2013-W01" ], // Mon
+ [ Date.UTC(2024, 11, 31), "2025-W01" ], // Tue
+ [ Date.UTC(1980, 11, 31), "1981-W01" ], // Wed
+ [ Date.UTC(1992, 11, 31), "1992-W53" ], // Thu
+ [ Date.UTC(2004, 11, 31), "2004-W53" ], // Fri
+ [ Date.UTC(1988, 11, 31), "1988-W52" ], // Sat
+ [ Date.UTC(2000, 11, 31), "2000-W52" ], // Sun
+ // Other normal cases.
+ [ Date.UTC(2016, 8, 9), "2016-W36" ],
+ [ Date.UTC(2010, 0, 3), "2009-W53" ],
+ [ Date.UTC(2010, 0, 4), "2010-W01" ],
+ [ Date.UTC(2010, 0, 10), "2010-W01" ],
+ [ Date.UTC(2010, 0, 11), "2010-W02" ],
+ [ 0, "1970-W01" ],
+ // Maximum valid month (limited by the ecma date object range).
+ [ 8640000000000000, "275760-W37" ],
+ // Minimum valid month (limited by the input element minimum valid value).
+ [ -62135596800000 , "0001-W01" ],
+ // "Values must be truncated to valid week"
+ [ 42.1234, "1970-W01" ],
+ [ 123.123456789123, "1970-W01" ],
+ [ 1e-1, "1970-W01" ],
+ [ -1.1, "1970-W01" ],
+ [ -345600000, "1969-W52" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding week string is the empty string
+ [ -62135596800001, "" ],
+ ];
+
+ element.type = "week";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to "
+ + data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+function checkDatetimeLocalGet()
+{
+ var validData =
+ [
+ // Simple cases.
+ [ "2016-12-27T10:30", Date.UTC(2016, 11, 27, 10, 30, 0) ],
+ [ "2016-12-27T10:30:40", Date.UTC(2016, 11, 27, 10, 30, 40) ],
+ [ "2016-12-27T10:30:40.567", Date.UTC(2016, 11, 27, 10, 30, 40, 567) ],
+ [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ],
+ [ "1970-01-01T00:00", 0 ],
+ // Leap years.
+ [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ],
+ [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ],
+ [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ],
+ [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ],
+ [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ],
+ // Maximum valid datetime-local (limited by the ecma date object range).
+ [ "275760-09-13T00:00", 8640000000000000 ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ "0001-01-01T00:00", -62135596800000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invaliddateime-local" ],
+ [ "0000-01-01T00:00" ],
+ [ "2016-12-25T00:00Z" ],
+ [ "2015-02-29T12:34" ],
+ [ "1-1-1T12:00" ],
+ [ "" ],
+ // This datetime-local is valid for the input element, but is out of the
+ // date object range. In this case, on getting valueAsDate, a Date object
+ // will be created, but it will have a NaN internal value, and will return
+ // the string "Invalid Date".
+ [ "275760-09-13T12:00", true ],
+ ];
+
+ element.type = "datetime-local";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this datetime-local");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid datetime-local");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid datetime-local");
+ }
+ }
+}
+
+function checkDatetimeLocalSet()
+{
+ var testData =
+ [
+ // Simple cases.
+ [ Date.UTC(2016, 11, 27, 10, 30, 0), "2016-12-27T10:30" ],
+ [ Date.UTC(2016, 11, 27, 10, 30, 30), "2016-12-27T10:30:30" ],
+ [ Date.UTC(1999, 11, 31, 23, 59, 59), "1999-12-31T23:59:59" ],
+ [ Date.UTC(1999, 11, 31, 23, 59, 59, 999), "1999-12-31T23:59:59.999" ],
+ [ Date.UTC(123456, 7, 8, 9, 10), "123456-08-08T09:10" ],
+ [ 0, "1970-01-01T00:00" ],
+ // Maximum valid datetime-local (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13T00:00" ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ -62135596800000, "0001-01-01T00:00" ],
+ // Leap years.
+ [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ],
+ [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ],
+ [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ],
+ [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ],
+ [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ],
+ // "Values must be truncated to valid datetime-local"
+ [ 123.123456789123, "1970-01-01T00:00:00.123" ],
+ [ 1e-1, "1970-01-01T00:00" ],
+ [ -1.1, "1969-12-31T23:59:59.999" ],
+ [ -345600000, "1969-12-28T00:00" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding datetime-local string is the empty string
+ [ -62135596800001, "" ],
+ ];
+
+ element.type = "datetime-local";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to " +
+ data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+checkAvailability();
+checkGarbageValues();
+checkWithBustedPrototype();
+
+// Test <input type='date'>.
+checkDateGet();
+checkDateSet();
+
+// Test <input type='time'>.
+checkTimeGet();
+checkTimeSet();
+
+// Test <input type='month'>.
+checkMonthGet();
+checkMonthSet();
+
+// Test <input type='week'>.
+checkWeekGet();
+checkWeekSet();
+
+// Test <input type='datetime-local'>.
+checkDatetimeLocalGet();
+checkDatetimeLocalSet();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_valueasnumber_attribute.html b/dom/html/test/forms/test_valueasnumber_attribute.html
new file mode 100644
index 0000000000..5f7537f7a8
--- /dev/null
+++ b/dom/html/test/forms/test_valueasnumber_attribute.html
@@ -0,0 +1,858 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=636737
+-->
+<head>
+ <title>Test for Bug input.valueAsNumber</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636737">Mozilla Bug 636737</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 636737 **/
+
+/**
+ * This test is checking .valueAsNumber.
+ */
+
+function checkAvailability()
+{
+ var testData =
+ [
+ ["text", false],
+ ["password", false],
+ ["search", false],
+ ["tel", false],
+ ["email", false],
+ ["url", false],
+ ["hidden", false],
+ ["checkbox", false],
+ ["radio", false],
+ ["file", false],
+ ["submit", false],
+ ["image", false],
+ ["reset", false],
+ ["button", false],
+ ["number", true],
+ ["range", true],
+ ["date", true],
+ ["time", true],
+ ["color", false],
+ ["month", true],
+ ["week", true],
+ ["datetime-local", true],
+ ];
+
+ var element = document.createElement('input');
+
+ for (let data of testData) {
+ var exceptionCatched = false;
+ element.type = data[0];
+ try {
+ element.valueAsNumber;
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, false,
+ "valueAsNumber shouldn't throw exception on getting");
+
+ exceptionCatched = false;
+ try {
+ element.valueAsNumber = 42;
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, !data[1], "valueAsNumber for " + data[0] +
+ " availability is not correct");
+ }
+}
+
+function checkNumberGet()
+{
+ var testData =
+ [
+ ["42", 42],
+ ["-42", -42], // should work for negative values
+ ["42.1234", 42.1234],
+ ["123.123456789123", 123.123456789123], // double precision
+ ["1e2", 100], // e should be usable
+ ["2e1", 20],
+ ["1e-1", 0.1], // value after e can be negative
+ ["1E2", 100], // E can be used instead of e
+ ["e", null],
+ ["e2", null],
+ ["1e0.1", null],
+ ["", null], // the empty string is not a number
+ ["foo", null],
+ ["42,13", null], // comma can't be used as a decimal separator
+ ];
+
+ var element = document.createElement('input');
+ element.type = "number";
+ for (let data of testData) {
+ element.value = data[0];
+
+ // Given that NaN != NaN, we have to use null when the expected value is NaN.
+ if (data[1] != null) {
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "floating point representation of the value");
+ } else {
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a number");
+ }
+ }
+}
+
+function checkNumberSet()
+{
+ var testData =
+ [
+ [42, "42"],
+ [-42, "-42"], // should work for negative values
+ [42.1234, "42.1234"],
+ [123.123456789123, "123.123456789123"], // double precision
+ [1e2, "100"], // e should be usable
+ [2e1, "20"],
+ [1e-1, "0.1"], // value after e can be negative
+ [1E2, "100"], // E can be used instead of e
+ // Setting a string will set NaN.
+ ["foo", ""],
+ // "" is converted to 0.
+ ["", "0"],
+ [42, "42"], // Keep this here, it is used by the next test.
+ // Setting Infinity should throw and not change the current value.
+ [Infinity, "42", true],
+ [-Infinity, "42", true],
+ // Setting NaN should change the value to the empty string.
+ [NaN, ""],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "number";
+ for (let data of testData) {
+ var caught = false;
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1],
+ "valueAsNumber should be able to set the value");
+ } catch (e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkRangeGet()
+{
+ // For type=range we should never get NaN since the user agent is required
+ // to fix up the input's value to be something sensible.
+
+ var min = -200;
+ var max = 200;
+ var defaultValue = min + (max - min)/2;
+
+ var testData =
+ [
+ ["42", 42],
+ ["-42", -42], // should work for negative values
+ ["42.1234", 42.1234],
+ ["123.123456789123", 123.123456789123], // double precision
+ ["1e2", 100], // e should be usable
+ ["2e1", 20],
+ ["1e-1", 0.1], // value after e can be negative
+ ["1E2", 100], // E can be used instead of e
+ ["e", defaultValue],
+ ["e2", defaultValue],
+ ["1e0.1", defaultValue],
+ ["", defaultValue],
+ ["foo", defaultValue],
+ ["42,13", defaultValue],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "range";
+ element.setAttribute("min", min); // avoids out of range sanitization
+ element.setAttribute("max", max);
+ element.setAttribute("step", "any"); // avoids step mismatch sanitization
+ for (let data of testData) {
+ element.value = data[0];
+
+ // Given that NaN != NaN, we have to use null when the expected value is NaN.
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "floating point representation of the value");
+ }
+}
+
+function checkRangeSet()
+{
+ var min = -200;
+ var max = 200;
+ var defaultValue = String(min + (max - min)/2);
+
+ var testData =
+ [
+ [42, "42"],
+ [-42, "-42"], // should work for negative values
+ [42.1234, "42.1234"],
+ [123.123456789123, "123.123456789123"], // double precision
+ [1e2, "100"], // e should be usable
+ [2e1, "20"],
+ [1e-1, "0.1"], // value after e can be negative
+ [1E2, "100"], // E can be used instead of e
+ ["foo", defaultValue],
+ ["", defaultValue],
+ [42, "42"], // Keep this here, it is used by the next test.
+ // Setting Infinity should throw and not change the current value.
+ [Infinity, "42", true],
+ [-Infinity, "42", true],
+ // Setting NaN should change the value to the empty string.
+ [NaN, defaultValue],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "range";
+ element.setAttribute("min", min); // avoids out of range sanitization
+ element.setAttribute("max", max);
+ element.setAttribute("step", "any"); // avoids step mismatch sanitization
+ for (let data of testData) {
+ var caught = false;
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1],
+ "valueAsNumber should be able to set the value");
+ } catch (e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkDateGet()
+{
+ var validData =
+ [
+ [ "2012-07-12", 1342051200000 ],
+ [ "1970-01-01", 0 ],
+ // We are supposed to support at least until this date.
+ // (corresponding to the date object maximal value)
+ [ "275760-09-13", 8640000000000000 ],
+ // Minimum valid date (limited by the input element minimum valid value)
+ [ "0001-01-01", -62135596800000 ],
+ [ "2012-02-29", 1330473600000 ],
+ [ "2011-02-28", 1298851200000 ],
+ ];
+
+ var invalidData =
+ [
+ "invaliddate",
+ "",
+ "275760-09-14",
+ "999-12-31",
+ "-001-12-31",
+ "0000-01-01",
+ "2011-02-29",
+ "1901-13-31",
+ "1901-12-32",
+ "1901-00-12",
+ "1901-01-00",
+ "1900-02-29",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "date";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "timestamp representing this date");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid date");
+ }
+}
+
+function checkDateSet()
+{
+ var testData =
+ [
+ [ 1342051200000, "2012-07-12" ],
+ [ 0, "1970-01-01" ],
+ // Maximum valid date (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13" ],
+ // Minimum valid date (limited by the input element minimum valid value)
+ [ -62135596800000, "0001-01-01" ],
+ [ 1330473600000, "2012-02-29" ],
+ [ 1298851200000, "2011-02-28" ],
+ // "Values must be truncated to valid dates"
+ [ 42.1234, "1970-01-01" ],
+ [ 123.123456789123, "1970-01-01" ],
+ [ 1e2, "1970-01-01" ],
+ [ 1E9, "1970-01-12" ],
+ [ 1e-1, "1970-01-01" ],
+ [ 2e10, "1970-08-20" ],
+ [ 1298851200010, "2011-02-28" ],
+ [ -1, "1969-12-31" ],
+ [ -86400000, "1969-12-31" ],
+ [ 86400000, "1970-01-02" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-01-01" ],
+ [ true, "1970-01-01" ],
+ [ false, "1970-01-01" ],
+ [ null, "1970-01-01" ],
+ // Those are converted to NaN, the corresponding date string is the empty string
+ [ "invaliddatenumber", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Out of range, the corresponding date string is the empty string
+ [ -62135596800001, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value).
+ [ 1298851200010, "2011-02-28" ],
+ [ Infinity, "2011-02-28", true ],
+ [ -Infinity, "2011-02-28", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "date";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " + data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+
+}
+
+function checkTimeGet()
+{
+ var tests = [
+ // Some invalid values to begin.
+ { value: "", result: NaN },
+ { value: "foobar", result: NaN },
+ { value: "00:", result: NaN },
+ { value: "24:00", result: NaN },
+ { value: "00:99", result: NaN },
+ { value: "00:00:", result: NaN },
+ { value: "00:00:99", result: NaN },
+ { value: "00:00:00:", result: NaN },
+ { value: "00:00:00.", result: NaN },
+ { value: "00:00:00.0000", result: NaN },
+ // Some simple valid values.
+ { value: "00:00", result: 0 },
+ { value: "00:01", result: 60000 },
+ { value: "01:00", result: 3600000 },
+ { value: "01:01", result: 3660000 },
+ { value: "13:37", result: 49020000 },
+ // Valid values including seconds.
+ { value: "00:00:01", result: 1000 },
+ { value: "13:37:42", result: 49062000 },
+ // Valid values including seconds fractions.
+ { value: "00:00:00.001", result: 1 },
+ { value: "00:00:00.123", result: 123 },
+ { value: "00:00:00.100", result: 100 },
+ { value: "00:00:00.000", result: 0 },
+ { value: "20:17:31.142", result: 73051142 },
+ // Highest possible value.
+ { value: "23:59:59.999", result: 86399999 },
+ // Some values with one or two digits for the fraction of seconds.
+ { value: "00:00:00.1", result: 100 },
+ { value: "00:00:00.14", result: 140 },
+ { value: "13:37:42.7", result: 49062700 },
+ { value: "23:31:12.23", result: 84672230 },
+ ];
+
+ var element = document.createElement('input');
+ element.type = 'time';
+
+ for (let test of tests) {
+ element.value = test.value;
+ if (isNaN(test.result)) {
+ ok(isNaN(element.valueAsNumber),
+ "invalid value should have .valueAsNumber return NaN");
+ } else {
+ is(element.valueAsNumber, test.result,
+ ".valueAsNumber should return " + test.result);
+ }
+ }
+}
+
+function checkTimeSet()
+{
+ var tests = [
+ // Some NaN values (should set to empty string).
+ { value: NaN, result: "" },
+ { value: "foobar", result: "" },
+ { value() {}, result: "" },
+ // Inifinity (should throw).
+ { value: Infinity, throw: true },
+ { value: -Infinity, throw: true },
+ // "" converts to 0... JS is fun :)
+ { value: "", result: "00:00" },
+ // Simple tests.
+ { value: 0, result: "00:00" },
+ { value: 1, result: "00:00:00.001" },
+ { value: 100, result: "00:00:00.100" },
+ { value: 1000, result: "00:00:01" },
+ { value: 60000, result: "00:01" },
+ { value: 3600000, result: "01:00" },
+ { value: 83622234, result: "23:13:42.234" },
+ // Some edge cases.
+ { value: 86400000, result: "00:00" },
+ { value: 86400001, result: "00:00:00.001" },
+ { value: 170022234, result: "23:13:42.234" },
+ { value: 432000000, result: "00:00" },
+ { value: -1, result: "23:59:59.999" },
+ { value: -86400000, result: "00:00" },
+ { value: -86400001, result: "23:59:59.999" },
+ { value: -56789, result: "23:59:03.211" },
+ { value: 0.9, result: "00:00" },
+ ];
+
+ var element = document.createElement('input');
+ element.type = 'time';
+
+ for (let test of tests) {
+ try {
+ var caught = false;
+ element.valueAsNumber = test.value;
+ is(element.value, test.result, "value should return " + test.result);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (!test.throw) {
+ test.throw = false;
+ }
+
+ is(caught, test.throw, "the test throwing status should be " + test.throw);
+ }
+}
+
+function checkMonthGet()
+{
+ var validData =
+ [
+ [ "2016-07", 558 ],
+ [ "1970-01", 0 ],
+ [ "1969-12", -1 ],
+ [ "0001-01", -23628 ],
+ [ "10000-12", 96371 ],
+ [ "275760-09", 3285488 ],
+ ];
+
+ var invalidData =
+ [
+ "invalidmonth",
+ "0000-01",
+ "2000-00",
+ "2012-13",
+ // Out of range.
+ "275760-10",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "month";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "integer value representing this month");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid month");
+ }
+}
+
+function checkMonthSet()
+{
+ var testData =
+ [
+ [ 558, "2016-07" ],
+ [ 0, "1970-01" ],
+ [ -1, "1969-12" ],
+ [ 96371, "10000-12" ],
+ [ 12, "1971-01" ],
+ [ -12, "1969-01" ],
+ // Maximum valid month (limited by the ecma date object range)
+ [ 3285488, "275760-09" ],
+ // Minimum valid month (limited by the input element minimum valid value)
+ [ -23628, "0001-01" ],
+ // "Values must be truncated to valid months"
+ [ 0.3, "1970-01" ],
+ [ -1.1, "1969-11" ],
+ [ 1e2, "1978-05" ],
+ [ 1e-1, "1970-01" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-01" ],
+ [ true, "1970-02" ],
+ [ false, "1970-01" ],
+ [ null, "1970-01" ],
+ // Those are converted to NaN, the corresponding month string is the empty string
+ [ "invalidmonth", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Out of range, the corresponding month string is the empty string
+ [ -23629, "" ],
+ [ 3285489, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value)
+ [ 558, "2016-07" ],
+ [ Infinity, "2016-07", true ],
+ [ -Infinity, "2016-07", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "month";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " + data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkWeekGet()
+{
+ var validData =
+ [
+ // Common years starting on different days of week.
+ [ "2007-W01", Date.UTC(2007, 0, 1) ], // Mon
+ [ "2013-W01", Date.UTC(2012, 11, 31) ], // Tue
+ [ "2014-W01", Date.UTC(2013, 11, 30) ], // Wed
+ [ "2015-W01", Date.UTC(2014, 11, 29) ], // Thu
+ [ "2010-W01", Date.UTC(2010, 0, 4) ], // Fri
+ [ "2011-W01", Date.UTC(2011, 0, 3) ], // Sat
+ [ "2017-W01", Date.UTC(2017, 0, 2) ], // Sun
+ // Common years ending on different days of week.
+ [ "2007-W52", Date.UTC(2007, 11, 24) ], // Mon
+ [ "2013-W52", Date.UTC(2013, 11, 23) ], // Tue
+ [ "2014-W52", Date.UTC(2014, 11, 22) ], // Wed
+ [ "2015-W53", Date.UTC(2015, 11, 28) ], // Thu
+ [ "2010-W52", Date.UTC(2010, 11, 27) ], // Fri
+ [ "2011-W52", Date.UTC(2011, 11, 26) ], // Sat
+ [ "2017-W52", Date.UTC(2017, 11, 25) ], // Sun
+ // Leap years starting on different days of week.
+ [ "1996-W01", Date.UTC(1996, 0, 1) ], // Mon
+ [ "2008-W01", Date.UTC(2007, 11, 31) ], // Tue
+ [ "2020-W01", Date.UTC(2019, 11, 30) ], // Wed
+ [ "2004-W01", Date.UTC(2003, 11, 29) ], // Thu
+ [ "2016-W01", Date.UTC(2016, 0, 4) ], // Fri
+ [ "2000-W01", Date.UTC(2000, 0, 3) ], // Sat
+ [ "2012-W01", Date.UTC(2012, 0, 2) ], // Sun
+ // Leap years ending on different days of week.
+ [ "2012-W52", Date.UTC(2012, 11, 24) ], // Mon
+ [ "2024-W52", Date.UTC(2024, 11, 23) ], // Tue
+ [ "1980-W52", Date.UTC(1980, 11, 22) ], // Wed
+ [ "1992-W53", Date.UTC(1992, 11, 28) ], // Thu
+ [ "2004-W53", Date.UTC(2004, 11, 27) ], // Fri
+ [ "1988-W52", Date.UTC(1988, 11, 26) ], // Sat
+ [ "2000-W52", Date.UTC(2000, 11, 25) ], // Sun
+ // Other normal cases.
+ [ "2015-W53", Date.UTC(2015, 11, 28) ],
+ [ "2016-W36", Date.UTC(2016, 8, 5) ],
+ [ "1970-W01", Date.UTC(1969, 11, 29) ],
+ [ "275760-W37", Date.UTC(275760, 8, 8) ],
+ ];
+
+ var invalidData =
+ [
+ "invalidweek",
+ "0000-W01",
+ "2016-W00",
+ "2016-W53",
+ // Out of range.
+ "275760-W38",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "week";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "integer value representing this week");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid week");
+ }
+}
+
+function checkWeekSet()
+{
+ var testData =
+ [
+ // Common years starting on different days of week.
+ [ Date.UTC(2007, 0, 1), "2007-W01" ], // Mon
+ [ Date.UTC(2013, 0, 1), "2013-W01" ], // Tue
+ [ Date.UTC(2014, 0, 1), "2014-W01" ], // Wed
+ [ Date.UTC(2015, 0, 1), "2015-W01" ], // Thu
+ [ Date.UTC(2010, 0, 1), "2009-W53" ], // Fri
+ [ Date.UTC(2011, 0, 1), "2010-W52" ], // Sat
+ [ Date.UTC(2017, 0, 1), "2016-W52" ], // Sun
+ // Common years ending on different days of week.
+ [ Date.UTC(2007, 11, 31), "2008-W01" ], // Mon
+ [ Date.UTC(2013, 11, 31), "2014-W01" ], // Tue
+ [ Date.UTC(2014, 11, 31), "2015-W01" ], // Wed
+ [ Date.UTC(2015, 11, 31), "2015-W53" ], // Thu
+ [ Date.UTC(2010, 11, 31), "2010-W52" ], // Fri
+ [ Date.UTC(2011, 11, 31), "2011-W52" ], // Sat
+ [ Date.UTC(2017, 11, 31), "2017-W52" ], // Sun
+ // Leap years starting on different days of week.
+ [ Date.UTC(1996, 0, 1), "1996-W01" ], // Mon
+ [ Date.UTC(2008, 0, 1), "2008-W01" ], // Tue
+ [ Date.UTC(2020, 0, 1), "2020-W01" ], // Wed
+ [ Date.UTC(2004, 0, 1), "2004-W01" ], // Thu
+ [ Date.UTC(2016, 0, 1), "2015-W53" ], // Fri
+ [ Date.UTC(2000, 0, 1), "1999-W52" ], // Sat
+ [ Date.UTC(2012, 0, 1), "2011-W52" ], // Sun
+ // Leap years ending on different days of week.
+ [ Date.UTC(2012, 11, 31), "2013-W01" ], // Mon
+ [ Date.UTC(2024, 11, 31), "2025-W01" ], // Tue
+ [ Date.UTC(1980, 11, 31), "1981-W01" ], // Wed
+ [ Date.UTC(1992, 11, 31), "1992-W53" ], // Thu
+ [ Date.UTC(2004, 11, 31), "2004-W53" ], // Fri
+ [ Date.UTC(1988, 11, 31), "1988-W52" ], // Sat
+ [ Date.UTC(2000, 11, 31), "2000-W52" ], // Sun
+ // Other normal cases.
+ [ Date.UTC(2008, 8, 26), "2008-W39" ],
+ [ Date.UTC(2016, 0, 4), "2016-W01" ],
+ [ Date.UTC(2016, 0, 10), "2016-W01" ],
+ [ Date.UTC(2016, 0, 11), "2016-W02" ],
+ // Maximum valid week (limited by the ecma date object range).
+ [ 8640000000000000, "275760-W37" ],
+ // Minimum valid week (limited by the input element minimum valid value)
+ [ -62135596800000, "0001-W01" ],
+ // "Values must be truncated to valid weeks"
+ [ 0.3, "1970-W01" ],
+ [ 1e-1, "1970-W01" ],
+ [ -1.1, "1970-W01" ],
+ [ -345600000, "1969-W52" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-W01" ],
+ [ true, "1970-W01" ],
+ [ false, "1970-W01" ],
+ [ null, "1970-W01" ],
+ // Those are converted to NaN, the corresponding week string is the empty string
+ [ "invalidweek", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value).
+ [ Date.UTC(2016, 8, 8), "2016-W36" ],
+ [ Infinity, "2016-W36", true ],
+ [ -Infinity, "2016-W36", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "week";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " +
+ data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkDatetimeLocalGet() {
+ var validData =
+ [
+ // Simple cases.
+ [ "2016-12-20T09:58", Date.UTC(2016, 11, 20, 9, 58) ],
+ [ "2016-12-20T09:58:30", Date.UTC(2016, 11, 20, 9, 58, 30) ],
+ [ "2016-12-20T09:58:30.123", Date.UTC(2016, 11, 20, 9, 58, 30, 123) ],
+ [ "2017-01-01T10:00", Date.UTC(2017, 0, 1, 10, 0, 0) ],
+ [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ],
+ [ "1970-01-01T00:00", 0 ],
+ // Leap years.
+ [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ],
+ [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ],
+ [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ],
+ [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ],
+ [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ],
+ // Maximum valid datetime-local (limited by the ecma date object range).
+ [ "275760-09-13T00:00", 8640000000000000 ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ "0001-01-01T00:00", -62135596800000 ],
+ ];
+
+ var invalidData =
+ [
+ "invaliddatetime-local",
+ "0000-01-01T00:00",
+ "2016-12-25T00:00Z",
+ "2015-02-29T12:34",
+ "1-1-1T12:00",
+ // Out of range.
+ "275760-09-13T12:00",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "datetime-local";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "integer value representing this datetime-local");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid datetime-local");
+ }
+}
+
+function checkDatetimeLocalSet()
+{
+ var testData =
+ [
+ // Simple cases.
+ [ Date.UTC(2016, 11, 20, 9, 58, 0), "2016-12-20T09:58", ],
+ [ Date.UTC(2016, 11, 20, 9, 58, 30), "2016-12-20T09:58:30" ],
+ [ Date.UTC(2016, 11, 20, 9, 58, 30, 123), "2016-12-20T09:58:30.123" ],
+ [ Date.UTC(2017, 0, 1, 10, 0, 0), "2017-01-01T10:00" ],
+ [ Date.UTC(1969, 11, 31, 12, 0, 0), "1969-12-31T12:00" ],
+ [ 0, "1970-01-01T00:00" ],
+ // Maximum valid week (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13T00:00" ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ -62135596800000, "0001-01-01T00:00" ],
+ // Leap years.
+ [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ],
+ [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ],
+ [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ],
+ [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ],
+ [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ],
+ // "Values must be truncated to valid datetime-local"
+ [ 0.3, "1970-01-01T00:00" ],
+ [ 1e-1, "1970-01-01T00:00" ],
+ [ -1 , "1969-12-31T23:59:59.999" ],
+ [ -345600000, "1969-12-28T00:00" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-01-01T00:00" ],
+ [ true, "1970-01-01T00:00:00.001" ],
+ [ false, "1970-01-01T00:00" ],
+ [ null, "1970-01-01T00:00" ],
+ // Those are converted to NaN, the corresponding week string is the empty string
+ [ "invaliddatetime-local", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value).
+ [ Date.UTC(2016, 11, 27, 15, 10, 0), "2016-12-27T15:10" ],
+ [ Infinity, "2016-12-27T15:10", true ],
+ [ -Infinity, "2016-12-27T15:10", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "datetime-local";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " +
+ data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+checkAvailability();
+
+// <input type='number'> test
+checkNumberGet();
+checkNumberSet();
+
+// <input type='range'> test
+checkRangeGet();
+checkRangeSet();
+
+// <input type='date'> test
+checkDateGet();
+checkDateSet();
+
+// <input type='time'> test
+checkTimeGet();
+checkTimeSet();
+
+// <input type='month'> test
+checkMonthGet();
+checkMonthSet();
+
+// <input type='week'> test
+checkWeekGet();
+checkWeekSet();
+
+// <input type='datetime-local'> test
+checkDatetimeLocalGet();
+checkDatetimeLocalSet();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/without_selectionchange/mochitest.toml b/dom/html/test/forms/without_selectionchange/mochitest.toml
new file mode 100644
index 0000000000..8f019d8d80
--- /dev/null
+++ b/dom/html/test/forms/without_selectionchange/mochitest.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+prefs = ["dom.select_events.textcontrols.enabled=false"]
+
+["test_select.html"]
+
diff --git a/dom/html/test/forms/without_selectionchange/test_select.html b/dom/html/test/forms/without_selectionchange/test_select.html
new file mode 100644
index 0000000000..3d11611b1b
--- /dev/null
+++ b/dom/html/test/forms/without_selectionchange/test_select.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<title>Test for Bug 1717435</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+
+<textarea id="textarea">foo</textarea>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ textarea.addEventListener("select", ev => {
+ ok(true, "A select event must fire regardless of dom.select_events.textcontrols.enabled");
+ SimpleTest.finish();
+ });
+
+ textarea.focus();
+ textarea.select();
+ is(textarea.selectionStart, 0, "selectionStart")
+ is(textarea.selectionEnd, 3, "selectionEnd")
+</script>