Bug 1504911 - part 0: Add "input" event tests into existing tests r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Tue, 20 Nov 2018 14:24:06 +0000
changeset 503890 d4a142e1648b3e341fb0f486d7b15a0bad7e0904
parent 503889 fbebc15cd4f40f0978bf22def0fcf355ce69f27b
child 503891 48440593d675ccfe5a6893118a7e91cc4b823c3f
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 0: Add "input" event tests into existing tests r=smaug It's difficult to create new test which checks "input" events caused by all edit operations especially when text is inserted from our UI. Therefore, this adds "input" event type checks into existing tests. Additionally, this adds new test for MozEditableElement.setUserInput() whose behavior needs to be fixed in this bug. Currently, InputEvent interface should be used only on text controls or contenteditable editor when dispatching "input" event. https://w3c.github.io/input-events/#events-inputevents You may feel odd to use different event interface for same "input" events. However, other browsers also use InputEvent interface only in the cases. So, we should follow them for now. Differential Revision: https://phabricator.services.mozilla.com/D12243
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/mochitest/test_clear_form.html
browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
dom/base/test/chrome/window_nsITextInputProcessor.xul
dom/html/test/forms/mochitest.ini
dom/html/test/forms/test_MozEditableElement_setUserInput.html
dom/html/test/forms/test_input_event.html
dom/html/test/forms/test_input_number_mouse_events.html
editor/libeditor/tests/test_abs_positioner_positioning_elements.html
editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
editor/libeditor/tests/test_dom_input_event_on_texteditor.html
editor/libeditor/tests/test_dragdrop.html
editor/libeditor/tests/test_middle_click_paste.html
editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
editor/libeditor/tests/test_nsIPlaintextEditor_insertLineBreak.html
editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
editor/libeditor/tests/test_resizers_resizing_elements.html
editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
toolkit/components/satchel/test/test_form_autocomplete.html
toolkit/components/satchel/test/test_form_autocomplete_with_list.html
toolkit/components/satchel/test/test_submit_on_keydown_enter.html
toolkit/content/tests/chrome/file_editor_with_autocomplete.js
toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html
toolkit/content/tests/chrome/test_editor_for_textbox_with_autocomplete.xul
widget/tests/window_composition_text_querycontent.xul
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -108,17 +108,32 @@ function checkFieldValue(elem, expectedV
 function triggerAutofillAndCheckProfile(profile) {
   const adaptedProfile = _getAdaptedProfile(profile);
   const promises = [];
 
   for (const [fieldName, value] of Object.entries(adaptedProfile)) {
     const element = document.getElementById(fieldName);
     const expectingEvent = document.activeElement == element ? "DOMAutoComplete" : "change";
     const checkFieldAutofilled = Promise.all([
-      new Promise(resolve => element.addEventListener("input", resolve, {once: true})),
+      new Promise(resolve => element.addEventListener("input", (event) => {
+        if (element.tagName == "INPUT" && element.type == "text") {
+          todo(event instanceof InputEvent,
+               `"input" event should be dispatched with InputEvent interface on ${element.tagName}`);
+          todo_is(event.cancelable, false,
+                  `"input" event should be never cancelable on ${element.tagName}`);
+        } else {
+          todo(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})),
     ]).then(() => checkFieldValue(element, value));
 
     promises.push(checkFieldAutofilled);
   }
   // Press Enter key and trigger form autofill.
   synthesizeKey("KEY_Enter");
 
--- a/browser/extensions/formautofill/test/mochitest/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_clear_form.html
@@ -62,17 +62,25 @@ function checkIsFormCleared(patch = {}) 
     checkFieldHighlighted(elem, false);
     checkFieldPreview(elem, "");
   }
 }
 
 async function confirmClear(selector) {
   info("Await for clearing input");
   let promise = new Promise(resolve =>
-    document.querySelector(selector).addEventListener("input", resolve, {once: true})
+    document.querySelector(selector).addEventListener("input", (event) => {
+      todo(event instanceof InputEvent,
+           '"input" event should be dispatched with InputEvent interface');
+      todo_is(event.cancelable, false,
+              '"input" event should be never cancelable');
+      is(event.bubbles, true,
+         '"input" event should always bubble');
+      resolve();
+    }, {once: true})
   );
   synthesizeKey("KEY_Enter");
   await promise;
 }
 
 add_task(async function simple_clear() {
   await triggerPopupAndHoverItem("#organization", 0);
   await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
--- 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
@@ -42,18 +42,31 @@ let MOCK_STORAGE = [{
   tel: "+14186917110",
   country: "CA",
   "address-level1": "Qu├ębec",
 }];
 
 function checkElementFilled(element, expectedvalue) {
   return [
     new Promise(resolve => {
-      element.addEventListener("input", function onInput() {
+      element.addEventListener("input", function onInput(event) {
         ok(true, "Checking " + element.name + " field fires input event");
+        if (element.tagName == "INPUT" && element.type == "text") {
+          todo(event instanceof InputEvent,
+               `"input" event should be dispatched with InputEvent interface on ${element.name}`);
+          todo_is(event.cancelable, false,
+                  `"input" event should be never cancelable on ${element.name}`);
+        } else {
+          todo(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});
     }),
     new Promise(resolve => {
       element.addEventListener("change", function onChange() {
         ok(true, "Checking " + element.name + " field fires change event");
         is(element.value, expectedvalue, "Checking " + element.name + " field");
         resolve();
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul
@@ -54,16 +54,26 @@ function finish()
   window.close();
 }
 
 function onunload()
 {
   SimpleTest.finish();
 }
 
+function checkInputEvent(aEvent, aIsComposing, aDescription) {
+  if (aEvent.type != "input") {
+    return;
+  }
+  ok(aEvent instanceof InputEvent, `${aDescription}"input" event should be dispatched with InputEvent interface`);
+  is(aEvent.cancelable, false, `${aDescription}"input" event should be never cancelable`);
+  is(aEvent.bubbles, true, `${aDescription}"input" event should always bubble`);
+  is(aEvent.isComposing, aIsComposing, `${aDescription}isComposing should be ${aIsComposing}`);
+}
+
 const kIsMac = (navigator.platform.indexOf("Mac") == 0);
 
 var iframe = document.getElementById("iframe");
 var childWindow = iframe.contentWindow;
 var textareaInFrame;
 var input = document.getElementById("input");
 var otherWindow = window.opener;
 var otherDocument = otherWindow.document;
@@ -217,16 +227,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], true, description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -252,16 +263,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");");
@@ -299,16 +311,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
+  checkInputEvent(events[4], false, description);
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during cancelComposition().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -341,16 +354,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], false, description);
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransaction(window, simpleCallback),
@@ -380,16 +394,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during startComposition().
   var events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
@@ -437,16 +452,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], true, description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -472,16 +488,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");");
@@ -519,16 +536,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
+  checkInputEvent(events[4], false, description);
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during cancelComposition().
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -561,16 +579,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], false, description);
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
   events = [];
   TIP1.beginInputTransactionForTests(window);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransactionForTests(window),
@@ -600,16 +619,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition().
   var events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
@@ -687,16 +707,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], true, description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -740,16 +761,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
@@ -817,16 +839,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
+  checkInputEvent(events[4], false, description);
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -883,16 +906,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], false, description);
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
@@ -946,16 +970,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition().
   var events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
@@ -1033,16 +1058,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], true, description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -1086,16 +1112,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
@@ -1163,16 +1190,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
+  checkInputEvent(events[4], false, description);
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -1229,16 +1257,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
+  checkInputEvent(events[3], false, description);
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
@@ -1292,16 +1321,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
+  checkInputEvent(events[2], false, description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if startComposition() throws an exception after ownership is stolen.
   input.value = "";
   ok(TIP1.beginInputTransactionForTests(window),
      description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
   ok(TIP2.beginInputTransactionForTests(window),
--- a/dom/html/test/forms/mochitest.ini
+++ b/dom/html/test/forms/mochitest.ini
@@ -77,16 +77,17 @@ skip-if = os == "android" && debug # bug
 [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_progress_element.html]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -0,0 +1,244 @@
+<!DOCTYPE>
+<html>
+<head>
+  <title>Test for MozEditableElement.setUserInput()</title>
+  <script src="/tests/SimpleTest/SimpleTest.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();
+SimpleTest.waitForFocus(() => {
+  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.
+   *     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}},
+                    {element: "input", type: "text",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                    {element: "input", type: "search",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                    {element: "input", type: "tel",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                    {element: "input", type: "url",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                    {element: "input", type: "email",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: true}},
+                    {element: "input", type: "password",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: 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}},
+                    // "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}},
+                    // "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}},
+                    // "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}},
+                    // "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}},
+                    {element: "input", type: "number",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                    {element: "input", type: "range",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                    // "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}},
+                    {element: "input", type: "checkbox",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                    {element: "input", type: "radio",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: false}},
+                    // "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}},
+                    {element: "input", type: "submit",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                    {element: "input", type: "image",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                    {element: "input", type: "reset",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                    {element: "input", type: "button",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: false, useInputEvent: false}},
+                    {element: "textarea",
+                     input: {before: "3", after: "6"},
+                     result: {before: "3", after:"6", fireInputEvent: true, useInputEvent: 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 = [];
+    function onInput(aEvent) {
+      inputEvents.push(aEvent);
+    }
+    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 (test.element === "textarea") {
+      todo_is(inputEvents.length, 1,
+              `Only one "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+    } else if (target.value == previousValue) {
+      if (test.type === "date" ||
+          test.type === "month" ||
+          test.type === "week" ||
+          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.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 > 0) {
+      if (test.result.useInputEvent) {
+        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 Event && !(inputEvents[0] instanceof UIEvent),
+           `"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+      }
+      todo_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)`);
+    }
+
+    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 (test.element === "textarea") {
+      todo_is(inputEvents.length, 1,
+              `Only one "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+    } else if (target.value == previousValue) {
+      if (test.type === "date" ||
+          test.type === "month" ||
+          test.type === "week" ||
+          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.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 > 0) {
+      if (test.result.useInputEvent) {
+        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 Event && !(inputEvents[0] instanceof UIEvent),
+           `"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+      }
+      todo_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);
+  }
+
+  SimpleTest.finish();
+});
+</script>
+</body>
+</html>
--- a/dom/html/test/forms/test_input_event.html
+++ b/dom/html/test/forms/test_input_event.html
@@ -8,69 +8,125 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
 <script type="application/javascript" 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">
-<input type="file" id="fileInput"></input>
-<textarea id="textarea" oninput="++textareaInput;"></textarea>
-<input type="text" id="input_text" oninput="++textInput[0];"></input>
-<input type="email" id="input_email" oninput="++textInput[1];"></input>
-<input type="search" id="input_search" oninput="++textInput[2];"></input>
-<input type="tel" id="input_tel" oninput="++textInput[3];"></input>
-<input type="url" id="input_url" oninput="++textInput[4];"></input>
-<input type="password" id="input_password" oninput="++textInput[5];"></input>
+<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" oninput="++NonTextInput[0];"></input>
-<input type="submit" id="input_submit" oninput="++NonTextInput[1];"></input>
-<input type="image" id="input_image" oninput="++NonTextInput[2];"></input>
-<input type="reset" id="input_reset" oninput="++NonTextInput[3];"></input>
-<input type="radio" id="input_radio" oninput="++NonTextInput[4];"></input>
-<input type="checkbox" id="input_checkbox" oninput="++NonTextInput[5];"></input>
-<input type="range" id="input_range" oninput="++rangeInput;"></input>
-<input type="number" id="input_number" oninput="++numberInput;"></input>
+<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">
  
 </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);
 
-  var textareaInput = 0;
+  function checkIfInputIsInputEvent(aEvent, aToDo, aDescription) {
+    if (aToDo) {
+      // Probably, key operation should fire "input" event with InputEvent interface.
+      // See https://github.com/w3c/input-events/issues/88
+      todo(aEvent instanceof InputEvent,
+         `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+    } else {
+      ok(aEvent instanceof InputEvent,
+         `"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}`);
+  }
 
-  // Those are types were the input event apply.
+  function checkIfInputIsEvent(aEvent, aDescription) {
+    if (event.target.type === "checkbox" || event.target.type === "radio") {
+      todo(event instanceof Event && !(event instanceof UIEvent),
+           `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+    } else {
+      ok(event instanceof Event && !(event 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}`);
+  }
+
+  var textareaInput = 0;
+  document.getElementById("textarea").oninput = (aEvent) => {
+    ++textareaInput;
+    checkIfInputIsInputEvent(aEvent, false, "on textarea element");
+  };
+
+  // These are the type were the input event apply.
   var textTypes = ["text", "email", "search", "tel", "url", "password"];
   var textInput = [0, 0, 0, 0, 0, 0];
+  for (let id of ["input_text", "input_email", "input_search", "input_tel", "input_url", "input_password"]) {
+    document.getElementById(id).oninput = (aEvent) => {
+      ++textInput[textTypes.indexOf(aEvent.target.type)];
+      checkIfInputIsInputEvent(aEvent, false, `on input element whose type is ${aEvent.target.type}`);
+    };
+  }
 
-  // Those are events were the input event does not apply.
+  // These are the type were the input event does not apply.
   var NonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
   var NonTextInput = [0, 0, 0, 0, 0, 0];
+  for (let id of ["input_button", "input_submit", "input_image", "input_reset", "input_radio", "input_checkbox"]) {
+    document.getElementById(id).oninput = (aEvent) => {
+      ++NonTextInput[NonTextTypes.indexOf(aEvent.target.type)];
+      checkIfInputIsEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+    };
+  }
 
   var rangeInput = 0;
+  document.getElementById("input_range").oninput = (aEvent) => {
+    ++rangeInput;
+    checkIfInputIsEvent(aEvent, "on input element whose type is range");
+  };
+
   var numberInput = 0;
+  document.getElementById("input_number").oninput = (aEvent) => {
+    ++numberInput;
+    checkIfInputIsInputEvent(aEvent, true, "on input element whose type is number");
+  };
 
   SimpleTest.waitForExplicitFinish();
   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("input", function (aEvent) {
       ok(true, "input event should have been dispatched on file input.");
+      checkIfInputIsEvent(aEvent, "on file input");
     });
 
     input.click();
     setTimeout(testUserInput2, 0);
   }
 
   function testUserInput2() {
     // Some generic checks for types that support the input event.
--- a/dom/html/test/forms/test_input_number_mouse_events.html
+++ b/dom/html/test/forms/test_input_number_mouse_events.html
@@ -43,16 +43,24 @@ var inputRect = input.getBoundingClientR
 
 // 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
+  todo(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}`);
+}
+
 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");
@@ -129,16 +137,17 @@ 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", function(evt) {
       ++inputEventCount;
+      checkInputEvent(event, "#1");
       if (inputEventCount == 3) {
         is(input.value, "3", "Testing spin-up button");
         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);
         synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
         runNextSpinTest();
@@ -149,16 +158,17 @@ var spinTests = [
 
   // 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", function(evt) {
       ++inputEventCount;
+      checkInputEvent(event, "#2");
       if (inputEventCount == 3) {
         is(input.value, "-3", "Testing spin-down button");
         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);
         synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
         runNextSpinTest();
@@ -169,16 +179,17 @@ var spinTests = [
 
   // 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", function(evt) {
       ++inputEventCount;
+      checkInputEvent(event, "#3");
       if (inputEventCount == 3) {
         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" });
@@ -190,16 +201,17 @@ var spinTests = [
   },
 
   // 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(event, "#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" });
--- a/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
+++ b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
@@ -67,28 +67,49 @@ SimpleTest.waitForFocus(async function()
 
       // left is abs positioned element's left + margin-left + border-left-width + 12.
       // XXX Perhaps, we need to add border-left-width here if you add new test to have thick border.
       const kPositionerX = 18;
       // top is abs positioned element's top + margin-top + border-top-width - 14.
       // XXX Perhaps, we need to add border-top-width here if you add new test to have thick border.
       const kPositionerY = -7;
 
+      let inputEventExpected = true;
+      function onInput(aEvent) {
+        if (!inputEventExpected) {
+          ok(false, "\"input\" event shouldn't be fired after stopping resizing");
+          return;
+        }
+        ok(aEvent instanceof InputEvent,
+           '"input" event should be dispatched with InputEvent interface');
+        is(aEvent.cancelable, false,
+           '"input" event should be never cancelable');
+        is(aEvent.bubbles, true,
+           '"input" event should always bubble');
+      }
+
+      content.addEventListener("input", onInput);
+
       // Click on the positioner.
       synthesizeMouse(target, kPositionerX, kPositionerY, {type: "mousedown"});
       // Drag it delta pixels.
       synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mousemove"});
       // Release the mouse button
       synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mouseup"});
+
+      inputEventExpected = false;
+
       // Move the mouse delta more pixels to the same direction to make sure that the
       // positioning operation has stopped.
       synthesizeMouse(target, kPositionerX + aDeltaX * 2, kPositionerY + aDeltaY * 2, {type: "mousemove"});
       // Click outside of the image to hide the positioner.
       synthesizeMouseAtCenter(outOfEditor, {});
 
+      content.removeEventListener("input", onInput);
+
       // Get the new dimensions for the absolute positioned element.
       let newRect = target.getBoundingClientRect();
       isfuzzy(newRect.x, rect.x + aDeltaX, 1, description + "The left should be increased by " + aDeltaX + " pixels");
       isfuzzy(newRect.y, rect.y + aDeltaY, 1, description + "The top should be increased by " + aDeltaY + "pixels");
     }
 
     await testPositioner( 10, 10);
     await testPositioner( 10, -10);
--- a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
+++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
@@ -58,16 +58,18 @@ function runTests() {
       aDocument.getSelection().collapse(editTarget, 0);
     }
 
     var inputEvent = null;
 
     var handler = function(aEvent) {
       is(aEvent.target, eventTarget,
          "input event is fired on unexpected element: " + aEvent.target.tagName);
+      ok(aEvent instanceof InputEvent,
+         "input event should be dispatched with InputEvent interface");
       ok(!aEvent.cancelable, "input event must not be cancelable");
       ok(aEvent.bubbles, "input event must be bubbles");
       if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {
         let duration = Math.abs(window.performance.now() - aEvent.timeStamp);
         ok(duration < 30 * 1000,
            "perhaps, timestamp wasn't set correctly :" + aEvent.timeStamp +
            " (expected it to be within 30s of the current time but it " +
            "differed by " + duration + "ms)");
--- a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
+++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
@@ -32,16 +32,18 @@ function runTests() {
     aElement.focus();
     aElement.value = "";
 
     var inputEvent = null;
 
     var handler = function(aEvent) {
       is(aEvent.target, aElement,
          "input event is fired on unexpected element: " + aEvent.target.tagName);
+      ok(aEvent instanceof InputEvent,
+         "input event should be dispatched with InputEvent interface");
       ok(!aEvent.cancelable, "input event must not be cancelable");
       ok(aEvent.bubbles, "input event must be bubbles");
       if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {
         let duration = Math.abs(window.performance.now() - aEvent.timeStamp);
         ok(duration < 30 * 1000,
            "perhaps, timestamp wasn't set correctly :" + aEvent.timeStamp +
            " (expected it to be within 30s of the current time but it " +
            "differed by " + duration + "ms)");
--- a/editor/libeditor/tests/test_dragdrop.html
+++ b/editor/libeditor/tests/test_dragdrop.html
@@ -19,160 +19,229 @@
 <script type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 
 // This listener allows us to clear the default data for the selection added for the drag.
 var shouldClear = false;
 window.addEventListener("dragstart", function(event) { if (shouldClear) event.dataTransfer.clearData(); }, true);
 
+function checkInputEvent(aEvent, aExpectedTarget, aDescription) {
+  ok(aEvent instanceof InputEvent, `"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}`);
+  is(aEvent.target, aExpectedTarget, `"input" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element ${aDescription}`);
+}
+
 function doTest() {
   const htmlContextData = { type: "text/_moz_htmlcontext",
                             data: "<html><body></body></html>" };
   const htmlInfoData = { type: "text/_moz_htmlinfo", data: "0,0" };
   const htmlData = { type: "text/html", data: '<span id="text" style="font-size: 40px;">Some Text</span>' };
 
   const htmlContextDataEditable = { type: "text/_moz_htmlcontext",
                                     data: '<html><body><p id="contenteditable" contenteditable="true"></p></body></html>' };
 
   var text = document.getElementById("text");
   var textarea = document.getElementById("textarea");
   var bold = document.getElementById("bold");
   var input = document.getElementById("input");
   var contenteditable = document.getElementById("contenteditable");
 
+  var inputEvents = [];
+  function onInput(event) {
+    inputEvents.push(event);
+  }
+  document.addEventListener("input", onInput);
+
   var selection = window.getSelection();
 
   // -------- Test dragging regular text
   selection.selectAllChildren(text);
+  inputEvents = [];
   var result = synthesizeDragStart(text, [[htmlContextData, htmlInfoData, htmlData,
                                            {type: "text/plain", data: "Some Text"}]], window, 40, 10);
   is(result, null, "Test dragging regular text");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dragging from text in <span> element');
 
   // -------- Test dragging text from an <input>
   input.setSelectionRange(1, 4);
+  inputEvents = [];
   result = synthesizeDragStart(input, [[{type: "text/plain", data: "rag"}]], window, 25, 8);
   is(result, null, "Test dragging input");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dragging from text in <input> element');
 
   // -------- Test dragging text from a <textarea>
   textarea.setSelectionRange(1, 7);
+  inputEvents = [];
   result = synthesizeDragStart(textarea, [[{type: "text/plain", data: "ome Te"}]], window, 25, 6);
   is(result, null, "Test dragging textarea");
   textarea.blur();
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dragging from text in <textarea> element');
 
   // -------- Test dragging text from a contenteditable
   selection.selectAllChildren(contenteditable.childNodes[1]);
+  inputEvents = [];
   result = synthesizeDragStart(contenteditable.childNodes[1],
                                [[htmlContextDataEditable, htmlInfoData,
                                 {type: "text/html", data: '<b id="bold">editable</b>' },
                                 {type: "text/plain", data: "editable"}]], window, 5, 6);
   is(result, null, "Test dragging contenteditable");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dragging from text in contenteditable element');
   contenteditable.blur();
 
   // -------- Test dragging regular text of text/html to <input>
 
   selection.selectAllChildren(text);
   input.value = "";
+  inputEvents = [];
   synthesizeDrop(text, input, [], "copy");
   is(input.value, "Some Text", "Drag text/html onto input");
+  is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dropping text/html into empty <input> element');
+  checkInputEvent(inputEvents[0], input, "when dropping text/html into empty <input> element");
 
   // -------- Test dragging regular text of text/html to disabled <input>
 
   selection.selectAllChildren(text);
   input.value = "";
   input.disabled = true;
+  inputEvents = [];
   synthesizeDrop(text, input, [], "copy");
   is(input.value, "", "Drag text/html onto disabled input");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dropping on disabled <input> element');
   input.disabled = false;
 
   // -------- Test dragging regular text of text/html to readonly <input>
 
   selection.selectAllChildren(text);
   input.readOnly = true;
+  inputEvents = [];
   synthesizeDrop(text, input, [], "copy");
   is(input.value, "", "Drag text/html onto readonly input");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dropping on readonly <input> element');
   input.readOnly = false;
 
   // -------- Test dragging regular text of text/html to <input>. This sets
   //          shouldClear to true so that the default drag data is not present
   //          and we can use the data passed to synthesizeDrop. This allows
   //          testing of a drop with just text/html.
   shouldClear = true;
   selection.selectAllChildren(text);
   input.value = "";
+  inputEvents = [];
   synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy");
   is(input.value, "", "Drag text/html onto input");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dropping text/html into empty <input> element');
 
   // -------- Test dragging regular text of text/plain and text/html to <input>
 
   selection.selectAllChildren(text);
   input.value = "";
+  inputEvents = [];
   synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"},
                                 {type: "text/plain", data: "Some Plain Text"}]], "copy");
   is(input.value, "Some Plain Text", "Drag text/html and text/plain onto input");
+  is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dropping text/plain into empty <input> element');
+  checkInputEvent(inputEvents[0], input, "when dropping text/plain into empty <input> element");
 
   // -------- Test dragging regular text of text/plain to <textarea>
 
 // XXXndeakin Can't test textareas due to some event handling issue
 //  selection.selectAllChildren(text);
 //  synthesizeDrop(text, textarea, [[{type: "text/plain", data: "Somewhat Longer Text"}]], "copy");
 //  is(textarea.value, "Somewhat Longer Text", "Drag text/plain onto textarea");
 
-  // -------- Test dragging special text type of text/plain to contenteditable
+  // -------- Test dragging special text type of text/plain to <input>
 
   selection.selectAllChildren(text);
+  inputEvents = [];
   synthesizeDrop(text, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy");
   is(input.value, "Some Plain Text", "Drag text/x-moz-text-internal onto input");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when dropping text/x-moz-text-internal into <input> element');
 
   // -------- Test dragging regular text of text/plain to contenteditable
 
   selection.selectAllChildren(text);
+  inputEvents = [];
   synthesizeDrop(text, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy");
   is(contenteditable.childNodes.length, 3, "Drag text/plain onto contenteditable child nodes");
   is(contenteditable.textContent, "This is some editable text.Sample Text",
                                   "Drag text/plain onto contenteditable text");
+  is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dropping text/plain into contenteditable element');
+  checkInputEvent(inputEvents[0], contenteditable, "when dropping text/plain into contenteditable element");
 
   // -------- Test dragging regular text of text/html to contenteditable
 
   selection.selectAllChildren(text);
+  inputEvents = [];
   synthesizeDrop(text, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy");
   is(contenteditable.childNodes.length, 6, "Drag text/html onto contenteditable child nodes");
   is(contenteditable.childNodes[4].tagName, "I", "Drag text/html onto contenteditable italic");
   is(contenteditable.childNodes[4].textContent, "Italic", "Drag text/html onto contenteditable italic text");
+  is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dropping text/html into contenteditable element');
+  checkInputEvent(inputEvents[0], contenteditable, "when dropping text/html into contenteditable element");
 
   // -------- Test dragging contenteditable to <input>
 
   selection.selectAllChildren(document.getElementById("bold"));
+  inputEvents = [];
   synthesizeDrop(bold, input, [[{type: "text/html", data: "<b>editable</b>"},
                                 {type: "text/plain", data: "editable"}]], "copy");
-  is(input.value, "Some Plain Texteditable", "Move text/html and text/plain from contenteditable onto input");
+  is(input.value, "Some Plain Texteditable", "Copy text/html and text/plain from contenteditable onto input");
+  is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dragging from contenteditable to <input> element');
+  checkInputEvent(inputEvents[0], input, "when dragging from contenteditable to <input> element");
 
   // -------- Test dragging contenteditable to contenteditable
 
   shouldClear = false;
 
   selection.selectAllChildren(contenteditable.childNodes[4]);
+  inputEvents = [];
   synthesizeDrop(contenteditable.childNodes[4], contenteditable, [], "copy");
   is(contenteditable.childNodes.length, 7, "Move text/html and text/plain from contenteditable onto itself child nodes");
   is(contenteditable.childNodes[6].tagName, "I", "Move text/html and text/plain from contenteditable onto itself italic");
   is(contenteditable.childNodes[6].textContent, "Italic", "Move text/html and text/plain from contenteditable onto itself text");
+  is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dragging (copy) in a contentediable element');
+  checkInputEvent(inputEvents[0], contenteditable, "when dragging (copy) in a contentediable element");
 
   // We'd test 'move' here as well as 'copy', but that requires knowledge of
   // the source of the drag which drag simulation doesn't provide.
 
   // -------- Test dragging non-editable nested inside contenteditable to contenteditable
 
   input.focus(); // this resets some state in the selection otherwise an inexplicable error occurs calling selectAllChildren.
   input.blur();
 
   var nonEditable = document.getElementById("noneditable");
   selection.selectAllChildren(nonEditable);
+  inputEvents = [];
   synthesizeDrop(nonEditable, document.getElementById("first"), [], "copy");
   is(document.getElementById("nestedce").textContent, " MiddleFirst letter Middle Last part",
      "Drag non-editable text/html onto contenteditable text");
+  todo_is(inputEvents.length, 1,
+     'Only one "input" events should be fired when dragging from inner non-editable element to a contentediable element');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], document.getElementById("nestedce"), "when dragging from inner non-editable element to a contentediable element");
+  }
+
+  document.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 }
 
 SimpleTest.waitForFocus(doTest);
 
 </script>
 </body>
--- a/editor/libeditor/tests/test_middle_click_paste.html
+++ b/editor/libeditor/tests/test_middle_click_paste.html
@@ -71,180 +71,257 @@ async function copyHTMLContent(aInnerHTM
       () => {
         ok(false, `Failed to copy "${aInnerHTML}" to clipboard`);
         SimpleTest.finish();
       },
       "text/html");
   });
 }
 
+function checkInputEvent(aEvent, aDescription) {
+  ok(aEvent instanceof InputEvent,
+     `"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}`);
+}
+
 async function doTextareaTests(aTextarea) {
+  let inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  aTextarea.addEventListener("input", onInput);
+
   await copyPlaintext("abc\ndef\nghi");
   aTextarea.focus();
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
   is(aTextarea.value,
      "> abc\n> def\n> ghi\n\n",
      "Pasted each line should start with \"> \"");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired #1');
+  checkInputEvent(inputEvents[0], "#1");
   aTextarea.value = "";
 
   await copyPlaintext("> abc\n> def\n> ghi");
   aTextarea.focus();
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
   is(aTextarea.value,
      ">> abc\n>> def\n>> ghi\n\n",
      "Pasted each line should be start with \">> \" when already quoted one level");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired #2');
+  checkInputEvent(inputEvents[0], "#2");
   aTextarea.value = "";
 
   await copyPlaintext("> abc\n> def\n\nghi");
   aTextarea.focus();
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
   is(aTextarea.value,
      ">> abc\n>> def\n> \n> ghi\n\n",
      "Pasted each line should be start with \">> \" when already quoted one level");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired #3');
+  checkInputEvent(inputEvents[0], "#3");
   aTextarea.value = "";
 
   await copyPlaintext("abc\ndef\n\n");
   aTextarea.focus();
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
   is(aTextarea.value,
      "> abc\n> def\n> \n",
      "If pasted text ends with \"\\n\", only the last line should not started with \">\"");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired #4');
+  checkInputEvent(inputEvents[0], "#4");
   aTextarea.value = "";
 
   let pasteEventCount = 0;
   function pasteEventLogger(event) {
     pasteEventCount++;
   }
   aTextarea.addEventListener("paste", pasteEventLogger);
 
   await copyPlaintext("abc");
   aTextarea.focus();
   document.body.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   is(aTextarea.value, "",
      "If 'click' event is consumed at capturing phase of the <body>, paste should be canceled");
   is(pasteEventCount, 0,
      "If 'click' event is consumed at capturing phase of the <body>, 'paste' event should not be fired");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when the "click" event is canceled');
   aTextarea.value = "";
 
   await copyPlaintext("abc");
   aTextarea.focus();
   aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   is(aTextarea.value, "abc",
      "Even if 'mouseup' event is consumed, paste should be done");
   is(pasteEventCount, 1,
      "Even if 'mouseup' event is consumed, 'paste' event should be fired once");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired even if "mouseup" event is canceled');
+  checkInputEvent(inputEvents[0], 'even if "mouseup" event is canceled');
   aTextarea.value = "";
 
   await copyPlaintext("abc");
   aTextarea.focus();
   aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   is(aTextarea.value, "abc",
      "Even if 'click' event handler is added to the <textarea>, paste should not be canceled");
   is(pasteEventCount, 1,
      "Even if 'click' event handler is added to the <textarea>, 'paste' event should be fired once");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired even if "click" event is canceled in bubbling phase');
+  checkInputEvent(inputEvents[0], 'even if "click" event is canceled in bubbling phase');
   aTextarea.value = "";
 
   await copyPlaintext("abc");
   aTextarea.focus();
   aTextarea.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   todo_is(aTextarea.value, "",
           "If 'auxclick' event is consumed, paste should be canceled");
   todo_is(pasteEventCount, 0,
           "If 'auxclick' event is consumed, 'paste' event should not be fired once");
+  todo_is(inputEvents.length, 0,
+          'No "input" event should be fired if "auxclick" event is canceled');
   aTextarea.value = "";
 
   aTextarea.removeEventListener("paste", pasteEventLogger);
+  aTextarea.removeEventListener("input", onInput);
 }
 
 async function doContenteditableTests(aEditableDiv) {
+  let inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  aEditableDiv.addEventListener("input", onInput);
+
   await copyPlaintext("abc\ndef\nghi");
   aEditableDiv.focus();
+  inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true});
   is(aEditableDiv.innerHTML,
      "<blockquote type=\"cite\">abc<br>def<br>ghi</blockquote>",
      "Pasted plaintext should be in <blockquote> element and each linebreaker should be <br> element");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired on the editing host');
+  checkInputEvent(inputEvents[0], "(contenteditable)");
   aEditableDiv.innerHTML = "";
 
   let pasteEventCount = 0;
   function pasteEventLogger(event) {
     pasteEventCount++;
   }
   aEditableDiv.addEventListener("paste", pasteEventLogger);
 
   await copyPlaintext("abc");
   aEditableDiv.focus();
   window.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
+  inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   is(aEditableDiv.innerHTML, "",
      "If 'click' event is consumed at capturing phase of the window, paste should be canceled");
   is(pasteEventCount, 0,
      "If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when the "click" event is canceled (contenteditable)');
   aEditableDiv.innerHTML = "";
 
   await copyPlaintext("abc");
   aEditableDiv.focus();
   aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   is(aEditableDiv.innerHTML, "abc",
      "Even if 'mouseup' event is consumed, paste should be done");
   is(pasteEventCount, 1,
      "Even if 'mouseup' event is consumed, 'paste' event should be fired once");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired even if "mouseup" event is canceled (contenteditable)');
+  checkInputEvent(inputEvents[0], 'even if "mouseup" event is canceled (contenteditable)');
   aEditableDiv.innerHTML = "";
 
   await copyPlaintext("abc");
   aEditableDiv.focus();
   aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   is(aEditableDiv.innerHTML, "abc",
      "Even if 'click' event handler is added to the editing host, paste should not be canceled");
   is(pasteEventCount, 1,
      "Even if 'click' event handler is added to the editing host, 'paste' event should be fired");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)');
+  checkInputEvent(inputEvents[0], 'even if "click" event is canceled in bubbling phase (contenteditable)');
   aEditableDiv.innerHTML = "";
 
   await copyPlaintext("abc");
   aEditableDiv.focus();
   aEditableDiv.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   todo_is(aEditableDiv.innerHTML, "",
           "If 'auxclick' event is consumed, paste should be canceled");
   todo_is(pasteEventCount, 0,
           "If 'auxclick' event is consumed, 'paste' event should not be fired");
+  todo_is(inputEvents.length, 0,
+          'No "input" event should be fired if "auxclick" event is canceled (contenteditable)');
   aEditableDiv.innerHTML = "";
 
   aEditableDiv.removeEventListener("paste", pasteEventLogger);
 
   // Oddly, copyHTMLContent fails randomly only on Linux.  Let's skip this.
   if (navigator.platform.startsWith("Linux")) {
+    aEditableDiv.removeEventListener("input", onInput);
     return;
   }
 
   await copyHTMLContent("<p>abc</p><p>def</p><p>ghi</p>");
   aEditableDiv.focus();
+  inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true});
   if (!navigator.appVersion.includes("Android")) {
     is(aEditableDiv.innerHTML,
        "<blockquote type=\"cite\"><p>abc</p><p>def</p><p>ghi</p></blockquote>",
        "Pasted HTML content should be set to the <blockquote>");
   } else {
     // Oddly, on Android, we use <br> elements for pasting <p> elements.
     is(aEditableDiv.innerHTML,
        "<blockquote type=\"cite\">abc<br><br>def<br><br>ghi</blockquote>",
        "Pasted HTML content should be set to the <blockquote>");
   }
+  is(inputEvents.length, 1,
+     'One "input" event should be fired when pasting HTML');
+  checkInputEvent(inputEvents[0], "when pasting HTML");
   aEditableDiv.innerHTML = "";
+
+  aEditableDiv.removeEventListener("input", onInput);
 }
 
 async function doNestedEditorTests(aEditableDiv) {
   await copyPlaintext("CLIPBOARD TEXT");
   aEditableDiv.innerHTML = '<p id="p">foo</p><textarea id="textarea"></textarea>';
   aEditableDiv.focus();
   let textarea = document.getElementById("textarea");
   let pasteTarget = null;
--- a/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
+++ b/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
@@ -14,62 +14,95 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  let inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
   editor.focus();
   selection.collapse(editor, 0);
 
   // Tests when the editor is in plaintext mode.
 
   getEditor().flags |= SpecialPowers.Ci.nsIPlaintextEditor.eEditorPlaintextMask;
 
+  inputEvents = [];
   getEditorMailSupport().insertAsCitedQuotation("this is quoted text\nAnd here is second line.", "this is cited text", false);
 
   ok(selection.isCollapsed,
      "Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor");
   is(selection.focusNode, editor,
      "focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor");
   is(selection.focusOffset, 1,
      "focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor");
   is(editor.innerHTML, '<span style="white-space: pre-wrap;">&gt; this is quoted text<br>&gt; And here is second line.<br><br></span>',
      "The quoted text should be inserted as plaintext into the plaintext editor");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
 
   // Tests when the editor is in HTML editor mode.
   getEditor().flags &= ~SpecialPowers.Ci.nsIPlaintextEditor.eEditorPlaintextMask;
 
   editor.innerHTML = "";
 
+  inputEvents = [];
   getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>", "this is cited text", false);
 
   ok(selection.isCollapsed,
      "Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)");
   is(selection.focusNode, editor,
      "focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)");
   is(selection.focusOffset, 1,
      "focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)");
   is(editor.innerHTML,
      '<blockquote type="cite" cite="this is cited text">this is quoted text&lt;br&gt;</blockquote>', "The quoted text should be inserted as plaintext into the HTML editor");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
 
   editor.innerHTML = "";
 
+  inputEvents = [];
   getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>And here is second line.", "this is cited text", true);
 
   ok(selection.isCollapsed,
      "Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)");
   is(selection.focusNode, editor,
      "focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)");
   is(selection.focusOffset, 1,
      "focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)");
   is(editor.innerHTML, '<blockquote type="cite" cite="this is cited text">this is quoted text<br>And here is second line.</blockquote>',
      "The quoted text should be inserted as HTML source into the HTML editor");
+  is(inputEvents.length, 1,
+     'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
 
   SimpleTest.finish();
 });
 
 function getEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window);
 }
--- a/editor/libeditor/tests/test_nsIPlaintextEditor_insertLineBreak.html
+++ b/editor/libeditor/tests/test_nsIPlaintextEditor_insertLineBreak.html
@@ -17,205 +17,283 @@
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let input = document.getElementsByTagName("input")[0];
   let textarea = document.getElementsByTagName("textarea")[0];
   let contenteditable = document.getElementById("content");
   let selection = window.getSelection();
-  let inputEventCount = 0;
 
+  let inputEvents = [];
   function onInput(event) {
-    inputEventCount++;
+    inputEvents.push(event);
   }
 
   input.focus();
   input.selectionStart = input.selectionEnd = 3;
-  inputEventCount = 0;
+  inputEvents = [];
   input.addEventListener("input", onInput);
   try {
     getPlaintextEditor(input).insertLineBreak();
   } catch (e) {}
   input.removeEventListener("input", onInput);
   is(input.value, "abcdef", "nsIPlaintextEditor.insertLineBreak() should do nothing on single line editor");
-  is(inputEventCount, 0, "nsIPlaintextEditor.insertLineBreak() shouldn't cause 'input' event on single line editor");
+  is(inputEvents.length, 0, "nsIPlaintextEditor.insertLineBreak() shouldn't cause 'input' event on single line editor");
 
   textarea.focus();
   textarea.selectionStart = textarea.selectionEnd = 3;
-  inputEventCount = 0;
+  inputEvents = [];
   textarea.addEventListener("input", onInput);
   getPlaintextEditor(textarea).insertLineBreak();
   textarea.removeEventListener("input", onInput);
   is(textarea.value, "abc\ndef", "nsIPlaintextEditor.insertLineBreak() should insert \n into multi-line editor");
-  is(inputEventCount, 1, "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on multi-line editor");
+  is(inputEvents.length, 1, "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on multi-line editor");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (on multi-line editor)');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (on multi-line editor)');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (on multi-line editor)');
 
   // Note that despite of the name, insertLineBreak() should insert paragraph separator in HTMLEditor.
 
   document.execCommand("defaultParagraphSeparator", false, "br");
 
   contenteditable.innerHTML = "abcdef";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "abc<br>def",
      "nsIPlaintextEditor.insertLineBreak() should insert <br> element into text node when defaultParagraphSeparator is \"br\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"br\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #1');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #1');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "br") #1');
 
   contenteditable.innerHTML = "<p>abcdef</p>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
      "nsIPlaintextEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is \"br\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"br\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #2');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #2');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "br") #2');
 
   contenteditable.innerHTML = "<div>abcdef</div>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<div>abc<br>def</div>",
      "nsIPlaintextEditor.insertLineBreak() should insert <br> element into <div> element when defaultParagraphSeparator is \"br\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"br\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #3');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #3');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "br") #3');
 
   contenteditable.innerHTML = "<pre>abcdef</pre>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
      "nsIPlaintextEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is \"br\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"br\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #4');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #4');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "br") #4');
 
   document.execCommand("defaultParagraphSeparator", false, "p");
 
   contenteditable.innerHTML = "abcdef";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
      "nsIPlaintextEditor.insertLineBreak() should create <p> elements when there is only text node and defaultParagraphSeparator is \"p\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"p\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #1');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #1');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "p") #1');
 
   contenteditable.innerHTML = "<p>abcdef</p>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
      "nsIPlaintextEditor.insertLineBreak() should add <p> element after <p> element when defaultParagraphSeparator is \"p\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"p\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #2');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #2');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "p") #2');
 
   contenteditable.innerHTML = "<div>abcdef</div>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
      "nsIPlaintextEditor.insertLineBreak() should add <div> element after <div> element even when defaultParagraphSeparator is \"p\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"p\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #3');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #3');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "p") #3');
 
   contenteditable.innerHTML = "<pre>abcdef</pre>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
      "nsIPlaintextEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is \"p\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"p\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #4');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #4');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "p") #4');
 
   document.execCommand("defaultParagraphSeparator", false, "div");
 
   contenteditable.innerHTML = "abcdef";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
      "nsIPlaintextEditor.insertLineBreak() should create <div> elements when there is only text node and defaultParagraphSeparator is \"div\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"div\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #1');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #1');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "div") #1');
 
   contenteditable.innerHTML = "<p>abcdef</p>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
      "nsIPlaintextEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is \"div\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"div\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #2');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #2');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "div") #2');
 
   contenteditable.innerHTML = "<div>abcdef</div>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
      "nsIPlaintextEditor.insertLineBreak() should add <div> element after <div> element when defaultParagraphSeparator is \"div\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"div\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #3');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #3');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "div") #3');
 
   contenteditable.innerHTML = "<pre>abcdef</pre>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
-  inputEventCount = 0;
+  inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
   contenteditable.removeEventListener("input", onInput);
   is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
      "nsIPlaintextEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is \"div\"");
