Bug 1504911 - part 4: Make all script for web content dispatch "input" event with proper event interface r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Tue, 20 Nov 2018 14:35:38 +0000
changeset 503894 09fd7845a50bad9fa7a579a9e7088828d8155a20
parent 503893 7f09364736a089ada819bd73b5e7e28a4f048b3b
child 503895 824bcd08c85e70d98109b04715b26f139e099ce8
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1504911
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1504911 - part 4: Make all script for web content dispatch "input" event with proper event interface r=smaug Currently, some "input" event dispatchers in our script dispatch "input" event with UIEvent. This is completely wrong. For conforming to HTML spec, Event is proper event. Additionally, for conforming to Input Events, InputEvent is proper event only on <textarea> or <input> element which has a single line editor. For making us to maintain easier, this patch adds new API, "isInputEventTarget" to MozEditableElement which returns true when "input" event dispatcher should use InputEvent for the input element. Finally, this makes some dispatchers use setUserInput() instead of setting value and dispatching event by themselves. This also makes us to maintain them easier. Note that this does not touch "input" event dispatchers which dispatch events only for chrome (such as URL bar, some pages in about: scheme) for making this change safer as far as possible. Differential Revision: https://phabricator.services.mozilla.com/D12247
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
dom/base/nsContentUtils.cpp
dom/html/HTMLInputElement.h
dom/html/HTMLTextAreaElement.h
dom/html/test/forms/test_MozEditableElement_setUserInput.html
dom/webidl/HTMLInputElement.webidl
mobile/android/components/geckoview/GeckoViewPrompt.js
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/modules/geckoview/GeckoViewAutoFill.jsm
toolkit/actors/SelectChild.jsm
toolkit/content/tests/chrome/file_editor_with_autocomplete.js
toolkit/content/tests/chrome/test_autocomplete4.xul
toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul
toolkit/content/widgets/autocomplete.xml
toolkit/content/widgets/textbox.xml
toolkit/modules/sessionstore/FormData.jsm
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -292,17 +292,17 @@ class FormAutofillSection {
         let option = cache[value] && cache[value].get();
         if (!option) {
           continue;
         }
         // Do not change value or dispatch events if the option is already selected.
         // Use case for multiple select is not considered here.
         if (!option.selected) {
           option.selected = true;
-          element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
+          element.dispatchEvent(new element.ownerGlobal.Event("input", {bubbles: true}));
           element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
         }
         // Autofill highlight appears regardless if value is changed or not
         this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
       }
     }
   }
 
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -113,18 +113,18 @@ function triggerAutofillAndCheckProfile(
     const element = document.getElementById(fieldName);
     const expectingEvent = document.activeElement == element ? "DOMAutoComplete" : "change";
     const checkFieldAutofilled = Promise.all([
       new Promise(resolve => element.addEventListener("input", (event) => {
         if (element.tagName == "INPUT" && element.type == "text") {
           ok(event instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface on ${element.tagName}`);
         } else {
-          todo(event instanceof Event && !(event instanceof UIEvent),
-               `"input" event should be dispatched with Event interface on ${element.tagName}`);
+          ok(event instanceof Event && !(event instanceof UIEvent),
+             `"input" event should be dispatched with Event interface on ${element.tagName}`);
         }
         is(event.cancelable, false,
            `"input" event should be never cancelable on ${element.tagName}`);
         is(event.bubbles, true,
            `"input" event should always bubble on ${element.tagName}`);
         resolve();
       }, {once: true})),
       new Promise(resolve => element.addEventListener(expectingEvent, resolve, {once: true})),
--- a/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
@@ -48,18 +48,18 @@ function checkElementFilled(element, exp
   return [
     new Promise(resolve => {
       element.addEventListener("input", function onInput(event) {
         ok(true, "Checking " + element.name + " field fires input event");
         if (element.tagName == "INPUT" && element.type == "text") {
           ok(event instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface on ${element.name}`);
         } else {
-          todo(event instanceof Event && !(event instanceof UIEvent),
-               `"input" event should be dispatched with Event interface on ${element.name}`);
+          ok(event instanceof Event && !(event instanceof UIEvent),
+             `"input" event should be dispatched with Event interface on ${element.name}`);
         }
         is(event.cancelable, false,
            `"input" event should be never cancelable on ${element.name}`);
         is(event.bubbles, true,
            `"input" event should always bubble on ${element.name}`);
         resolve();
       }, {once: true});
     }),
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -4479,17 +4479,17 @@ nsContentUtils::DispatchInputEvent(Eleme
   if (aTextEditor) {
     useInputEvent = true;
   } else if (HTMLTextAreaElement* textAreaElement=
                HTMLTextAreaElement::FromNode(aEventTargetElement)) {
     aTextEditor = textAreaElement->GetTextEditorWithoutCreation();
     useInputEvent = true;
   } else if (HTMLInputElement* inputElement =
                HTMLInputElement::FromNode(aEventTargetElement)) {
-    if (inputElement->IsSingleLineTextControl()) {
+    if (inputElement->IsInputEventTarget()) {
       aTextEditor = inputElement->GetTextEditorWithoutCreation();
       useInputEvent = true;
     }
   }
 #ifdef DEBUG
   else {
     nsCOMPtr<nsITextControlElement> textControlElement =
       do_QueryInterface(aEventTargetElement);
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -929,16 +929,21 @@ public:
 
   bool MozIsTextField(bool aExcludePassword);
 
   /**
    * GetEditor() is for webidl bindings.
    */
   nsIEditor* GetEditor();
 
+  bool IsInputEventTarget() const
+  {
+    return IsSingleLineTextControl(false);
+  }
+
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   void SetUserInput(const nsAString& aInput,
                     nsIPrincipal& aSubjectPrincipal);
 
   /**
    * If aValue contains a valid floating-point number in the format specified
    * by the HTML 5 spec:
    *
--- a/dom/html/HTMLTextAreaElement.h
+++ b/dom/html/HTMLTextAreaElement.h
@@ -304,16 +304,21 @@ public:
   // XPCOM adapter function widely used throughout code, leaving it as is.
   nsresult GetControllers(nsIControllers** aResult);
 
   nsIEditor* GetEditor()
   {
     return mState.GetTextEditor();
   }
 
+  bool IsInputEventTarget() const
+  {
+    return true;
+  }
+
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   void SetUserInput(const nsAString& aValue,
                     nsIPrincipal& aSubjectPrincipal);
 
 protected:
   virtual ~HTMLTextAreaElement() {}
 
   // get rid of the compiler warning
--- a/dom/html/test/forms/test_MozEditableElement_setUserInput.html
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -22,94 +22,93 @@ SimpleTest.waitForFocus(() => {
    *   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.
    *     fireInputEvent: true if "input" event should be fired.  Otherwise, false.
-   *     useInputEvent: true if "input" event should be fired with InputEvent interface.  Otherwise, false.
    */
   for (let test of [{element: "input", type: "hidden",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: false}},
                     {element: "input", type: "text",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "search",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "tel",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "url",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "email",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "password",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", 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", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "3", after:"6", 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", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", 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", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", 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", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", 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", fireInputEvent: true, useInputEvent: true}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "number",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "range",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "3", after:"6", 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", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "#5c5c5c", after:"#ffffff", fireInputEvent: true}},
                     {element: "input", type: "checkbox",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: true}},
                     {element: "input", type: "radio",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "3", after:"6", 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:"", fireInputEvent: true, useInputEvent: false}},
+                     result: {before: "", after:"", fireInputEvent: true}},
                     {element: "input", type: "submit",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: false}},
                     {element: "input", type: "image",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: false}},
                     {element: "input", type: "reset",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: false}},
                     {element: "input", type: "button",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                     result: {before: "3", after:"6", fireInputEvent: false}},
                     {element: "textarea",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}}]) {
+                     result: {before: "3", after:"6", 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;
 
@@ -151,17 +150,17 @@ SimpleTest.waitForFocus(() => {
         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 > 0) {
-      if (test.result.useInputEvent) {
+      if (SpecialPowers.wrap(target).isInputEventTarget) {
         if (test.type === "number" || 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 {
           ok(inputEvents[0] instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
         }
       } else {
@@ -207,17 +206,17 @@ SimpleTest.waitForFocus(() => {
         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 > 0) {
-      if (test.result.useInputEvent) {
+      if (SpecialPowers.wrap(target).isInputEventTarget) {
         if (test.type === "number" || 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 {
           ok(inputEvents[0] instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
         }
       } else {
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -187,16 +187,22 @@ partial interface HTMLInputElement {
   AutocompleteInfo? getAutocompleteInfo();
 };
 
 [NoInterfaceObject]
 interface MozEditableElement {
   [Pure, ChromeOnly]
   readonly attribute nsIEditor? editor;
 
+  // This is set to true if "input" event should be fired with InputEvent on
+  // the element.  Otherwise, i.e., if "input" event should be fired with
+  // Event, set to false.
+  [Func="IsChromeOrXBLOrUAWidget"]
+  readonly attribute boolean isInputEventTarget;
+
   // This is similar to set .value on nsIDOMInput/TextAreaElements, but handling
   // of the value change is closer to the normal user input, so 'change' event
   // for example will be dispatched when focusing out the element.
   [Func="IsChromeOrXBLOrUAWidget", NeedsSubjectPrincipal]
   void setUserInput(DOMString input);
 };
 
 HTMLInputElement implements MozEditableElement;
--- a/mobile/android/components/geckoview/GeckoViewPrompt.js
+++ b/mobile/android/components/geckoview/GeckoViewPrompt.js
@@ -155,17 +155,18 @@ PromptFactory.prototype = {
         return;
       }
       aElement.value = result.datetime;
       this._dispatchEvents(aElement);
     });
   },
 
   _dispatchEvents: function(aElement) {
-    // Fire both "input" and "change" events for <select> and <input>.
+    // Fire both "input" and "change" events for <select> and <input> for
+    // date/time.
     aElement.dispatchEvent(new aElement.ownerGlobal.Event("input", { bubbles: true }));
     aElement.dispatchEvent(new aElement.ownerGlobal.Event("change", { bubbles: true }));
   },
 
   _handleContextMenu: function(aEvent) {
     let target = aEvent.composedTarget;
     if (aEvent.defaultPrevented || target.isContentEditable) {
       return;
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -610,18 +610,23 @@ class AccessibilityTest : BaseSessionTes
                         "#email1" to "bar", "#number1" to "", "#tel1" to "bar")
 
         // Set up promises to monitor the values changing.
         val promises = autoFills.flatMap { entry ->
             // Repeat each test with both the top document and the iframe document.
             arrayOf("document", "$('#iframe').contentDocument").map { doc ->
                 mainSession.evaluateJS("""new Promise(resolve =>
                     $doc.querySelector('${entry.key}').addEventListener(
-                        'input', event => resolve([event.target.value, '${entry.value}']),
-                        { once: true }))""").asJSPromise()
+                        'input', event => {
+                          let eventInterface =
+                            event instanceof InputEvent ? "InputEvent" :
+                            event instanceof UIEvent ? "UIEvent" :
+                            event instanceof Event ? "Event" : "Unknown";
+                          resolve([event.target.value, '${entry.value}', eventInterface]);
+                        }, { once: true }))""").asJSPromise()
             }
         }
 
         // Perform auto-fill and return number of auto-fills performed.
         fun autoFillChild(id: Int, child: AccessibilityNodeInfo) {
             // Seal the node info instance so we can perform actions on it.
             if (child.childCount > 0) {
                 for (i in 0 until child.childCount) {
@@ -660,18 +665,19 @@ class AccessibilityTest : BaseSessionTes
                 assertThat("Can perform auto-fill",
                            provider.performAction(id, ACTION_SET_TEXT, args), equalTo(true))
             }
         }
 
         autoFillChild(View.NO_ID, createNodeInfo(View.NO_ID))
 
         // Wait on the promises and check for correct values.
-        for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
+        for ((actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
             assertThat("Auto-filled value must match", actual, equalTo(expected))
+            assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
         }
     }
 
     @Setting(key = Setting.Key.FULL_ACCESSIBILITY_TREE, value = "true")
     @Test fun autoFill_navigation() {
         fun countAutoFillNodes(cond: (AccessibilityNodeInfo) -> Boolean =
                                        { it.className == "android.widget.EditText" },
                                id: Int = View.NO_ID): Int {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -264,18 +264,23 @@ class ContentDelegateTest : BaseSessionT
                         "#number1" to "", "#tel1" to "bar")
 
         // Set up promises to monitor the values changing.
         val promises = autoFills.flatMap { entry ->
             // Repeat each test with both the top document and the iframe document.
             arrayOf("document", "$('#iframe').contentDocument").map { doc ->
                 mainSession.evaluateJS("""new Promise(resolve =>
                 $doc.querySelector('${entry.key}').addEventListener(
-                    'input', event => resolve([event.target.value, '${entry.value}']),
-                    { once: true }))""").asJSPromise()
+                    'input', event => {
+                      let eventInterface =
+                        event instanceof InputEvent ? "InputEvent" :
+                        event instanceof UIEvent ? "UIEvent" :
+                        event instanceof Event ? "Event" : "Unknown";
+                      resolve([event.target.value, '${entry.value}', eventInterface]);
+                    }, { once: true }))""").asJSPromise()
             }
         }
 
         val rootNode = ViewNode.newInstance()
         val rootStructure = ViewNodeBuilder.newInstance(AssistStructure(), rootNode,
                 /* async */ false) as ViewStructure
         val autoFillValues = SparseArray<CharSequence>()
 
@@ -343,18 +348,19 @@ class ContentDelegateTest : BaseSessionT
             }
         }
 
         mainSession.textInput.onProvideAutofillVirtualStructure(rootStructure, 0)
         checkAutoFillChild(rootNode)
         mainSession.textInput.autofill(autoFillValues)
 
         // Wait on the promises and check for correct values.
-        for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
+        for ((actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
             assertThat("Auto-filled value must match", actual, equalTo(expected))
+            assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
         }
     }
 
     // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
     @SdkSuppress(minSdkVersion = 23)
     @WithDevToolsAPI
     @WithDisplay(width = 100, height = 100)
     @Test fun autoFill_navigation() {
--- a/mobile/android/modules/geckoview/GeckoViewAutoFill.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewAutoFill.jsm
@@ -122,24 +122,17 @@ class GeckoViewAutoFill {
         for (let id in responses) {
           const entry = this._autoFillElements &&
                         this._autoFillElements.get(+id);
           const element = entry && entry.get();
           const value = responses[id] || "";
 
           if (element instanceof window.HTMLInputElement &&
               !element.disabled && element.parentElement) {
-            element.value = value;
-
-            // Fire both "input" and "change" events.
-            element.dispatchEvent(new element.ownerGlobal.Event(
-                "input", { bubbles: true }));
-            element.dispatchEvent(new element.ownerGlobal.Event(
-                "change", { bubbles: true }));
-
+            element.setUserInput(value);
             if (winUtils && element.value === value) {
               // Add highlighting for autofilled fields.
               winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
 
               // Remove highlighting when the field is changed.
               element.addEventListener("input", _ =>
                   winUtils.removeManuallyManagedState(element, AUTOFILL_STATE),
                   { mozSystemGroup: true, once: true });
--- a/toolkit/actors/SelectChild.jsm
+++ b/toolkit/actors/SelectChild.jsm
@@ -276,17 +276,17 @@ this.SelectContentHelper.prototype = {
           }
 
           // Clear active document no matter user selects via keyboard or mouse
           InspectorUtils.removeContentState(this.element, kStateActive,
                                             /* aClearActiveDocument */ true);
 
           // Fire input and change events when selected option changes
           if (this.initialSelection !== selectedOption) {
-            let inputEvent = new win.UIEvent("input", {
+            let inputEvent = new win.Event("input", {
               bubbles: true,
             });
             this.element.dispatchEvent(inputEvent);
 
             let changeEvent = new win.Event("change", {
               bubbles: true,
             });
             this.element.dispatchEvent(changeEvent);
--- a/toolkit/content/tests/chrome/file_editor_with_autocomplete.js
+++ b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js
@@ -87,23 +87,18 @@ nsDoTestsForEditorWithAutoComplete.proto
     this._is(aInputEvents.length, aTest.inputEvents.length,
              this._description + ", " + aTest.description + ": number of input events wrong");
     for (let i = 0; i < aInputEvents.length; i++) {
       if (aTest.inputEvents[i] === undefined) {
         this._is(true, false,
                  this._description + ", " + aTest.description + ": \"input\" event shouldn't be dispatched anymore");
         return;
       }
-      if (aTest.inputEvents[i].todoInterfaceOnXUL && aInputEvents[i].target.tagName === "textbox") {
-        this._todo_is(aInputEvents[i] instanceof this._window.InputEvent, true,
-                      this._description + ", " + aTest.description + ': "input" event should be dispatched with InputEvent interface');
-      } else {
-        this._is(aInputEvents[i] instanceof this._window.InputEvent, true,
-                 this._description + ", " + aTest.description + ': "input" event should be dispatched with InputEvent interface');
-      }
+      this._is(aInputEvents[i] instanceof this._window.InputEvent, true,
+               this._description + ", " + aTest.description + ': "input" event should be dispatched with InputEvent interface');
       this._is(aInputEvents[i].cancelable, false,
                this._description + ", " + aTest.description + ': "input" event should be never cancelable');
       this._is(aInputEvents[i].bubbles, true,
                this._description + ", " + aTest.description + ': "input" event should always bubble');
     }
   },
 
   _tests: [
@@ -122,17 +117,17 @@ nsDoTestsForEditorWithAutoComplete.proto
     { description: "Undo/Redo behavior check when typed text exactly matches the case: select 'Mozilla' to complete the word",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("KEY_ArrowDown", {}, aWindow);
         synthesizeKey("KEY_Enter", {}, aWindow);
         return true;
       }, popup: false, value: "Mozilla", searchString: "Mozilla",
       inputEvents: [
-        {inputType: "insertReplacementText", todoInterfaceOnXUL: true},
+        {inputType: "insertReplacementText"},
       ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case: undo the word, but typed text shouldn't be canceled",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mo", searchString: "Mo",
@@ -197,17 +192,17 @@ nsDoTestsForEditorWithAutoComplete.proto
     { description: "Undo/Redo behavior check when typed text does not match the case: select 'Mozilla' to complete the word",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("KEY_ArrowDown", {}, aWindow);
         synthesizeKey("KEY_Enter", {}, aWindow);
         return true;
       }, popup: false, value: "Mozilla", searchString: "Mozilla",
       inputEvents: [
-        {inputType: "insertReplacementText", todoInterfaceOnXUL: true},
+        {inputType: "insertReplacementText"},
       ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case: undo the word, but typed text shouldn't be canceled",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: true, value: "mo", searchString: "mo",
--- a/toolkit/content/tests/chrome/test_autocomplete4.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete4.xul
@@ -86,23 +86,16 @@ var componentManager = Components.manage
 componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete",
                                  autoCompleteSimpleName, autoCompleteSimple);
 
 
 // Test Bug 325842 - completeDefaultIndex
 
 SimpleTest.waitForExplicitFinish();
 
-// 8 or 6 assertions are recorded due to nested setting <input>.value but not a problem.
-// They are necessary to detect invalid method call without event loop.
-if (IS_MAC)
-  SimpleTest.expectAssertions(6, 6);
-else
-  SimpleTest.expectAssertions(8, 8);
-
 setTimeout(nextTest, 0);
 
 var currentTest = null;
 
 // Note the entries for these tests (key) are incremental.
 const tests = [
   {
     desc: "HOME key remove selection",
--- a/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul
@@ -102,20 +102,16 @@ let autoCompleteSimple = {
   },
   stopSearch: function () {
     clearTimeout(this.pendingSearch);
   }
 };
 
 SimpleTest.waitForExplicitFinish();
 
-// 6 assertions are recorded due to nested setting <input>.value but not a problem.
-// They are necessary to detect invalid method call without mutation event listers.
-SimpleTest.expectAssertions(6, 6);
-
 let gACTimer;
 let gAutoComplete;
 let asyncTest;
 
 let searchCompleteTimeoutId = null;
 
 function finishTest() {
   // Unregister the factory so that we don't get in the way of other tests
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -187,25 +187,19 @@
           return this.value;
         ]]></getter>
         <setter><![CDATA[
           if (typeof this.onBeforeTextValueSet == "function" &&
               !this._textValueSetByCompleteDefault) {
             val = this.onBeforeTextValueSet(val);
           }
 
-          this.value = val;
-
-          // Completing a result should simulate the user typing the result, so
-          // fire an input event.
-          let evt = document.createEvent("UIEvents");
-          evt.initUIEvent("input", true, false, window, 0);
-          this.mIgnoreInput = true;
-          this.dispatchEvent(evt);
-          this.mIgnoreInput = false;
+          // "input" event is automatically dispatched by the editor if
+          // necessary.
+          this._setValueInternal(val, true);
 
           return this.value;
         ]]></setter>
       </property>
 
       <method name="selectTextRange">
         <parameter name="aStartIndex"/>
         <parameter name="aEndIndex"/>
@@ -277,47 +271,26 @@
             this.closePopup();
         ]]></setter>
       </property>
 
       <!-- =================== PUBLIC MEMBERS =================== -->
 
       <field name="valueIsTyped">false</field>
       <field name="_textValueSetByCompleteDefault">false</field>
-      <property name="value">
+      <property name="value"
+                onset="return this._setValueInternal(val, false);">
         <getter><![CDATA[
           if (typeof this.onBeforeValueGet == "function") {
             var result = this.onBeforeValueGet();
             if (result)
               return result.value;
           }
           return this.inputField.value;
         ]]></getter>
-        <setter><![CDATA[
-          this.mIgnoreInput = true;
-
-          if (typeof this.onBeforeValueSet == "function")
-            val = this.onBeforeValueSet(val);
-
-          if (typeof this.trimValue == "function" &&
-              !this._textValueSetByCompleteDefault)
-            val = this.trimValue(val);
-
-          this.valueIsTyped = false;
-          this.inputField.value = val;
-
-          if (typeof this.formatValue == "function")
-            this.formatValue();
-
-          this.mIgnoreInput = false;
-          var event = document.createEvent("Events");
-          event.initEvent("ValueChange", true, true);
-          this.inputField.dispatchEvent(event);
-          return val;
-        ]]></setter>
       </property>
 
       <property name="focused" readonly="true"
                 onget="return this.getAttribute('focused') == 'true';"/>
 
       <!-- maximum number of rows to display at a time -->
       <property name="maxRows"
                 onset="this.setAttribute('maxrows', val); return val;"
@@ -599,16 +572,47 @@
           isCommandEnabled(aCommand) {
             return this._autocomplete.editor.isSelectionEditable &&
                    this._autocomplete.editor.canPaste(this._kGlobalClipboard);
           },
           onEvent() {},
         })
       ]]></field>
 
+      <method name="_setValueInternal">
+        <parameter name="aValue"/>
+        <parameter name="aIsUserInput"/>
+        <body><![CDATA[
+          this.mIgnoreInput = true;
+
+          if (typeof this.onBeforeValueSet == "function")
+            aValue = this.onBeforeValueSet(aValue);
+
+          if (typeof this.trimValue == "function" &&
+              !this._textValueSetByCompleteDefault)
+            aValue = this.trimValue(aValue);
+
+          this.valueIsTyped = false;
+          if (aIsUserInput) {
+            this.inputField.setUserInput(aValue);
+          } else {
+            this.inputField.value = aValue;
+          }
+
+          if (typeof this.formatValue == "function")
+            this.formatValue();
+
+          this.mIgnoreInput = false;
+          var event = document.createEvent("Events");
+          event.initEvent("ValueChange", true, true);
+          this.inputField.dispatchEvent(event);
+          return aValue;
+        ]]></body>
+      </method>
+
       <method name="onInput">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (!this.mIgnoreInput && this.mController.input == this) {
             this.valueIsTyped = true;
             this.mController.handleText();
           }
           this.resetActionType();
--- a/toolkit/content/widgets/textbox.xml
+++ b/toolkit/content/widgets/textbox.xml
@@ -104,16 +104,23 @@
       </method>
 
       <method name="select">
         <body>
           this.inputField.select();
         </body>
       </method>
 
+      <method name="setUserInput">
+        <parameter name="value"/>
+        <body><![CDATA[
+          this.inputField.setUserInput(value);
+        ]]></body>
+      </method>
+
       <property name="controllers"    readonly="true" onget="return this.inputField.controllers"/>
       <property name="textLength"     readonly="true"
                                       onget="return this.inputField.textLength;"/>
       <property name="selectionStart" onset="this.inputField.selectionStart = val; return val;"
                                       onget="return this.inputField.selectionStart;"/>
       <property name="selectionEnd"   onset="this.inputField.selectionEnd = val; return val;"
                                       onget="return this.inputField.selectionEnd;"/>
 
--- a/toolkit/modules/sessionstore/FormData.jsm
+++ b/toolkit/modules/sessionstore/FormData.jsm
@@ -380,19 +380,19 @@ var FormDataInternal = {
   },
 
   /**
    * Dispatches an event of type "input" to the given |node|.
    *
    * @param node (DOMNode)
    */
   fireInputEvent(node) {
-    let doc = node.ownerDocument;
-    let event = doc.createEvent("UIEvents");
-    event.initUIEvent("input", true, true, doc.defaultView, 0);
+    let event = node.isInputEventTarget ?
+      new node.ownerGlobal.InputEvent("input", {bubbles: true}) :
+      new node.ownerGlobal.Event("input", {bubbles: true});
     node.dispatchEvent(event);
   },
 
   /**
    * Restores form data for the current frame hierarchy starting at |root|
    * using the given form |data|.
    *
    * If the given |root| frame's hierarchy doesn't match that of the given