diff options
Diffstat (limited to 'dom/html/test/forms')
117 files changed, 19793 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.ini b/dom/html/test/forms/chrome.ini new file mode 100644 index 0000000000..0c8fa82314 --- /dev/null +++ b/dom/html/test/forms/chrome.ini @@ -0,0 +1,5 @@ +[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.ini b/dom/html/test/forms/mochitest.ini new file mode 100644 index 0000000000..ab67dc6091 --- /dev/null +++ b/dom/html/test/forms/mochitest.ini @@ -0,0 +1,121 @@ +[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_autocomplete.html] +[test_bug1039548.html] +[test_bug1283915.html] +[test_bug1286509.html] +[test_button_attributes_reflection.html] +[test_input_radio_indeterminate.html] +[test_input_radio_radiogroup.html] +[test_input_radio_required.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_input_change_events.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_readonly.html] +[test_input_datetime_calendar_button.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_l10n.html] +[test_input_number_key_events.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_rounding.html] +[test_input_number_validation.html] +[test_input_number_focus.html] +[test_input_number_placeholder_shown.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_minlength_attribute.html] +[test_meter_element.html] +[test_meter_pseudo-classes.html] +[test_min_attribute.html] +[test_MozEditableElement_setUserInput.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_required_attribute.html] +[test_restore_form_elements.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_valueasdate_attribute.html] +[test_valueasnumber_attribute.html] +[test_validation_not_in_doc.html] +[test_reportValidation_preventDefault.html] +[test_input_password_show_password_button.html] +[test_input_password_click_show_password_button.html] 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..de92254386 --- /dev/null +++ b/dom/html/test/forms/test_autocomplete.html @@ -0,0 +1,137 @@ +<!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", ""], + + // 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"], + + // 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"], + + // 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"], + + // Five tokens (invalid) + ["billing billing billing mobile tel", ""], + ["section-blue section-blue billing mobile tel", ""], +]; + +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..5f8673d9ba --- /dev/null +++ b/dom/html/test/forms/test_autocompleteinfo.html @@ -0,0 +1,177 @@ +<!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", {}, ""], + + // 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"], + + // 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"], + + // 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"], + + // Five tokens (invalid) + ["billing billing billing mobile tel", {}, ""], + ["section-blue section-blue billing mobile tel", {}, ""], +]; + +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.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..9bc72ada72 --- /dev/null +++ b/dom/html/test/forms/test_button_attributes_reflection.html @@ -0,0 +1,161 @@ +<!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() { + await SpecialPowers.pushPrefEnv({ set: [["dom.dialog_element.enabled", false]] }) + reflectLimitedEnumerated({ + element: document.createElement("button"), + attribute: "formMethod", + validValues: [ "get", "post" ], + invalidValues: [ "put", "", "tulip" ], + unsupportedValues: [ "dialog" ], + defaultValue: { + invalid: "get", + missing: "", + } + }); +}); + +// .formMethod when dialog is enabled +add_task(async function() { + await SpecialPowers.pushPrefEnv({ set: [["dom.dialog_element.enabled", true]] }) + 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..88aeb7f16f --- /dev/null +++ b/dom/html/test/forms/test_input_attributes_reflection.html @@ -0,0 +1,269 @@ +<!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 +todo("dirName" in document.createElement("input"), + "dirName isn't implemented yet"); + +// .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 <input type='date'> 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..28004d3ce3 --- /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 the maximum year, which is + // 275760. + keys: ["0101275769"], + initialVal: "", + expectedVal: "275760-01-01" + }, + { + // Type 000000 in the year field will set it to the minimum year, which is + // 0001. + keys: ["0101000000"], + 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..d7bbe28dd8 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_calendar_button.html @@ -0,0 +1,177 @@ +<!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> +<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( + get_calendar_button(id).hidden, + `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( + !get_calendar_button(id).hidden, + `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(get_calendar_button(id).hidden, `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( + !get_calendar_button(id).hidden, + `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 calendarButton = get_calendar_button(id); + is(calendarButton.hidden, false, `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 calendarButton = get_calendar_button(id); + is(calendarButton.hidden, true, `Calendar button is hidden on #${id}`); +} + +function get_calendar_button(id) { + const input = document.getElementById(id); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + return shadowRoot.getElementById("calendar-button"); +} + +</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..b4b7ce2c0b --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_disabled_focus.html @@ -0,0 +1,42 @@ +<!DOCTYPE html>
+<title>Test for bug 1772841</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>
+<div id="content">
+ <input type="date" id="date" disabled>
+ <input type="time" id="time" disabled>
+ <input type="datetime-local" id="datetime-local" disabled>
+
+ <input type="date" id="date1">
+ <input type="time" id="time1">
+ <input type="datetime-local" id="datetime-local1">
+</div>
+<script>
+ /*
+ * Test for bug 1772841
+ * This test checks that when a datetime input element is disabled,
+ * it should not be focusable by click.
+ **/
+
+ add_task(async function() {
+ await SimpleTest.promiseFocus(window);
+ for (let inputType of ["time", "date", "datetime-local"]) {
+ testFocusState(inputType);
+ testDynamicChange(inputType);
+ }
+ })
+ function testFocusState(inputType) {
+ let input = document.getElementById(inputType);
+ input.click();
+ isnot(document.activeElement, input, "disabled element should not be focusable by click");
+
+ synthesizeMouseAtCenter(input, {});
+ isnot(document.activeElement, input, "disabled element should not be focusable by click");
+ }
+ function testDynamicChange(inputType) {
+ document.getElementById(inputType + "1").disabled = true;
+ testFocusState(inputType + "1");
+ }
+</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 <input type='email'> 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..f59c9e759c --- /dev/null +++ b/dom/html/test/forms/test_input_file_picker.html @@ -0,0 +1,280 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for <input type='file'> 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" + +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 <input type='number'></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..6d14eb2263 --- /dev/null +++ b/dom/html/test/forms/test_input_number_mouse_events.html @@ -0,0 +1,259 @@ +<!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> + +/** + * 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(); +}); + +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", "osKey"]; + 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", "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 <input type='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"> +</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..fd5cfe6935 --- /dev/null +++ b/dom/html/test/forms/test_input_password_click_show_password_button.html @@ -0,0 +1,95 @@ +<!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"); + + 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..0bb5fdbee6 --- /dev/null +++ b/dom/html/test/forms/test_input_password_show_password_button.html @@ -0,0 +1,80 @@ +<!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"); + + element.value = element.value; + let tmpSnapshot = await snapshotWindow(window); + + // Re-setting value shouldn't have changed anything. + 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..92cd1b3095 --- /dev/null +++ b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html @@ -0,0 +1,227 @@ +<!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"> + +/** + * 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 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", "osKey"]; + 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", "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..8c3855960b --- /dev/null +++ b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html @@ -0,0 +1,123 @@ +<!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"); + + element.value = element.value; + var tmpSnapshot = await snapshotWindow(window); + + results = compareSnapshots(baseSnapshot, tmpSnapshot, false); + ok(results[0], "re-settig the value should change nothing"); + + results = compareSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, true); + ok(results[0], "re-settig 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 <input type='url'> 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..74b0517c51 --- /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..4fca31f9af --- /dev/null +++ b/dom/html/test/forms/test_required_attribute.html @@ -0,0 +1,420 @@ +<!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"); + ok(!element.matches(':-moz-ui-valid'), ":-moz-ui-valid should not apply"); + ok(!element.matches(':-moz-ui-invalid'), ":-moz-ui-invalid should not apply"); + } else { + ok(element.matches(':valid'), ":valid should apply"); + ok(!element.matches(':invalid'), ":invalid should not apply"); + ok(element.matches(':-moz-ui-valid'), ":-moz-ui-valid should apply"); + ok(!element.matches(':-moz-ui-invalid'), ":-moz-ui-invalid should not apply"); + } +} + +function checkSufferingFromBeingMissing(element, hasMozUIInvalid) +{ + 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"); + ok(!element.matches(':-moz-ui-valid'), ":-moz-ui-valid should not apply"); + is(element.matches(':-moz-ui-invalid'), hasMozUIInvalid, ":-moz-ui-invalid expected state is " + hasMozUIInvalid); +} + +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, true); + + element.readOnly = true; + checkNotSufferingFromBeingMissing(element, true); + + element.readOnly = false; + checkSufferingFromBeingMissing(element, true); + + SpecialPowers.wrap(element).value = 'foo'; + checkNotSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ''; + checkSufferingFromBeingMissing(element, true); + + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.focus(); + element.required = true; + SpecialPowers.wrap(element).value = 'foobar'; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + SpecialPowers.wrap(element).value = ''; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element, true); + + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + // 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, true); + + element.readOnly = true; + checkNotSufferingFromBeingMissing(element, true); + + element.readOnly = false; + checkSufferingFromBeingMissing(element, true); + + 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'; + } 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, true); + + element.focus(); + element.required = true; + SpecialPowers.wrap(element).value = 'foobar'; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + SpecialPowers.wrap(element).value = ''; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element, true); + + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + element.required = true; + SpecialPowers.wrap(element).value = ''; // To make :-moz-ui-valid apply. + checkSufferingFromBeingMissing(element, true); + document.forms[0].removeChild(element); + // Removing the child changes nothing about whether it's valid + checkSufferingFromBeingMissing(element, true); +} + +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, true); + + element.checked = true; + checkNotSufferingFromBeingMissing(element); + + element.checked = false; + checkSufferingFromBeingMissing(element, true); + + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.focus(); + element.required = true; + element.checked = true; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + element.required = true; + element.checked = false; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element, true); + + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + element.required = true; + element.checked = false; + document.forms[0].removeChild(element); + checkSufferingFromBeingMissing(element, true); +} + +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, true); + + element.checked = true; + checkNotSufferingFromBeingMissing(element); + + element.checked = false; + checkSufferingFromBeingMissing(element, true); + + // 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, true); + + // 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, true); + checkSufferingFromBeingMissing(element2, true); + + 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, true); + + element2.focus(); + element2.required = true; + element2.checked = true; + element2.blur(); + element2.form.reset(); + checkSufferingFromBeingMissing(element2, false); + + element2.required = true; + element2.checked = false; + element2.form.reportValidity(); + checkSufferingFromBeingMissing(element2, true); + + element2.form.reset(); + checkSufferingFromBeingMissing(element2, false); + + element2.required = true; + element2.checked = false; + document.forms[0].removeChild(element2); + checkSufferingFromBeingMissing(element2, true); +} + +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, true); + + SpecialPowers.wrap(element).mozSetFileArray([file]); + checkNotSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ""; + checkSufferingFromBeingMissing(element, true); + + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.focus(); + SpecialPowers.wrap(element).mozSetFileArray([file]); + element.required = true; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + element.required = true; + SpecialPowers.wrap(element).value = ''; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element, true); + + element.form.reset(); + checkSufferingFromBeingMissing(element, false); + + element.required = true; + SpecialPowers.wrap(element).value = ''; + document.forms[0].removeChild(element); + checkSufferingFromBeingMissing(element, true); +} + +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_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..4fcadc37ce --- /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.import( + "resource://gre/modules/FileUtils.jsm" + ); + + var i = document.getElementsByTagName('input')[0]; + + var file = FileUtils.getDir("TmpD", [], false); + 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..8f8e810b0e --- /dev/null +++ b/dom/html/test/forms/test_textarea_attributes_reflection.html @@ -0,0 +1,104 @@ +<!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, +}); + +todo("dirName" in document.createElement("textarea"), + "dirName isn't implemented yet"); + +// .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.ini b/dom/html/test/forms/without_selectionchange/mochitest.ini new file mode 100644 index 0000000000..8224e29b34 --- /dev/null +++ b/dom/html/test/forms/without_selectionchange/mochitest.ini @@ -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> |