-  is(inputEventCount, 1,
+  is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"div\"");
+  ok(inputEvents[0] instanceof InputEvent,
+     '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #4');
+  is(inputEvents[0].cancelable, false,
+     '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #4');
+  is(inputEvents[0].bubbles, true,
+     '"input" event should always bubble (when defaultParagraphSeparator is "div") #4');
 
   SimpleTest.finish();
 });
 
 function getPlaintextEditor(aEditorElement) {
   let editor = aEditorElement ? SpecialPowers.wrap(aEditorElement).editor : null;
   if (!editor) {
     editor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
@@ -14,436 +14,557 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
+  inputEvents = [];
   selection.collapse(editor.firstChild, 0);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.deleteTableCell(1) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.deleteTableCell(1) does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().deleteTableCell(1);
     ok(false, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.deleteTableCell(1) causes exception due to no selection range');
   }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   let range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-2</td><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-1");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when only one cell is selected #1-1');
+  checkInputEvent(inputEvents[0], "when only one cell is selected #1-1");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-2");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when only one cell is selected #1-2');
+  checkInputEvent(inputEvents[0], "when only one cell is selected #1-2");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-3");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when only one cell is selected #1-3');
+  checkInputEvent(inputEvents[0], "when only one cell is selected #1-3");
 
   // When only one cell element is selected, the argument should be used.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(2);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
-     "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and next cell element in same row");
+     "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and next cell element in same row #1-4");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when only one cell is selected #1-4');
+  checkInputEvent(inputEvents[0], "when only one cell is selected #1-4");
 
   // When the argument is larger than remaining cell elements from selected
   // cell element, the behavior is really buggy.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(2);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and its previous cell element when it reaches the last cell element in the row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when the argument is larger than remaining cell elements from selected cell element');
