Bug 1539172 - Make nsContentUtils::DispatchInputEvent() notify content of valid state change of input element r=smaug a=pascalc
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 28 Mar 2019 23:40:18 +0000
changeset 525961 985158cfb6950e416e0d4664e1aa9aad83c923ab
parent 525960 3dc7b1a572313386a459b174e7335847e3c1dbb2
child 525962 21acdae6bafe5ad41736fec5e14bb787622acb5d
push id2032
push userffxbld-merge
push dateMon, 13 May 2019 09:36:57 +0000
treeherdermozilla-release@455c1065dcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, pascalc
bugs1539172
milestone67.0
Bug 1539172 - Make nsContentUtils::DispatchInputEvent() notify content of valid state change of input element r=smaug a=pascalc When `nsContentUtils::DispatchInputEvent()` dispatches `input` event, the editor's value was changed by somebody. In this case, it needs to update the valid state **and** notify to update the style. (Note that I'm not sure whether this is right approach.) Differential Revision: https://phabricator.services.mozilla.com/D25029
dom/base/nsContentUtils.cpp
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/html/test/forms/test_MozEditableElement_setUserInput.html
toolkit/components/satchel/test/mochitest.ini
toolkit/components/satchel/test/test_input_valid_state_with_autocomplete.html
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -4144,17 +4144,19 @@ nsresult nsContentUtils::DispatchInputEv
 #endif  // #ifdef DEBUG
 
   // If the event target is an <input> element, we need to update
   // validationMessage value before dispatching "input" event because
   // "input" event listener may need to check it.
   HTMLInputElement* inputElement =
       HTMLInputElement::FromNode(aEventTargetElement);
   if (inputElement) {
-    inputElement->MaybeUpdateAllValidityStates();
+    MOZ_KnownLive(inputElement)->MaybeUpdateAllValidityStates(true);
+    // XXX Should we stop dispatching "input" event if the target is removed
+    //     from the DOM tree?
   }
 
   if (!useInputEvent) {
     MOZ_ASSERT(aEditorInputType == EditorInputType::eUnknown);
     // Dispatch "input" event with Event instance.
     WidgetEvent widgetEvent(true, eUnidentifiedEvent);
     widgetEvent.mSpecifiedEventType = nsGkAtoms::oninput;
     widgetEvent.mFlags.mCancelable = false;
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -2609,17 +2609,17 @@ nsresult HTMLInputElement::SetValueInter
         }
         // If the caller won't dispatch "input" event via
         // nsContentUtils::DispatchInputEvent(), we need to modify
         // validationMessage value here.
         //
         // FIXME(emilio): eSetValue_Internal is not supposed to change state,
         // but maybe we could run this too?
         if (aFlags & nsTextEditorState::eSetValue_ByContent) {
-          MaybeUpdateAllValidityStates();
+          MaybeUpdateAllValidityStates(!mDoneCreating);
         }
       } else {
         free(mInputData.mValue);
         mInputData.mValue = ToNewUnicode(value);
         if (setValueChanged) {
           SetValueChanged(true);
         }
         if (mType == NS_FORM_INPUT_NUMBER) {
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -340,21 +340,22 @@ class HTMLInputElement final : public ns
   void UpdateRangeOverflowValidityState();
   void UpdateRangeUnderflowValidityState();
   void UpdateStepMismatchValidityState();
   void UpdateBadInputValidityState();
   // Update all our validity states and then update our element state
   // as needed.  aNotify controls whether the element state update
   // needs to notify.
   void UpdateAllValidityStates(bool aNotify);
-  void MaybeUpdateAllValidityStates() {
+  MOZ_CAN_RUN_SCRIPT
+  void MaybeUpdateAllValidityStates(bool aNotify) {
     // If you need to add new type which supports validationMessage, you should
     // add test cases into test_MozEditableElement_setUserInput.html.
     if (mType == NS_FORM_INPUT_EMAIL) {
-      UpdateAllValidityStates(!mDoneCreating);
+      UpdateAllValidityStates(aNotify);
     }
   }
 
   // Update all our validity states without updating element state.
   // This should be called instead of UpdateAllValidityStates any time
   // we're guaranteed that element state will be updated anyway.
   void UpdateAllValidityStatesButNotElementState();
   void UpdateBarredFromConstraintValidation();
--- a/dom/html/test/forms/test_MozEditableElement_setUserInput.html
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -265,54 +265,66 @@ SimpleTest.waitForFocus(() => {
     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");
 
   SimpleTest.finish();
 });
 </script>
--- a/toolkit/components/satchel/test/mochitest.ini
+++ b/toolkit/components/satchel/test/mochitest.ini
@@ -14,13 +14,14 @@ skip-if = os == 'linux' # bug 1022386
 [test_datalist_shadow_dom.html]
 [test_form_autocomplete.html]
 skip-if = (verify && debug && (os == 'win')) || os == 'linux' # linux - bug 1022386
 [test_form_autocomplete_with_list.html]
 skip-if = os == 'linux' # bug 1022386
 [test_form_submission.html]
 [test_form_submission_cap.html]
 [test_form_submission_cap2.html]
+[test_input_valid_state_with_autocomplete.html]
 [test_password_autocomplete.html]
 scheme = https
 [test_popup_direction.html]
 [test_popup_enter_event.html]
 [test_submit_on_keydown_enter.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/test/test_input_valid_state_with_autocomplete.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for valid state with autocomplete</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="satchel_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <style>
+    input:invalid {
+      border: red 1px solid;
+    }
+  </style>
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content">
+  <form id="form1">
+    <input  type="email" name="field1">
+    <button type="submit">Submit</button>
+  </form>
+</div>
+
+<pre id="test">
+<script class="testbody">
+var input = document.querySelector("input[name=field1]");
+
+async function runTest() {
+  let resolveFunc = null;
+  function onPopup() {
+    if (resolveFunc) {
+      resolveFunc();
+      resolveFunc = null;
+    }
+  }
+  registerPopupShownListener(onPopup);
+  function promisePopup() {
+    return new Promise(resolve => {
+      resolveFunc = resolve;
+    });
+  }
+
+  let waitForPopup = promisePopup();
+  input.focus();
+  input.addEventListener("input", (event) => {
+    is(event.inputType, "insertText",
+       "inputType of \"input\" event should be \"insertText\"");
+    ok(!input.validity.valid,
+       "Should be invalid immediately after inserting a character");
+    ok(input.matches(":invalid"),
+       "Should match \":invalid\" immediately after inserting a character");
+  }, {once: true});
+  synthesizeKey("e");
+
+  await waitForPopup;
+  synthesizeKey("KEY_ArrowDown");
+  input.addEventListener("input", (event) => {
+    is(event.inputType, "insertReplacementText",
+       "inputType of \"input\" event should be \"insertReplacementText\"");
+    ok(input.validity.valid,
+       "Should be valid immediately after selecting valid item in autocomplete list");
+    ok(!input.matches(":invalid"),
+       "Shouldn't match \":invalid\" immediately after selecting valid item in autocomplete list");
+  }, {once: true});
+  synthesizeKey("KEY_Enter"); // Select valid item
+
+  waitForPopup = promisePopup();
+  synthesizeKey("KEY_Backspace");
+  await waitForPopup;
+  synthesizeKey("KEY_ArrowDown");
+  synthesizeKey("KEY_ArrowDown");
+  input.addEventListener("input", (event) => {
+    is(event.inputType, "insertReplacementText",
+       "inputType of \"input\" event should be \"insertReplacementText\"");
+    ok(!input.validity.valid,
+       "Should be invalid immediately after selecting invalid item in autocomplete list");
+    ok(input.matches(":invalid"),
+       "Should match \":invalid\" immediately after selecting invalid item in autocomplete list");
+  }, {once: true});
+  synthesizeKey("KEY_Enter"); // Select invalid item
+
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+  updateFormHistory([
+    { op: "remove" },
+    { op: "add", fieldname: "field1", value: "email@example.com" },
+    { op: "add", fieldname: "field1", value: "email@example.com." },
+  ], runTest);
+});
+</script>
+</pre>
+</body>