+  checkInputEvent(inputEvents[0], "when the argument is larger than remaining cell elements from selected cell element");
 
   // XXX If the former case is expected, first row should be removed in this
   //     case, but it removes only selected cell and its previous cell.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(4);
   todo_is(editor.innerHTML, "<table><tbody>" +
                                "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(4) should remove the first row when a cell in it is selected and the argument is larger than number of cells in the row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when the argument is larger than number of cells in the row');
+  checkInputEvent(inputEvents[0], "when the argument is larger than number of cells in the row");
 
   // If 2 or more cells are selected, the argument should be ignored.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
                        '<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-2</td><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove selected cell elements even if the argument is smaller than number of selected cells");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired even if the argument is smaller than number of selected cells');
+  checkInputEvent(inputEvents[0], "even if the argument is smaller than number of selected cells");
 
   // If all cells in a row are selected, the <tr> element should also be removed.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td id="select3">cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select3"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when all cell elements in a row is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all cell elements in a row is selected');
+  checkInputEvent(inputEvents[0], "when all cell elements in a row is selected");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td></tr>' +
                        "<tr><td>cell2-1</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell2-1</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when a cell element which is only child of a row is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell element which is only child of a row is selected');
+  checkInputEvent(inputEvents[0], "when a cell element which is only child of a row is selected");
 
   // If all cells are removed, the <table> element should be removed.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' +
                        '<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select3"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select4"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableCellContents(1) should remove also <table> element when all cell elements are selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all cell elements are selected');
+  checkInputEvent(inputEvents[0], "when all cell elements are selected");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableCellContents(1) should remove also <table> element when a cell element which is only child of <table> is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell element which is only child of <table> is selected');
+  checkInputEvent(inputEvents[0], "when a cell element which is only child of <table> is selected");
 
   // rowspan
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                        "<tr><td>cell2-2</td></tr>" +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-2</td></tr>" +
                          "<tr><td>cell2-2</td></tr>" +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when removing the cell spanning rows');
+  checkInputEvent(inputEvents[0], "when removing the cell spanning rows");
 
   // XXX cell3-1 is also removed even though it's not selected.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                        "<tr><td>cell2-2</td></tr>" +
                        '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   todo_is(editor.innerHTML, "<table><tbody>" +
                               "<tr><td>cell1-2</td></tr>" +
                               "<tr><td>cell2-2</td></tr>" +
                               "<tr><td>cell3-1</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows (when 2 cell elements are selected)");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when removing the cell spanning rows (when 2 cell elements are selected)');
+  checkInputEvent(inputEvents[0], "when removing the cell spanning rows (when 2 cell elements are selected)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                        '<tr><td id="select">cell2-2</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed");
+  is(inputEvents.length, 1,
+     "Only one \"input\" event should be fired when spanned <tr>'s last child cell element is removed");
+  checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed");
 
   // XXX broken case, the second row isn't removed.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                        '<tr><td id="select1">cell2-2</td></tr>' +
                        '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   todo_is(editor.innerHTML, "<table><tbody>" +
                               '<tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr>' +
                               "<tr><td>cell3-1</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
+  is(inputEvents.length, 1,
+     "Only one \"input\" event should be fired when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
+  checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
 
   // XXX broken case, neither the selected cell nor the second row is removed.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr>' +
                        '<tr><td id="select">cell2-2</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   todo_is(editor.innerHTML, "<table><tbody>" +
                               '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                               "<tr><td>cell3-2</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (removing middle row)");
+  todo_is(inputEvents.length, 1,
+          "Only one \"input\" event should be fired when spanned <tr>'s last child cell element is removed (removing middle row)");
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (removing middle row)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr>' +
                        '<tr><td id="select1">cell2-2</td></tr>' +
                        '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   todo_is(editor.innerHTML, "<table><tbody>" +
                               '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element and remove the last row");
+  is(inputEvents.length, 1,
+     "Only one \"input\" event should be fired when spanned <tr>'s last child cell element and remove the last row");
+  checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element and remove the last row");
 
   // colspan
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when removing the cell spanning columns');
+  checkInputEvent(inputEvents[0], "when removing the cell spanning columns");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1" colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
                        '<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns and the other selected cell element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when removing the cell spanning columns and the other selected cell element');
+  checkInputEvent(inputEvents[0], "when removing the cell spanning columns and the other selected cell element");
 
   // XXX broken case, colspan is not adjusted.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
                        '<tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   todo_is(editor.innerHTML, "<table><tbody>" +
                               '<tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr>' +
                               "<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when corresponding cell element is removed');
+  checkInputEvent(inputEvents[0], "when corresponding cell element is removed");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
                        '<tr><td>cell2-1</td><td id="select1">cell2-2</td><td id="select2">cell2-3</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   todo_is(editor.innerHTML, "<table><tbody>" +
                               '<tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr>' +
                               "<tr><td>cell2-1</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
+  checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select1">cell1-2</td><td>cell1-4</td></tr>' +
                        '<tr><td id="select2" colspan="2">cell2-1</td><td>cell2-3</td><td>cell2-4</td></tr>' +
                      "</table>";
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                               "<tr><td>cell1-1</td><td>cell1-4</td></tr>" +
                               "<tr><td>cell2-3</td><td>cell2-4</td></tr>" +
                             "</tbody></table>",
           "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
+  checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
 
   // When a cell contains first selection range, it should be removed.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
   editor.scrollTop; // requires layout information.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().deleteTableCell(1);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-2</td><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(1) should remove only a cell containing first selection range when there is no selected cell element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when there is no selected cell element');
+  checkInputEvent(inputEvents[0], "when there is no selected cell element");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
   editor.scrollTop; // requires layout information.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().deleteTableCell(2);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-3</td></tr>" +
                          "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents(2) should remove only 2 cell elements starting from a cell containing first selection range when there is no selected cell element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when there is no selected cell element');
+  checkInputEvent(inputEvents[0], "when there is no selected cell element");
+
+  editor.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
@@ -14,86 +14,120 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
   selection.collapse(editor.firstChild, 0);
+  inputEvents = [];
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
-     "nsITableEditor.deleteTableColumn() should do nothing if selection is not in <table>");
+     "nsITableEditor.deleteTableCellContents() should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.deleteTableCellContents() does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().deleteTableCellContents();
     ok(false, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.deleteTableCellContents() causes exception due to no selection range');
   }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td><td>cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                      "</table>";
   editor.focus();
+  inputEvents = [];
   let range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td id="select"><br></td><td>cell1-2</td></tr>' +
                          "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell is selected');
+  checkInputEvent(inputEvents[0], "when all text in a cell is selected");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select"><ul><li>list1</li></ul></td><td>cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                      "</table>";
   editor.focus();
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td id="select"><br></td><td>cell1-2</td></tr>' +
                          "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace the selected cell's <ul> element with <br> element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when <ul> element in a cell is selected');
+  checkInputEvent(inputEvents[0], "when <ul> element in a cell is selected");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1">cell1-1</td><td>cell1-2</td></tr>' +
                        '<tr><td id="select2">cell2-1</td><td>cell2-2</td></tr>' +
                      "</table>";
   editor.focus();
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td id="select1"><br></td><td>cell1-2</td></tr>' +
                          '<tr><td id="select2"><br></td><td>cell2-2</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace the selected 2 cells' text with <br> element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cell elements are selected');
+  checkInputEvent(inputEvents[0], "when 2 cell elements are selected");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' +
                        '<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' +
                      "</table>";
   editor.focus();
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select3"));
@@ -102,64 +136,81 @@ SimpleTest.waitForFocus(function() {
   range.selectNode(document.getElementById("select4"));
   selection.addRange(range);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td id="select1"><br></td><td id="select2"><br></td></tr>' +
                          '<tr><td id="select3"><br></td><td id="select4"><br></td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace the selected 4 cells' text with <br> element");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 4 cell elements are selected');
+  checkInputEvent(inputEvents[0], "when 4 cell elements are selected");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
                        "<tr><td>cell2-2</td></tr>" +
                      "</table>";
   editor.focus();
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td id="select" rowspan="2"><br></td><td>cell1-2</td></tr>' +
                          "<tr><td>cell2-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is row-spanning)");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell element are selected (even if the cell is row-spanning)');
+  checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is row-spanning)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select" colspan="2">cell1-1</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                      "</table>";
   editor.focus();
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                           '<tr><td id="select" colspan="2"><br></td></tr>' +
                           "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                         "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is column-spanning)");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell element are selected (even if the cell is column-spanning)');
+  checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is column-spanning)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td id="select">cell1-1</td><td>cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // Requires layout information.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().deleteTableCellContents();
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td id="select"><br></td><td>cell1-2</td></tr>' +
                          "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.deleteTableCellContents() should replace a cell's text with <br> element when the cell contains selection range");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when the cell contains selection range');
+  checkInputEvent(inputEvents[0], "when the cell contains selection range");
+
+  editor.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
@@ -14,272 +14,379 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
+  inputEvents = [];
   selection.collapse(editor.firstChild, 0);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.deleteTableRow(1) does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().deleteTableRow(1);
     ok(false, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.deleteTableRow(1) causes exception due to no selection range');
   }
 
   // If a cell is selected and the argument is less than number of rows,
   // specified number of rows should be removed starting from the row
   // containing the selected cell.  But if the argument is same or
   // larger than actual number of rows when a cell in the first row is
   // selected, the <table> should be removed.
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   let range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the first row is selected');
+  checkInputEvent(inputEvents[0], "when a cell in the first row is selected");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the second row is selected');
+  checkInputEvent(inputEvents[0], "when a cell in the second row is selected");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in first row is selected and argument is same as number of rows');
+  checkInputEvent(inputEvents[0], "when a cell in first row is selected and argument is same as number of rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(3);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when argument is larger than actual number of rows');
+  checkInputEvent(inputEvents[0], "when argument is larger than actual number of rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(2);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(2) should delete the second row containing selected cell and next row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in second row and argument is same as the remaining rows');
+  checkInputEvent(inputEvents[0], "when a cell in second row and argument is same as the remaining rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(3);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(3) should delete the second row (containing selected cell) and the third row even though the argument is larger than the rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in second row and argument is larger than the remaining rows');
+  checkInputEvent(inputEvents[0], "when a cell in second row and argument is larger than the remaining rows");
 
   // Similar to selected a cell, when selection is in a cell, the cell should
   // treated as selected.
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row contains selection range");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the first row contains selection range');
+  checkInputEvent(inputEvents[0], "when a cell in the first row contains selection range");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>';
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row contains selection range");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the second row contains selection range');
+  checkInputEvent(inputEvents[0], "when a cell in the second row contains selection range");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableRow(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell in first row is selected and argument includes next row');
+  checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument includes next row");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableRow(3);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell in first row is selected and argument is same as number of all rows');
+  checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument is same as number of all rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableRow(2);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(2) should delete the second row containing a cell containing selection range and next row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of rows');
+  checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableRow(3);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(3) should delete the second row (containing selection range) and the third row even though the argument is larger than the rows");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell in the second row and argument is larger than renaming number of rows');
+  checkInputEvent(inputEvents[0], "when all text in a cell in the second row and argument is larger than renaming number of rows");
 
   // The argument should be ignored when 2 or more cells are selected.
   // XXX If the argument is less than number of rows and cells in all rows are
   //     selected, only all rows are removed.  However, this leaves empty <table>
   //     element.  Is this expected?
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete all rows if every row's cell is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when cells in every row are selected #1');
+  checkInputEvent(inputEvents[0], "when cells in every row are selected #1");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when cells in every row are selected #2');
+  checkInputEvent(inputEvents[0], "when cells in every row are selected #2");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cells in same row are selected');
+  checkInputEvent(inputEvents[0], "when 2 cells in same row are selected");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete first 2 rows because cells in the both rows are selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cell elements in different rows are selected #1');
+  checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #1");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td id="select2">cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableRow(1) should delete the first and the last rows because cells in the both rows are selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cell elements in different rows are selected #2');
+  checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #2");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, '<table><tbody><tr><td valign="top"><br></td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>',
      "nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"2\" should delete the first row and add empty cell to the second row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell is selected and its rowspan is 2');
+  checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 2");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select" rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, '<table><tbody><tr><td rowspan="2" valign="top"><br></td><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>',
      "nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"3\" should delete the first row and add empty cell whose rowspan is 2 to the second row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell is selected and its rowspan is 3');
+  checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 3");
 
   // XXX Must be buggy case.  When removing a row which does not have a cell due
   //     to rowspan, the rowspan is not changed properly.
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td id="select1">cell2-2</td></tr><tr><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>',
      "nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #1');
+  checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #1");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableRow(1);
   is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>',
      "nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #2');
+  checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #2");
+
+  editor.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
@@ -14,214 +14,304 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
+  inputEvents = [];
   selection.collapse(editor.firstChild, 0);
   getTableEditor().insertTableCell(1, false);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.insertTableCell(1, false) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, false) does nothing');
+
+  inputEvents = [];
   getTableEditor().insertTableCell(1, true);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.insertTableCell(1, true) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, true) does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().insertTableCell(1, false);
     ok(false, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.insertTableCell(1, false) causes exception due to no selection range');
   }
   try {
+    inputEvents = [];
     getTableEditor().insertTableCell(1, true);
     ok(false, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range');
   }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                        '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableCell(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                          '<tr><td valign="top"><br></td><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell in middle row (before)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (before)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                        '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableCell(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                          '<tr><td id="select">cell2-1</td><td valign="top"><br></td><td>cell2-2</td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell in middle row (after)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (after)");
+  }
 
   // with rowspan.
 
   // Odd case. This puts the cell containing selection moves right of row-spanning cell.
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' +
                        '<tr><td id="select">cell2-1</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableCell(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' +
                          '<tr><td valign="top"><br></td><td id="select">cell2-1</td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection and moves the cell to right of the row-spanning cell element");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (before)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (before)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
                        '<tr><td id="select">cell2-1</td></tr>' +
                        "<tr><td>cell3-1</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableCell(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
                          '<tr><td id="select">cell2-1</td><td valign="top"><br></td></tr>' +
                          "<tr><td>cell3-1</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection and moves the cell to right of the row-spanning cell element");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (after)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (after)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td></tr>" +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableCell(2, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td valign="top"><br></td><td valign="top"><br></td><td id="select" rowspan="2">cell1-2</td></tr>' +
                          "<tr><td>cell2-1</td></tr>" +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(2, false) should insert 2 cells before the row-spanning cell containing selection");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (before)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (before)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td></tr>" +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableCell(2, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          "<tr><td>cell2-1</td></tr>" +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(2, false) should insert 2 cells after the row-spanning cell containing selection");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (after)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (after)");
+  }
 
   // with colspan
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableCell(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                          '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection but do not modify col-spanning cell");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (before)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (before)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        '<tr><td colspan="3">cell2-1</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableCell(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' +
                          '<tr><td colspan="3">cell2-1</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection but do not modify col-spanning cell");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (after)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (after)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
                        '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableCell(2, false);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(2, false) should insert 2 cells before the col-spanning cell containing selection");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (before)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (before)");
+  }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
                        '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableCell(2, true);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
                          '<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableCell(2, false) should insert 2 cells after the col-spanning cell containing selection");
+  todo_is(inputEvents.length, 1,
+          'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (after)');
+  if (inputEvents.length > 0) {
+    checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (after)");
+  }
+
+  editor.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
@@ -14,133 +14,187 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
+  inputEvents = [];
   selection.collapse(editor.firstChild, 0);
   getTableEditor().insertTableColumn(1, false);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.insertTableColumn(1, false) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, false) does nothing');
+
+  inputEvents = [];
   getTableEditor().insertTableColumn(1, true);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.insertTableColumn(1, true) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, true) does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().insertTableColumn(1, false);
     ok(false, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.insertTableColumn(1, false) causes exception due to no selection range');
   }
   try {
+    inputEvents = [];
     getTableEditor().insertTableColumn(1, true);
     ok(false, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.insertTableColumn(1, true) causes exception due to no selection range');
   }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableColumn(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                          '<tr><td>cell2-1</td><td valign="top"><br></td><td>cell2-2</td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second column (before)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (before)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableColumn(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' +
                          '<tr><td>cell2-1</td><td>cell2-2</td><td valign="top"><br></td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableColumn(1, false) should insert a column to right of the second column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second column (after)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (after)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableColumn(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                          '<tr><td colspan="3">cell2-1</td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column and colspan in the first column should be increased");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
                        '<tr><td colspan="3">cell2-1</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableColumn(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' +
                          '<tr><td colspan="4">cell2-1</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableColumn(1, true) should insert a column to right of the second column and colspan in the first column should be increased");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
                        '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableColumn(2, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableColumn(2, false) should insert 2 columns to left of the first column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (before)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (before)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
                        '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableColumn(2, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td>cell1-2</td><td valign="top"><br></td><td valign="top"><br></td><td>cell1-3</td></tr>' +
                          '<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' +
                        "</tbody></table>",
      "nsITableEditor.insertTableColumn(2, false) should insert 2 columns to right of the second column (i.e., right of the right-most column of the column-spanning cell");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (after)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (after)");
+
+  editor.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
@@ -14,153 +14,207 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
+  inputEvents = [];
   selection.collapse(editor.firstChild, 0);
   getTableEditor().insertTableRow(1, false);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.insertTableRow(1, false) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, false) does nothing');
+
+  inputEvents = [];
   getTableEditor().insertTableRow(1, true);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.insertTableRow(1, true) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, true) does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().insertTableRow(1, false);
     ok(false, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.insertTableRow(1, false) causes exception due to no selection range');
   }
   try {
+    inputEvents = [];
     getTableEditor().insertTableRow(1, true);
     ok(false, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.insertTableRow(1, true) causes exception due to no selection range');
   }
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                        '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableRow(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableRow(1, false) should insert a row above the second row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second row (before)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (before)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                        '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableRow(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                          '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableRow(1, true) should insert a row below the second row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second row (after)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (after)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' +
                        '<tr><td id="select">cell2-1</td></tr>' +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableRow(1, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
                          '<tr><td valign="top"><br></td></tr>' +
                          '<tr><td id="select">cell2-1</td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableRow(1, false) should insert a row above the second row and rowspan in the first row should be increased");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (before)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (before)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
                        '<tr><td id="select">cell2-1</td></tr>' +
                        "<tr><td>cell3-1</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 0);
   getTableEditor().insertTableRow(1, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td rowspan="4">cell1-2</td></tr>' +
                          '<tr><td id="select">cell2-1</td></tr>' +
                          '<tr><td valign="top"><br></td></tr>' +
                          "<tr><td>cell3-1</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableRow(1, true) should insert a row below the second row and rowspan in the first row should be increased");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (after)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (after)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td></tr>" +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableRow(2, false);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
                          "<tr><td>cell2-1</td></tr>" +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableRow(2, false) should insert 2 rows above the first row");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (before)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (before)");
 
   selection.removeAllRanges();
   editor.innerHTML = "<table>" +
                        '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
                        "<tr><td>cell2-1</td></tr>" +
                        "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                      "</table>";
   editor.focus();
   editor.scrollTop; // layout information required.
+  inputEvents = [];
   selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                              document.getElementById("select").firstChild, 1);
   getTableEditor().insertTableRow(2, true);
   is(editor.innerHTML, "<table><tbody>" +
                          '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
                          "<tr><td>cell2-1</td></tr>" +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
                          "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                        "</tbody></table>",
      "nsITableEditor.insertTableRow(2, false) should insert 2 rows below the second row (i.e., below the bottom row of the row-spanning cell");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (after)');
+  checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)");
+
+  editor.removeEventListener("input", onInput);
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_resizers_resizing_elements.html
+++ b/editor/libeditor/tests/test_resizers_resizing_elements.html
@@ -80,35 +80,56 @@ SimpleTest.waitForFocus(async function()
       let promiseSelectionChangeEvent = waitForSelectionChange();
       synthesizeMouseAtCenter(target, {});
       await promiseSelectionChangeEvent;
 
       // Determine which resizer we're dealing with.
       let basePosX = rect.width * baseX;
       let basePosY = rect.height * baseY;
 
+      let inputEventExpected = true;
+      function onInput(aEvent) {
+        if (!inputEventExpected) {
+          ok(false, "\"input\" event shouldn't be fired after stopping resizing");
+          return;
+        }
+        ok(aEvent instanceof InputEvent,
+           '"input" event should be dispatched with InputEvent interface');
+        is(aEvent.cancelable, false,
+           '"input" event should be never cancelable');
+        is(aEvent.bubbles, true,
+           '"input" event should always bubble');
+      }
+
+      content.addEventListener("input", onInput);
+
       // Click on the correct resizer
       synthesizeMouse(target, basePosX, basePosY, {type: "mousedown"});
       // Drag it delta pixels to the right and bottom (or maybe left and top!)
       synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"});
       // Release the mouse button
       synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mouseup"});
+
+      inputEventExpected = false;
+
       // Move the mouse delta more pixels to the same direction to make sure that the
       // resize operation has stopped.
       synthesizeMouse(target, basePosX + deltaX * 2, basePosY + deltaY * 2, {type: "mousemove"});
       // Click outside of the editor to hide the resizers
       synthesizeMouseAtCenter(outOfEditor, {});
 
       // Get the new dimensions for the target
       // XXX I don't know why we need 2px margin to check this on Android.
       //     Fortunately, this test checks whether objects are resizable
       //     actually.  So, bigger difference is okay.
       let newRect = target.getBoundingClientRect();
       isfuzzy(newRect.width, rect.width + expectedDeltaX, 2, description + "The width should be increased by " + expectedDeltaX + " pixels");
       isfuzzy(newRect.height, rect.height + expectedDeltaY, 2, description + "The height should be increased by " + expectedDeltaY + "pixels");
+
+      content.removeEventListener("input", onInput);
     }
 
     // Account for changes in the resizing behavior when we're trying to preserve
     // the aspect ration of image.
     // ignoredGrowth means we don't change the size of a dimension because otherwise
     // the aspect ratio would change undesirably.
     // needlessGrowth means that we change the size of a dimension perpendecular to
     // the mouse movement axis in order to preserve the aspect ratio.
--- a/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
+++ b/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
@@ -16,34 +16,66 @@ SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(() => {
   let textarea = document.getElementById("textarea");
   let editor = SpecialPowers.wrap(textarea).editor;
 
   let inlineSpellChecker = editor.getInlineSpellChecker(true);
 
   textarea.focus();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"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 inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+
   SpecialPowers.Cu.import(
     "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
   .onSpellCheck(textarea, () => {
     SimpleTest.executeSoon(() => {
+      textarea.addEventListener("input", onInput);
+
       let misspelledWord = inlineSpellChecker.getMisspelledWord(editor.rootElement.firstChild, 5);
       is(misspelledWord.startOffset, 4,
          "Misspelled word should start from 4");
       is(misspelledWord.endOffset, 7,
          "Misspelled word should end at 7");
+      inputEvents = [];
       inlineSpellChecker.replaceWord(editor.rootElement.firstChild, 5, "aux");
       is(textarea.value, "abc aux abc",
          "'abx' should be replaced with 'aux'");
+      is(inputEvents.length, 1,
+         'Only one "input" event should be fired when replacing a word with spellchecker');
+      checkInputEvent(inputEvents[0], "when replacing a word with spellchecker");
+
+      inputEvents = [];
       synthesizeKey("z", { accelKey: true });
       is(textarea.value, "abc abx abc",
          "'abx' should be restored by undo");
+      is(inputEvents.length, 1,
+         'Only one "input" event should be fired when undoing the replacing word');
+      checkInputEvent(inputEvents[0], "when undoing the replacing word");
+
+      inputEvents = [];
       synthesizeKey("z", { accelKey: true, shiftKey: true });
       is(textarea.value, "abc aux abc",
          "'aux' should be restored by redo");
+      is(inputEvents.length, 1,
+         'Only one "input" event should be fired when redoing the replacing word');
+      checkInputEvent(inputEvents[0], "when redoing the replacing word");
+
+      textarea.removeEventListener("input", onInput);
 
       SimpleTest.finish();
     });
   });
 });
 </script>
 </body>
 </html>
--- a/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
+++ b/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
@@ -24,44 +24,96 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script class="testbody" type="application/javascript">
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editableElements = [
     document.getElementById("input"),
     document.getElementById("textarea"),
   ];
   for (let editableElement of editableElements) {
+    function checkInputEvent(aEvent, aDescription) {
+      ok(aEvent instanceof InputEvent,
+         `"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 inputEvents = [];
+    function onInput(aEvent) {
+      inputEvents.push(aEvent);
+    }
+    editableElement.addEventListener("input", onInput);
+
     editableElement.focus();
+
+    inputEvents = [];
     synthesizeKey("a");
+    is(inputEvents.length, 1,
+       `Only one "input" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+
+    inputEvents = [];
     synthesizeKey("c");
+    is(inputEvents.length, 1,
+       `Only one "input" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+
+    inputEvents = [];
     synthesizeKey("KEY_ArrowLeft");
+    is(inputEvents.length, 0,
+       `No "input" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`);
+
+    inputEvents = [];
     synthesizeKey("b");
+    is(inputEvents.length, 1,
+       `Only one "input" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+
     let editor = SpecialPowers.wrap(editableElement).editor;
     let transactionManager = editor.transactionManager;
     is(transactionManager.numberOfUndoItems, 2,
        editableElement.tagName + ": Initially, there should be 2 undo items");
     // Defined as nsITextControlElement::DEFAULT_UNDO_CAP
     is(transactionManager.maxTransactionCount, 1000,
        editableElement.tagName + ": Initially, transaction manager should be able to have 1,000 undo items");
+
+    inputEvents = [];
     editableElement.value = "def";
+    is(inputEvents.length, 0,
+       `No "input" event should be fired when setting value of <${editableElement.tagName.toLowerCase()}> element`);
+
     is(transactionManager.numberOfUndoItems, 0,
        editableElement.tagName + ": After setting value, all undo items must be deleted");
     is(transactionManager.maxTransactionCount, 1000,
        editableElement.tagName + ": After setting value, maximum transaction count should be restored to the previous value");
+
+    inputEvents = [];
     synthesizeKey("a");
+    is(inputEvents.length, 1,
+       `Only one "input" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+
+    inputEvents = [];
     synthesizeKey("z", { accelKey: true });
     is(editableElement.value, "def",
        editableElement.tagName + ": undo should work after setting value");
+    is(inputEvents.length, 1,
+       `Only one "input" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], `when undoing on <${editableElement.tagName.toLowerCase()}> element`);
 
     // Disable undo/redo.
     editor.enableUndo(0);
     is(transactionManager.maxTransactionCount, 0,
        editableElement.tagName + ": Transaction manager should not be able to have undo items");
     editableElement.value = "hij";
     is(transactionManager.maxTransactionCount, 0,
        editableElement.tagName + ": Transaction manager should not be able to have undo items after setting value");
+
+    editableElement.removeEventListener("input", onInput);
   }
   SimpleTest.finish();
 });
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/components/satchel/test/test_form_autocomplete.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete.html
@@ -988,18 +988,20 @@ function runTest() { // eslint-disable-l
       restoreForm();
       synthesizeKey("KEY_ArrowDown");
       break;
 
     // Check that the input event is fired.
     case 500:
       input.addEventListener("input", function(event) {
         ok(true, testNum + " oninput should have been received");
+        ok(event instanceof InputEvent,
+           testNum + " input event should be dispatched with InputEvent interface");
         ok(event.bubbles, testNum + " input event should bubble");
-        ok(event.cancelable, testNum + " input event should be cancelable");
+        ok(!event.cancelable, testNum + " input event shouldn't be cancelable");
       }, {once: true});
 
       synthesizeKey("KEY_ArrowDown");
       checkForm("");
       synthesizeKey("KEY_Enter");
       checkForm("value1");
       testNum = 599;
       setTimeout(runTest, 100);
--- a/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
@@ -457,18 +457,20 @@ function runTest() {
       restoreForm();
       synthesizeKey("KEY_ArrowDown");
       break;
 
     case 400:
       // Check that the input event is fired.
       input.addEventListener("input", function(event) {
         ok(true, "oninput should have been received");
+        ok(event instanceof InputEvent,
+           "input event should be dispatched with InputEvent interface");
         ok(event.bubbles, "input event should bubble");
-        ok(event.cancelable, "input event should be cancelable");
+        ok(!event.cancelable, "input event should be cancelable");
         checkForm("Google");
         input.blur();
         SimpleTest.finish();
       }, {once: true});
 
       synthesizeKey("KEY_ArrowDown");
       checkForm("");
       synthesizeKey("KEY_Enter");
--- a/toolkit/components/satchel/test/test_submit_on_keydown_enter.html
+++ b/toolkit/components/satchel/test/test_submit_on_keydown_enter.html
@@ -27,19 +27,25 @@ var expectedValue = "value1";
 
 function handleSubmit() { // eslint-disable-line no-unused-vars
   info("Submit");
   ok(false, "The form should not be submitted");
   input.removeEventListener("input", handleInput, true);
   SimpleTest.finish();
 }
 
-function handleInput() {
+function handleInput(aEvent) {
   info("Input");
   is(input.value, expectedValue, "Check input value");
+  todo(aEvent instanceof InputEvent,
+       '"input" event should be dispatched with InputEvent interface');
+  todo_is(aEvent.cancelable, false,
+          '"input" event should be never cancelable');
+  is(aEvent.bubbles, true,
+     '"input" event should always bubble');
   input.removeEventListener("input", handleInput, true);
   SimpleTest.finish();
 }
 
 function runTest() {
   input.addEventListener("input", handleInput, true);
   input.addEventListener("keydown", function handleEnterDown(e) {
     if (e.keyCode != KeyEvent.DOM_VK_RETURN) {
--- a/toolkit/content/tests/chrome/file_editor_with_autocomplete.js
+++ b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js
@@ -16,23 +16,25 @@ async function waitForCondition(conditio
   });
 }
 
 function nsDoTestsForEditorWithAutoComplete(aDescription,
                                                   aWindow,
                                                   aTarget,
                                                   aAutoCompleteController,
                                                   aIsFunc,
+                                                  aTodoIsFunc,
                                                   aGetTargetValueFunc) {
   this._description = aDescription;
   this._window = aWindow;
   this._target = aTarget;
   this._controller = aAutoCompleteController;
 
   this._is = aIsFunc;
+  this._todo_is = aTodoIsFunc;
   this._getTargetValue = aGetTargetValueFunc;
 
   this._target.focus();
 
   this._DefaultCompleteDefaultIndex =
     this._controller.input.completeDefaultIndex;
 }
 
@@ -47,331 +49,461 @@ nsDoTestsForEditorWithAutoComplete.proto
   _getTargetValue() { return "not initialized"; },
 
   run: async function runTestsImpl() {
     for (let test of this._tests) {
       if (this._controller.input.completeDefaultIndex != test.completeDefaultIndex) {
         this._controller.input.completeDefaultIndex = test.completeDefaultIndex;
       }
 
+      let inputEvents = [];
+      function onInput(aEvent) {
+        inputEvents.push(aEvent);
+      }
+      this._target.addEventListener("input", onInput);
+
       if (test.execute(this._window, this._target) === false) {
+        this._target.removeEventListener("input", onInput);
         continue;
       }
 
       await waitForCondition(() => {
         return this._controller.searchStatus >=
                Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
       });
-      this._checkResult(test);
+      this._target.removeEventListener("input", onInput);
+      this._checkResult(test, inputEvents);
     }
     this._controller.input.completeDefaultIndex = this._DefaultCompleteDefaultIndex;
   },
 
-  _checkResult(aTest) {
+  _checkResult(aTest, aInputEvents) {
     this._is(this._getTargetValue(), aTest.value,
              this._description + ", " + aTest.description + ": value");
     this._is(this._controller.searchString, aTest.searchString,
              this._description + ", " + aTest.description + ": searchString");
     this._is(this._controller.input.popupOpen, aTest.popup,
              this._description + ", " + aTest.description + ": popupOpen");
     this._is(this._controller.searchStatus, Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH,
              this._description + ", " + aTest.description + ": status");
+    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');
+        this._is(aInputEvents[i].cancelable, false,
+                 this._description + ", " + aTest.description + ': "input" event should be never cancelable');
+      } else if (aTest.inputEvents[i].inputType === "insertReplacementText") {
+        this._todo_is(aInputEvents[i] instanceof this._window.InputEvent, true,
+                      this._description + ", " + aTest.description + ': "input" event should be dispatched with InputEvent interface');
+        this._todo_is(aInputEvents[i].cancelable, false,
+                      this._description + ", " + aTest.description + ': "input" event should be never cancelable');
+      } 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].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: [
     { description: "Undo/Redo behavior check when typed text exactly matches the case: type 'Mo'",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("M", { shiftKey: true }, aWindow);
         synthesizeKey("o", {}, aWindow);
         return true;
       }, popup: true, value: "Mo", searchString: "Mo",
+      inputEvents: [
+        {inputType: "insertText"},
+        {inputType: "insertText"},
+      ],
     },
     { 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},
+      ],
     },
     { 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",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case: undo the typed text",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case: redo the typed text",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mo", searchString: "Mo",
+      inputEvents: [
+        {inputType: "historyRedo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case: redo the word",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mozilla", searchString: "Mozilla",
+      inputEvents: [
+        {inputType: "historyRedo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case: removing all text for next test...",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("a", { accelKey: true }, aWindow);
         synthesizeKey("KEY_Backspace", {}, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "deleteContentBackward"},
+      ],
     },
 
     { description: "Undo/Redo behavior check when typed text does not match the case: type 'mo'",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("m", {}, aWindow);
         synthesizeKey("o", {}, aWindow);
         return true;
       }, popup: true, value: "mo", searchString: "mo",
+      inputEvents: [
+        {inputType: "insertText"},
+        {inputType: "insertText"},
+      ],
     },
     { 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},
+      ],
     },
     { 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",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case: undo the typed text",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case: redo the typed text",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "mo", searchString: "mo",
+      inputEvents: [
+        {inputType: "historyRedo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case: redo the word",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mozilla", searchString: "Mozilla",
+      inputEvents: [
+        {inputType: "historyRedo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case: removing all text for next test...",
       completeDefaultIndex: false,
       execute(aWindow, aTarget) {
         synthesizeKey("a", { accelKey: true }, aWindow);
         synthesizeKey("KEY_Backspace", {}, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "deleteContentBackward"},
+      ],
     },
 
     // Testing for nsIAutoCompleteInput.completeDefaultIndex being true.
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): type 'Mo'",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("M", { shiftKey: true }, aWindow);
         synthesizeKey("o", {}, aWindow);
         return true;
       }, popup: true, value: "Mozilla", searchString: "Mo",
+      inputEvents: [
+        {inputType: "insertText"},
+        {inputType: "insertText"},
+        {inputType: "insertReplacementText"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): select 'Mozilla' to complete the word",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("KEY_ArrowDown", {}, aWindow);
         synthesizeKey("KEY_Enter", {}, aWindow);
         return true;
       }, popup: false, value: "Mozilla", searchString: "Mozilla",
+      inputEvents: [
+        {inputType: "insertReplacementText"}, // TODO: We don't need to dispatch "input" event int this case because of not changing the value.
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mo", searchString: "Mo",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the typed text",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the typed text",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mozilla", searchString: "Mo",
+      inputEvents: [
+        {inputType: "historyRedo"},
+        {inputType: "insertReplacementText"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the word",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "Mozilla", searchString: "Mo",
+      inputEvents: [
+      ],
     },
     { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): removing all text for next test...",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("a", { accelKey: true }, aWindow);
         synthesizeKey("KEY_Backspace", {}, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "deleteContentBackward"},
+      ],
     },
 
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): type 'mo'",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("m", {}, aWindow);
         synthesizeKey("o", {}, aWindow);
         return true;
       }, popup: true, value: "mozilla", searchString: "mo",
+      inputEvents: [
+        {inputType: "insertText"},
+        {inputType: "insertText"},
+        {inputType: "insertReplacementText"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): select 'Mozilla' to complete the word",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("KEY_ArrowDown", {}, aWindow);
         synthesizeKey("KEY_Enter", {}, aWindow);
         return true;
       }, popup: false, value: "Mozilla", searchString: "Mozilla",
+      inputEvents: [
+        {inputType: "insertReplacementText"},
+      ],
     },
     // Different from "exactly matches the case" case, modifying the case causes one additional transaction.
     // Although we could make this transaction ignored.
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the selected word, but typed text shouldn't be canceled",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: true, value: "mozilla", searchString: "mozilla",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: true, value: "mo", searchString: "mo",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the typed text",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("z", { accelKey: true }, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "historyUndo"},
+      ],
     },
     // XXX This is odd case.  Consistency with undo behavior, this should restore "mo".
     //     However, looks like that autocomplete automatically restores "mozilla".
     //     Additionally, looks like that it causes clearing the redo stack.
     //     Therefore, the following redo operations do nothing.
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the typed text",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "mozilla", searchString: "mo",
+      inputEvents: [
+        {inputType: "historyRedo"},
+        {inputType: "insertReplacementText"},
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the default index word",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "mozilla", searchString: "mo",
+      inputEvents: [
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the word",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
         return true;
       }, popup: true, value: "mozilla", searchString: "mo",
+      inputEvents: [
+      ],
     },
     { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): removing all text for next test...",
       completeDefaultIndex: true,
       execute(aWindow, aTarget) {
         // Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
         if (aTarget.tagName === "textbox") {
           return false;
         }
         synthesizeKey("a", { accelKey: true }, aWindow);
         synthesizeKey("KEY_Backspace", {}, aWindow);
         return true;
       }, popup: false, value: "", searchString: "",
+      inputEvents: [
+        {inputType: "deleteContentBackward"},
+      ],
     },
   ],
 };
--- a/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html
+++ b/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html
@@ -46,24 +46,24 @@ async function runTests() {
   SpecialPowers.attachFormFillControllerTo(window);
   var target = document.getElementById("input");
 
   // Register a word to the form history.
   await registerWord(target, formFillController.controller);
 
   let tests1 = new nsDoTestsForEditorWithAutoComplete(
     "Testing on HTML input (asynchronously search)",
-    window, target, formFillController.controller, is,
+    window, target, formFillController.controller, is, todo_is,
     function() { return target.value; });
   await tests1.run();
 
   target.setAttribute("timeout", 0);
   let tests2 = new nsDoTestsForEditorWithAutoComplete(
         "Testing on HTML input (synchronously search)",
-        window, target, formFillController.controller, is,
+        window, target, formFillController.controller, is, todo_is,
         function() { return target.value; });
   await tests2.run();
 
   formFillController.timeout = originalFormFillTimeout;
   SpecialPowers.detachFormFillControllerFrom(window);
   SimpleTest.finish();
 }
 
--- a/toolkit/content/tests/chrome/test_editor_for_textbox_with_autocomplete.xul
+++ b/toolkit/content/tests/chrome/test_editor_for_textbox_with_autocomplete.xul
@@ -97,24 +97,24 @@ componentManager.registerFactory(autoCom
 
 async function runTests()
 {
   var target = document.getElementById("textbox");
 
   target.setAttribute("timeout", 1);
   let tests1 = new nsDoTestsForEditorWithAutoComplete(
     "Testing on XUL textbox (asynchronously search)",
-    window, target, target.controller, is,
+    window, target, target.controller, is, todo_is,
     function() { return target.value; });
   await tests1.run();
 
   target.setAttribute("timeout", 0);
   let tests2 = new nsDoTestsForEditorWithAutoComplete(
         "Testing on XUL textbox (synchronously search)",
-        window, target, target.controller, is,
+        window, target, target.controller, is, todo_is,
         function() { return target.value; });
   await tests2.run();
 
   // Unregister the factory so that we don't get in the way of other
   // tests
   componentManager.unregisterFactory(autoCompleteSimpleID,
                                      autoCompleteSimple);
   SimpleTest.finish();
--- a/widget/tests/window_composition_text_querycontent.xul
+++ b/widget/tests/window_composition_text_querycontent.xul
@@ -638,29 +638,39 @@ function runUndoRedoTest()
 
   if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
                     "runUndoRedoTest", "#" + ++i) ||
       !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
     return;
   }
 }
 
+function checkInputEvent(aEvent, aIsComposing, aDescription) {
+  if (aEvent.type != "input") {
+    return;
+  }
+  ok(aEvent instanceof InputEvent, `"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}`);
+  is(aEvent.isComposing, aIsComposing, `isComposing should be ${aIsComposing}: ${aDescription}`);
+}
+
 function runCompositionCommitAsIsTest()
 {
   textarea.focus();
 
-  var result = {};
+  var result = [];
   function clearResult()
   {
-    result = { compositionupdate: false, compositionend: false, text: false, input: false }
+    result = [];
   }
 
   function handler(aEvent)
   {
-    result[aEvent.type] = true;
+    result.push(aEvent);
   }
 
   textarea.addEventListener("compositionupdate", handler, true);
   textarea.addEventListener("compositionend", handler, true);
   textarea.addEventListener("input", handler, true);
   textarea.addEventListener("text", handler, true);
 
   // compositioncommitasis with composing string.
@@ -676,20 +686,26 @@ function runCompositionCommitAsIsTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "a" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #1");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } });
 
-  is(result.compositionupdate, false, "runCompositionCommitAsIsTest: compositionupdate shouldn't be fired after dispatching compositioncommitasis #1");
-  is(result.compositionend, true, "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1");
-  is(result.text, true, "runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
-  is(result.input, true, "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1");
+  is(result.length, 3,
+     "runCompositionCommitAsIsTest: 3 events should be fired after dispatching compositioncommitasis #1");
+  is(result[0].type, "text",
+     "runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
+  is(result[1].type, "compositionend",
+     "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1");
+  is(result[2].type, "input",
+     "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1");
+  checkInputEvent(result[2], false,
+                  "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
   is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #1");
 
   // compositioncommitasis with committed string.
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
         "clauses":
@@ -712,20 +728,24 @@ function runCompositionCommitAsIsTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "KEY_Enter", type: "keydown" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter", type: "keyup" } });
 
-  is(result.compositionupdate, false, "runCompositionCommitAsIsTest: compositionupdate shouldn't be fired after dispatching compositioncommitasis #2");
-  is(result.compositionend, true, "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #2");
-  is(result.text, false, "runCompositionCommitAsIsTest: text shouldn't be fired after dispatching compositioncommitasis because it's dispatched when there is already committed string #2");
-  is(result.input, true, "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #2");
+  is(result.length, 2,
+     "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #2");
+  is(result[0].type, "compositionend",
+     "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #2");
+  is(result[1].type, "input",
+     "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #2");
+  checkInputEvent(result[1], false,
+                  "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #2");
   is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");
 
   // compositioncommitasis with committed string.
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
         "clauses":
@@ -748,41 +768,45 @@ function runCompositionCommitAsIsTest()
       "caret": { "start": 0, "length": 0 },
       "key": { key: "KEY_Escape", type: "keydown" },
     });
   is(textarea.value, "", "runCompositionCommitAsIsTest: textarea has non-empty composition string #3");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape", type: "keyup" } });
 
-  is(result.compositionupdate, false, "runCompositionCommitAsIsTest: compositionupdate shouldn't be fired after dispatching compositioncommitasis #3");
-  is(result.compositionend, true, "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #3");
-  is(result.text, false, "runCompositionCommitAsIsTest: text shouldn't be fired after dispatching compositioncommitasis because it's dispatched when there is empty composition string #3");
-  is(result.input, true, "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #3");
+  is(result.length, 2,
+     "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #3");
+  is(result[0].type, "compositionend",
+     "runCompositionCommitAsIsTest: compositionend shouldn't be fired after dispatching compositioncommitasis #3");
+  is(result[1].type, "input",
+     "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #3");
+  checkInputEvent(result[1], false,
+                  "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #3");
   is(textarea.value, "", "runCompositionCommitAsIsTest: textarea doesn't have committed string #3");
 
   textarea.removeEventListener("compositionupdate", handler, true);
   textarea.removeEventListener("compositionend", handler, true);
   textarea.removeEventListener("input", handler, true);
   textarea.removeEventListener("text", handler, true);
 }
 
 function runCompositionCommitTest()
 {
   textarea.focus();
 
-  var result = {};
+  var result = [];
   function clearResult()
   {
-    result = { compositionupdate: false, compositionend: false, text: false, input: false }
+    result = [];
   }
 
   function handler(aEvent)
   {
-    result[aEvent.type] = true;
+    result.push(aEvent);
   }
 
   textarea.addEventListener("compositionupdate", handler, true);
   textarea.addEventListener("compositionend", handler, true);
   textarea.addEventListener("input", handler, true);
   textarea.addEventListener("text", handler, true);
 
   // compositioncommit with different composing string.
@@ -798,20 +822,28 @@ function runCompositionCommitTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "a", type: "keydown" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #1");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "a", type: "keyup" } });
 
-  is(result.compositionupdate, true, "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #1");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1");
-  is(result.text, true, "runCompositionCommitTest: text should be fired after dispatching compositioncommit because it's dispatched when there is compoing string #1");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1");
+  is(result.length, 4,
+     "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #1");
+  is(result[0].type, "compositionupdate",
+     "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
+  is(result[1].type, "text",
+     "runCompositionCommitTest: text should be fired after dispatching compositioncommit #1");
+  is(result[2].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1");
+  is(result[3].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1");
+  checkInputEvent(result[3], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #1");
   is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #1");
 
   // compositioncommit with different committed string when there is already committed string
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
         "clauses":
@@ -834,20 +866,28 @@ function runCompositionCommitTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "KEY_Enter", type: "keydown" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have committed string #2");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });
 
-  is(result.compositionupdate, true, "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2");
-  is(result.text, true, "runCompositionCommitTest: text should be fired after dispatching compositioncommit #2");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2");
+  is(result.length, 4,
+     "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #2");
+  is(result[0].type, "compositionupdate",
+     "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2");
+  is(result[1].type, "text",
+     "runCompositionCommitTest: text should be fired after dispatching compositioncommit #2");
+  is(result[2].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2");
+  is(result[3].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2");
+  checkInputEvent(result[3], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #2");
   is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #2");
 
   // compositioncommit with empty composition string.
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
         "clauses":
@@ -870,76 +910,92 @@ function runCompositionCommitTest()
       "caret": { "start": 0, "length": 0 },
       "key": { key: "KEY_Enter", type: "keydown" },
     });
   is(textarea.value, "", "runCompositionCommitTest: textarea has non-empty composition string #3");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });
 
-  is(result.compositionupdate, true, "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3");
-  is(result.text, true, "runCompositionCommitTest: text should be fired after dispatching compositioncommit #3");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3");
+  is(result.length, 4,
+     "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #3");
+  is(result[0].type, "compositionupdate",
+     "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3");
+  is(result[1].type, "text",
+     "runCompositionCommitTest: text should be fired after dispatching compositioncommit #3");
+  is(result[2].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3");
+  is(result[3].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3");
+  checkInputEvent(result[3], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #3");
   is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #3");
 
   // inserting empty string with simple composition.
   textarea.value = "abc";
   textarea.setSelectionRange(3, 3);
   synthesizeComposition({ type: "compositionstart" });
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "" });
 
-  is(result.compositionupdate, false,
-     "runCompositionCommitTest: compositionupdate should not be fired when interting empty string with composition");
-  is(result.compositionend, true,
-     "runCompositionCommitTest: compositionend should be fired when interting empty string with composition");
-  is(result.text, true,
-     "runCompositionCommitTest: text should be fired when interting empty string with composition");
-  is(result.input, true,
-     "runCompositionCommitTest: input should be fired when interting empty string with composition");
+  is(result.length, 3,
+     "runCompositionCommitTest: 3 events should be fired when inserting empty string with composition");
+  is(result[0].type, "text",
+     "runCompositionCommitTest: text should be fired when inserting empty string with composition");
+  is(result[1].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired when inserting empty string with composition");
+  is(result[2].type, "input",
+     "runCompositionCommitTest: input should be fired when inserting empty string with composition");
+  checkInputEvent(result[2], false,
+                  "runCompositionCommitTest: when inserting empty string with composition");
   is(textarea.value, "abc",
-     "runCompositionCommitTest: textarea should keep original value when interting empty string with composition");
+     "runCompositionCommitTest: textarea should keep original value when inserting empty string with composition");
 
   // replacing selection with empty string with simple composition.
   textarea.value = "abc";
   textarea.setSelectionRange(0, 3);
   synthesizeComposition({ type: "compositionstart" });
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "" });
 
-  is(result.compositionupdate, false,
-     "runCompositionCommitTest: compositionupdate should not be fired when replacing selection with empty string with composition");
-  is(result.compositionend, true,
-     "runCompositionCommitTest: compositionend should be fired when replacing selection with empty string with composition");
-  is(result.text, true,
-     "runCompositionCommitTest: text should be fired when replacing selection with empty string with composition");
-  is(result.input, true,
-     "runCompositionCommitTest: input should be fired when replacing selection with empty string with composition");
+  is(result.length, 3,
+     "runCompositionCommitTest: 3 events should be fired when replacing with empty string with composition");
+  is(result[0].type, "text",
+     "runCompositionCommitTest: text should be fired when replacing with empty string with composition");
+  is(result[1].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired when replacing with empty string with composition");
+  is(result[2].type, "input",
+     "runCompositionCommitTest: input should be fired when replacing with empty string with composition");
+  checkInputEvent(result[2], false,
+                  "runCompositionCommitTest: when replacing with empty string with composition");
   is(textarea.value, "",
      "runCompositionCommitTest: textarea should become empty when replacing selection with empty string with composition");
 
   // replacing selection with same string with simple composition.
   textarea.value = "abc";
   textarea.setSelectionRange(0, 3);
   synthesizeComposition({ type: "compositionstart" });
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "abc" });
 
-  is(result.compositionupdate, true,
+  is(result.length, 4,
+     "runCompositionCommitTest: 4 events should be fired when replacing selection with same string with composition");
+  is(result[0].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired when replacing selection with same string with composition");
-  is(result.compositionend, true,
+  is(result[1].type, "text",
+     "runCompositionCommitTest: text should be fired when replacing selection with same string with composition");
+  is(result[2].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired when replacing selection with same string with composition");
-  is(result.text, true,
-     "runCompositionCommitTest: text should be fired when replacing selection with same string with composition");
-  is(result.input, true,
+  is(result[3].type, "input",
      "runCompositionCommitTest: input should be fired when replacing selection with same string with composition");
+  checkInputEvent(result[3], false,
+                  "runCompositionCommitTest: when replacing selection with same string with composition");
   is(textarea.value, "abc",
      "runCompositionCommitTest: textarea should keep same value when replacing selection with same string with composition");
 
   // compositioncommit with non-empty composition string.
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
@@ -951,32 +1007,48 @@ function runCompositionCommitTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "a" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #4");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Enter" } });
 
-  is(result.compositionupdate, true, "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4");
-  is(result.text, true, "runCompositionCommitTest: text should be fired after dispatching compositioncommit #4");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4");
+  is(result.length, 4,
+     "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #4");
+  is(result[0].type, "compositionupdate",
+     "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4");
+  is(result[1].type, "text",
+     "runCompositionCommitTest: text should be fired after dispatching compositioncommit #4");
+  is(result[2].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4");
+  is(result[3].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4");
+  checkInputEvent(result[3], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #4");
   is(textarea.value, "", "runCompositionCommitTest: textarea should be empty #4");
 
   // compositioncommit immediately without compositionstart
   textarea.value = "";
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "a" } });
 
-  is(result.compositionupdate, true, "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5");
-  is(result.text, true, "runCompositionCommitTest: text should be fired after dispatching compositioncommit #5");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
+  is(result.length, 4,
+     "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #5");
+  is(result[0].type, "compositionupdate",
+     "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5");
+  is(result[1].type, "text",
+     "runCompositionCommitTest: text should be fired after dispatching compositioncommit #5");
+  is(result[2].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5");
+  is(result[3].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
+  checkInputEvent(result[3], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #5");
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should be empty #5");
 
   // compositioncommit with same composition string.
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
         "clauses":
@@ -987,21 +1059,27 @@ function runCompositionCommitTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "a" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #5");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter" } });
 
-  is(result.compositionupdate, false, "runCompositionCommitTest: compositionupdate shouldn't be fired after dispatching compositioncommit #5");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5");
-  is(result.text, true, "runCompositionCommitTest: text should be fired after dispatching compositioncommit because there was composition string #5");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
-  is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #5");
+  is(result.length, 3,
+     "runCompositionCommitTest: 3 events should be fired after dispatching compositioncommit #6");
+  is(result[0].type, "text",
+     "runCompositionCommitTest: text should be fired after dispatching compositioncommit #6");
+  is(result[1].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6");
+  is(result[2].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6");
+  checkInputEvent(result[2], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #6");
+  is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");
 
   // compositioncommit with same composition string when there is committed string
   textarea.value = "";
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042",
         "clauses":
         [
@@ -1024,20 +1102,24 @@ function runCompositionCommitTest()
       "caret": { "start": 1, "length": 0 },
       "key": { key: "KEY_Enter", type: "keydown" },
     });
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter", type: "keyup" } });
 
-  is(result.compositionupdate, false, "runCompositionCommitTest: compositionupdate shouldn't be fired after dispatching compositioncommit #6");
-  is(result.compositionend, true, "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6");
-  is(result.text, false, "runCompositionCommitTest: text shouldn't be fired after dispatching compositioncommit because there was already committed string #6");
-  is(result.input, true, "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6");
+  is(result.length, 2,
+     "runCompositionCommitTest: 2 events should be fired after dispatching compositioncommit #7");
+  is(result[0].type, "compositionend",
+     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #7");
+  is(result[1].type, "input",
+     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #7");
+  checkInputEvent(result[1], false,
+                  "runCompositionCommitTest: after dispatching compositioncommit #7");
   is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");
 
   textarea.removeEventListener("compositionupdate", handler, true);
   textarea.removeEventListener("compositionend", handler, true);
   textarea.removeEventListener("input", handler, true);
   textarea.removeEventListener("text", handler, true);
 }
 
@@ -4967,28 +5049,30 @@ function runForceCommitTest()
   is(events[0].type, "compositionstart",
      "runForceCommitTest: the 1st event must be compositionstart #1");
   is(events[1].type, "compositionupdate",
      "runForceCommitTest: the 2nd event must be compositionupdate #1");
   is(events[2].type, "text",
      "runForceCommitTest: the 3rd event must be text #1");
   is(events[3].type, "input",
      "runForceCommitTest: the 4th event must be input #1");
+  checkInputEvent(events[3], true, "runForceCommitTest #1");
 
   events = [];
   synthesizeMouseAtCenter(textarea, {});
 
   is(events.length, 3,
      "runForceCommitTest: wrong event count #2");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #2");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #2");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #2");
+  checkInputEvent(events[2], false, "runForceCommitTest #2");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #2");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #2");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #2");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #2");
@@ -5019,16 +5103,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #3");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #3");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #3");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #3");
+  checkInputEvent(events[2], false, "runForceCommitTest #3");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #3");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #3");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #3");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #3");
@@ -5062,16 +5147,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #4");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #4");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #4");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #4");
+  checkInputEvent(events[2], false, "runForceCommitTest #4");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #4");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #4");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #4");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #4");
@@ -5102,16 +5188,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #5");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #5");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #5");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #5");
+  checkInputEvent(events[2], false, "runForceCommitTest #5");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #5");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #5");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #5");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #5");
@@ -5146,16 +5233,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #6");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #6");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #6");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #6");
+  checkInputEvent(events[2], false, "runForceCommitTest #6");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #6");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #6");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #6");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #6");
@@ -5190,16 +5278,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #7");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #7");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #7");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #7");
+  checkInputEvent(events[2], false, "runForceCommitTest #7");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #7");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #7");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #7");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #7");
@@ -5235,16 +5324,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #8");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #8");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #8");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #8");
+  checkInputEvent(events[2], false, "runForceCommitTest #8");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #8");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #8");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #8");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #8");
@@ -5279,16 +5369,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #9");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #9");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #9");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #9");
+  checkInputEvent(events[2], false, "runForceCommitTest #9");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #9");
   is(events[0].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 1st event was fired on wrong event target #9");
   is(events[1].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 2nd event was fired on wrong event target #9");
   is(events[2].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 3rd event was fired on wrong event target #9");
@@ -5321,16 +5412,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #10");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #10");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #10");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #10");
+  checkInputEvent(events[2], false, "runForceCommitTest #10");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #10");
   is(events[0].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 1st event was fired on wrong event target #10");
   is(events[1].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 2nd event was fired on wrong event target #10");
   is(events[2].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 3rd event was fired on wrong event target #10");
@@ -5368,16 +5460,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #11");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #11");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #11");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #11");
+  checkInputEvent(events[2], false, "runForceCommitTest #11");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #11");
   is(events[0].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 1st event was fired on wrong event target #11");
   is(events[1].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 2nd event was fired on wrong event target #11");
   is(events[2].target, iframe2.contentDocument.body,
      "runForceCommitTest: The 3rd event was fired on wrong event target #11");
@@ -5411,16 +5504,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #12");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #12");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #12");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #12");
+  checkInputEvent(events[2], false, "runForceCommitTest #12");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #12");
   is(events[0].target, input,
      "runForceCommitTest: The 1st event was fired on wrong event target #12");
   is(events[1].target, input,
      "runForceCommitTest: The 2nd event was fired on wrong event target #12");
   is(events[2].target, input,
      "runForceCommitTest: The 3rd event was fired on wrong event target #12");
@@ -5449,16 +5543,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #13");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #13");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #13");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #13");
+  checkInputEvent(events[2], false, "runForceCommitTest #13");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #13");
   is(events[0].target, textarea,
      "runForceCommitTest: The 1st event was fired on wrong event target #13");
   is(events[1].target, textarea,
      "runForceCommitTest: The 2nd event was fired on wrong event target #13");
   is(events[2].target, textarea,
      "runForceCommitTest: The 3rd event was fired on wrong event target #13");
@@ -5487,16 +5582,17 @@ function runForceCommitTest()
   is(events.length, 3,
      "runForceCommitTest: wrong event count #14");
   is(events[0].type, "text",
      "runForceCommitTest: the 1st event must be text #14");
   is(events[1].type, "compositionend",
      "runForceCommitTest: the 2nd event must be compositionend #14");
   is(events[2].type, "input",
      "runForceCommitTest: the 3rd event must be input #14");
+  checkInputEvent(events[2], false, "runForceCommitTest #14");
   is(events[1].data, "\u306E",
      "runForceCommitTest: compositionend has wrong data #14");
   is(events[0].target, input,
      "runForceCommitTest: The 1st event was fired on wrong event target #14");
   is(events[1].target, input,
      "runForceCommitTest: The 2nd event was fired on wrong event target #14");
   is(events[2].target, input,
      "runForceCommitTest: The 3rd event was fired on wrong event target #14");
@@ -5599,16 +5695,17 @@ function runNestedSettingValue()
   is(events.length, 3,
      "runNestedSettingValue: wrong event count #1");
   is(events[0].type, "text",
      "runNestedSettingValue: the 1st event must be text #1");
   is(events[1].type, "compositionend",
      "runNestedSettingValue: the 2nd event must be compositionend #1");
   is(events[2].type, "input",
      "runNestedSettingValue: the 3rd event must be input #1");
+  checkInputEvent(events[2], false, "runNestedSettingValue #1");
   is(events[1].data, "\u306E",
      "runNestedSettingValue: compositionend has wrong data #1");
   is(events[0].target, textarea,
      "runNestedSettingValue: The 1st event was fired on wrong event target #1");
   is(events[1].target, textarea,
      "runNestedSettingValue: The 2nd event was fired on wrong event target #1");
   is(events[2].target, textarea,
      "runNestedSettingValue: The 3rd event was fired on wrong event target #1");
@@ -5639,16 +5736,17 @@ function runNestedSettingValue()
   is(events.length, 3,
      "runNestedSettingValue: wrong event count #2");
   is(events[0].type, "text",
      "runNestedSettingValue: the 1st event must be text #2");
   is(events[1].type, "compositionend",
      "runNestedSettingValue: the 2nd event must be compositionend #2");
   is(events[2].type, "input",
      "runNestedSettingValue: the 3rd event must be input #2");
+  checkInputEvent(events[2], false, "runNestedSettingValue #2");
   is(events[1].data, "\u306E",
      "runNestedSettingValue: compositionend has wrong data #2");
   is(events[0].target, input,
      "runNestedSettingValue: The 1st event was fired on wrong event target #2");
   is(events[1].target, input,
      "runNestedSettingValue: The 2nd event was fired on wrong event target #2");
   is(events[2].target, input,
      "runNestedSettingValue: The 3rd event was fired on wrong event target #2");
@@ -5679,16 +5777,17 @@ function runNestedSettingValue()
   is(events.length, 3,
      "runNestedSettingValue: wrong event count #3");
   is(events[0].type, "text",
      "runNestedSettingValue: the 1st event must be text #3");
   is(events[1].type, "compositionend",
      "runNestedSettingValue: the 2nd event must be compositionend #3");
   is(events[2].type, "input",
      "runNestedSettingValue: the 3rd event must be input #3");
+  checkInputEvent(events[2], false, "runNestedSettingValue #3");
   is(events[1].data, "\u306E",
      "runNestedSettingValue: compositionend has wrong data #3");
   is(events[0].target, textarea,
      "runNestedSettingValue: The 1st event was fired on wrong event target #3");
   is(events[1].target, textarea,
      "runNestedSettingValue: The 2nd event was fired on wrong event target #3");
   is(events[2].target, textarea,
      "runNestedSettingValue: The 3rd event was fired on wrong event target #3");
@@ -5719,16 +5818,17 @@ function runNestedSettingValue()
   is(events.length, 3,
      "runNestedSettingValue: wrong event count #4");
   is(events[0].type, "text",
      "runNestedSettingValue: the 1st event must be text #4");
   is(events[1].type, "compositionend",
      "runNestedSettingValue: the 2nd event must be compositionend #4");
   is(events[2].type, "input",
      "runNestedSettingValue: the 3rd event must be input #4");
+  checkInputEvent(events[2], false, "runNestedSettingValue #4");
   is(events[1].data, "\u306E",
      "runNestedSettingValue: compositionend has wrong data #4");
   is(events[0].target, input,
      "runNestedSettingValue: The 1st event was fired on wrong event target #4");
   is(events[1].target, input,
      "runNestedSettingValue: The 2nd event was fired on wrong event target #4");
   is(events[2].target, input,
      "runNestedSettingValue: The 3rd event was fired on wrong event target #4");
@@ -5811,31 +5911,33 @@ function runAsyncForceCommitTest()
   is(events[0].type, "compositionstart",
      "runAsyncForceCommitTest: the 1st event must be compositionstart #1");
   is(events[1].type, "compositionupdate",
      "runAsyncForceCommitTest: the 2nd event must be compositionupdate #1");
   is(events[2].type, "text",
      "runAsyncForceCommitTest: the 3rd event must be text #1");
   is(events[3].type, "input",
      "runAsyncForceCommitTest: the 4th event must be input #1");
+  checkInputEvent(events[3], true, "runAsyncForceCommitTest #1");
 
   events = [];
   commitRequested = false;
   synthesizeMouseAtCenter(textarea, {});
 
   ok(commitRequested,
      "runAsyncForceCommitTest: \"request-to-commit\" should've been notified");
   is(events.length, 3,
      "runAsyncForceCommitTest: wrong event count #2");
   is(events[0].type, "text",
      "runAsyncForceCommitTest: the 1st event must be text #2");
   is(events[1].type, "compositionend",
      "runAsyncForceCommitTest: the 2nd event must be compositionend #2");
   is(events[2].type, "input",
      "runAsyncForceCommitTest: the 3rd event must be input #2");
+  checkInputEvent(events[2], false, "runAsyncForceCommitTest #2");
   is(events[1].data, "\u306E",
      "runAsyncForceCommitTest: compositionend has wrong data #2");
   is(events[0].target, textarea,
      "runAsyncForceCommitTest: The 1st event was fired on wrong event target #2");
   is(events[1].target, textarea,
      "runAsyncForceCommitTest: The 2nd event was fired on wrong event target #2");
   is(events[2].target, textarea,
      "runAsyncForceCommitTest: The 3rd event was fired on wrong event target #2");
@@ -5872,19 +5974,22 @@ function runIsComposingTest()
   var descriptionBase = "runIsComposingTest: ";
   var description = "";
 
   function eventHandler(aEvent)
   {
     if (aEvent.type == "keydown" || aEvent.type == "keyup") {
       is(aEvent.isComposing, expectedIsComposing,
          "runIsComposingTest: " + description + " (type=" + aEvent.type + ", key=" + aEvent.key + ")");
+    } else if (aEvent.type == "keypress") {
+      // keypress event shouldn't be fired during composition so that isComposing should be always false.
+      is(aEvent.isComposing, false,
+         "runIsComposingTest: " + description + " (type=" + aEvent.type + ")");
     } else {
-      is(aEvent.isComposing, expectedIsComposing,
-         "runIsComposingTest: " + description + " (type=" + aEvent.type + ")");
+      checkInputEvent(aEvent, expectedIsComposing, `runIsComposingTest: ${description}`);
     }
   }
 
   function onComposition(aEvent)
   {
     if (aEvent.type == "compositionstart") {
       expectedIsComposing = true;
     } else if (aEvent.type == "compositionend") {
@@ -5945,29 +6050,25 @@ function runIsComposingTest()
 
   textarea.value = "";
 }
 
 function runRedundantChangeTest()
 {
   textarea.focus();
 
-  var result = {};
+  var result = [];
   function clearResult()
   {
-    result = { compositionupdate: false, compositionend: false, text: false, input: false, inputaftercompositionend: false };
+    result = [];
   }
 
   function handler(aEvent)
   {
-    if (aEvent.type == "input" && result.compositionend) {
-      result.inputaftercompositionend = true;
-      return;
-    }
-    result[aEvent.type] = true;
+    result.push(aEvent);
   }
 
   textarea.addEventListener("compositionupdate", handler, true);
   textarea.addEventListener("compositionend", handler, true);
   textarea.addEventListener("input", handler, true);
   textarea.addEventListener("text", handler, true);
 
   textarea.value = "";
@@ -5980,93 +6081,103 @@ function runRedundantChangeTest()
         "clauses":
         [
           { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
         ]
       },
       "caret": { "start": 1, "length": 0 }
     });
 
-  is(result.compositionupdate, true, "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #1");
-  is(result.compositionend, false, "runRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change #1");
-  is(result.text, true, "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #1");
-  is(result.input, true, "runRedundantChangeTest: input should be fired after synthesizing composition change #1");
+  is(result.length, 3,
+     "runRedundantChangeTest: 3 events should be fired after synthesizing composition change #1");
+  is(result[0].type, "compositionupdate",
+     "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #1");
+  is(result[1].type, "text",
+     "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #1");
+  is(result[2].type, "input",
+     "runRedundantChangeTest: input should be fired after synthesizing composition change #1");
+  checkInputEvent(result[2], true,
+                  "runRedundantChangeTest: after synthesizing composition change #1");
   is(textarea.value, "\u3042", "runRedundantChangeTest: textarea has uncommitted string #1");
 
   // synthesize another change event
   clearResult();
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042\u3044",
         "clauses":
         [
           { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
         ]
       },
       "caret": { "start": 2, "length": 0 }
     });
 
-  is(result.compositionupdate, true, "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #2");
-  is(result.compositionend, false, "runRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change #2");
-  is(result.text, true, "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #2");
-  is(result.input, true, "runRedundantChangeTest: input should be fired after synthesizing composition change #2");
+  is(result.length, 3,
+     "runRedundantChangeTest: 3 events should be fired after synthesizing composition change #2");
+  is(result[0].type, "compositionupdate",
+     "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #2");
+  is(result[1].type, "text",
+     "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #2");
+  is(result[2].type, "input",
+     "runRedundantChangeTest: input should be fired after synthesizing composition change #2");
+  checkInputEvent(result[2], true,
+                  "runRedundantChangeTest: after synthesizing composition change #2");
   is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #2");
 
   // synthesize same change event again
   clearResult();
   synthesizeCompositionChange(
     { "composition":
       { "string": "\u3042\u3044",
         "clauses":
         [
           { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
         ]
       },
       "caret": { "start": 2, "length": 0 }
     });
 
-  is(result.compositionupdate, false, "runRedundantChangeTest: compositionupdate shouldn't be fired after synthesizing composition change again");
-  is(result.compositionend, false, "runRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change again");
-  is(result.text, false, "runRedundantChangeTest: text shouldn't be fired after synthesizing composition change again because it's dispatched when there is composing string");
-  is(result.input, false, "runRedundantChangeTest: input shouldn't be fired after synthesizing composition change again");
+  is(result.length, 0, "runRedundantChangeTest: no events should be fired after synthesizing composition change again");
   is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #3");
 
   // synthesize commit-as-is
   clearResult();
   synthesizeComposition({ type: "compositioncommitasis" });
-  is(result.compositionupdate, false, "runRedundantChangeTest: compositionupdate shouldn't be fired after synthesizing composition commit-as-is");
-  is(result.compositionend, true, "runRedundantChangeTest: compositionend should be fired after synthesizing composition commit-as-is");
-  is(result.text, true, "runRedundantChangeTest: text shouldn't be fired after synthesizing composition commit-as-is for removing the ranges");
-  is(result.input, false, "runRedundantChangeTest: input shouldn't be fired before compositionend at synthesizing commit-as-is");
-  is(result.inputaftercompositionend, true, "runRedundantChangeTest: input should be fired after synthesizing composition commit-as-is after compositionend");
+  is(result.length, 3,
+     "runRedundantChangeTest: 3 events be fired after synthesizing composition commit-as-is");
+  is(result[0].type, "text",
+     "runRedundantChangeTest: text shouldn't be fired after synthesizing composition commit-as-is for removing the ranges");
+  is(result[1].type, "compositionend",
+     "runRedundantChangeTest: compositionend should be fired after synthesizing composition commit-as-is");
+  is(result[2].type, "input",
+     "runRedundantChangeTest: input shouldn't be fired before compositionend at synthesizing commit-as-is");
+  checkInputEvent(result[2], false,
+                  "runRedundantChangeTest: at synthesizing commit-as-is");
   is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has the commit string");
 
   textarea.removeEventListener("compositionupdate", handler, true);
   textarea.removeEventListener("compositionend", handler, true);
   textarea.removeEventListener("input", handler, true);
   textarea.removeEventListener("text", handler, true);
 }
 
 function runNotRedundantChangeTest()
 {
   textarea.focus();
 
-  var result = {};
+  var result = [];
   function clearResult()
   {
-    result = { compositionupdate: false, compositionend: false, text: false, input: false, inputaftercompositionend: false };
+    result = [];
   }
 
   function handler(aEvent)
   {
-    if (aEvent.type == "input" && result.compositionend) {
-      result.inputaftercompositionend = true;
-      return;
-    }
-    result[aEvent.type] = true;
+    result.push(aEvent);
   }
 
   textarea.addEventListener("compositionupdate", handler, true);
   textarea.addEventListener("compositionend", handler, true);
   textarea.addEventListener("input", handler, true);
   textarea.addEventListener("text", handler, true);
 
   textarea.value = "abcde";
@@ -6079,102 +6190,135 @@ function runNotRedundantChangeTest()
         "clauses":
         [
           { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
         ]
       },
       "caret": { "start": 5, "length": 0 }
     });
 
-  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges");
-  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with non-null ranges");
-  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges");
-  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges");
+  is(result.length, 3,
+     "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with non-null ranges");
+  is(result[0].type, "compositionupdate",
+     "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges");
+  is(result[1].type, "text",
+     "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges");
+  is(result[2].type, "input",
+     "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges");
+  checkInputEvent(result[2], true,
+                  "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges");
   is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #1");
 
   // synthesize change event with null ranges
   clearResult();
   synthesizeCompositionChange(
     { "composition":
       { "string": "ABCDE",
         "clauses":
         [
           { "length": 0, "attr": 0 }
         ]
       },
     });
-  is(result.compositionupdate, false, "runNotRedundantChangeTest: compositionupdate shouldn't be fired after synthesizing composition change with null ranges after non-null ranges");
-  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with null ranges after non-null ranges");
-  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges");
-  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with null ranges after non-null ranges");
+  is(result.length, 2,
+     "runNotRedundantChangeTest: 2 events should be fired after synthesizing composition change with null ranges after non-null ranges");
+  is(result[0].type, "text",
+     "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges");
+  is(result[1].type, "input",
+     "runNotRedundantChangeTest: input should be fired after synthesizing composition change with null ranges after non-null ranges");
+  checkInputEvent(result[1], true,
+                  "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges");
   is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #2");
 
   // synthesize change event with non-null ranges
   clearResult();
   synthesizeCompositionChange(
     { "composition":
       { "string": "ABCDE",
         "clauses":
         [
           { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
         ]
       },
       "caret": { "start": 5, "length": 0 }
     });
 
-  is(result.compositionupdate, false, "runNotRedundantChangeTest: compositionupdate shouldn't be fired after synthesizing composition change with non-null ranges after null ranges");
-  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with non-null ranges after null ranges");
-  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges");
-  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after null ranges");
+  is(result.length, 2,
+     "runNotRedundantChangeTest: 2 events should be fired after synthesizing composition change with null ranges after non-null ranges");
+  is(result[0].type, "text",
+     "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges");
+  is(result[1].type, "input",
+     "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after null ranges");
+  checkInputEvent(result[1], true,
+                  "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges");
   is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #3");
 
   // synthesize change event with empty data and null ranges
   clearResult();
   synthesizeCompositionChange(
     { "composition":
       { "string": "",
         "clauses":
         [
           { "length": 0, "attr": 0 }
         ]
       },
     });
-  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
-  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
-  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data and null ranges after non-null ranges");
-  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+  is(result.length, 3,
+     "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+  is(result[0].type, "compositionupdate",
+     "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+  is(result[1].type, "text",
+     "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data and null ranges after non-null ranges");
+  is(result[2].type, "input",
+     "runNotRedundantChangeTest: input should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+  checkInputEvent(result[2], true,
+                  "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges");
   is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #1");
 
   // synthesize change event with non-null ranges
   clearResult();
   synthesizeCompositionChange(
     { "composition":
       { "string": "ABCDE",
         "clauses":
         [
           { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
         ]
       },
       "caret": { "start": 5, "length": 0 }
     });
 
-  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
-  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
-  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after empty data and null ranges");
-  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+  is(result.length, 3,
+     "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+  is(result[0].type, "compositionupdate",
+     "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+  is(result[1].type, "text",
+     "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after empty data and null ranges");
+  is(result[2].type, "input",
+     "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+  checkInputEvent(result[2], true,
+                  "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges");
   is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #4");
 
   clearResult();
   synthesizeComposition({ type: "compositioncommit", data: "" });
 
-  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition commit with empty data after non-empty data");
-  is(result.compositionend, true, "runNotRedundantChangeTest: compositionend should be fired after synthesizing composition commit with empty data after non-empty data");
-  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data after non-empty data");
-  is(result.input, false, "runNotRedundantChangeTest: input shouldn't be fired before compositionend after synthesizing composition change with empty data after non-empty data");
-  is(result.inputaftercompositionend, true, "runNotRedundantChangeTest: input should be fired after compositionend after synthesizing composition change with empty data after non-empty data");
+  is(result.length, 4,
+     "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition commit with empty data after non-empty data");
+  is(result[0].type, "compositionupdate",
+     "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition commit with empty data after non-empty data");
+  is(result[1].type, "text",
+     "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data after non-empty data");
+  is(result[2].type, "compositionend",
+     "runNotRedundantChangeTest: compositionend should be fired after synthesizing composition commit with empty data after non-empty data");
+  is(result[3].type, "input",
+     "runNotRedundantChangeTest: input should be fired after compositionend after synthesizing composition change with empty data after non-empty data");
+  checkInputEvent(result[3], false,
+                  "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data");
   is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #2");
 
   textarea.removeEventListener("compositionupdate", handler, true);
   textarea.removeEventListener("compositionend", handler, true);
   textarea.removeEventListener("input", handler, true);
   textarea.removeEventListener("text", handler, true);
 }