Bug 970802 - part 1: Add `beforeinput` event tests into existing mochitests r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Tue, 14 Jan 2020 07:14:50 +0000
changeset 510082 9b29ee6fd8912d1ce80428da27afc686666b9691
parent 510081 b9f97507c65183b7429147f3d7274d44e4821c75
child 510083 648ad637c58e6f0558f769666ddb18d36b550344
push id37014
push usercbrindusan@mozilla.com
push dateTue, 14 Jan 2020 21:43:07 +0000
treeherdermozilla-central@12d8255184b1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs970802
milestone74.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 970802 - part 1: Add `beforeinput` event tests into existing mochitests r=smaug This patch adds a lot of `beforeinput` event tests into existing mochitests which test `input` events. But this does not add tests of canceling `beforeinput` event because it requires really complicated path until implementing `beforeinput` actually. Note that `beforeinput` event is not fired with `Document.execCommand()`. Therefore, this patch does not add WPT for testing `beforeinput` event. And unfortunately, WPT cannot test most cases of the new tests. Differential Revision: https://phabricator.services.mozilla.com/D58123
browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
dom/base/test/chrome/window_nsITextInputProcessor.xhtml
dom/html/test/forms/test_MozEditableElement_setUserInput.html
dom/html/test/forms/test_input_event.html
dom/tests/mochitest/general/test_clipboard_events.html
editor/libeditor/tests/test_abs_positioner_positioning_elements.html
editor/libeditor/tests/test_bug520189.html
editor/libeditor/tests/test_bug596333.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_nsIHTMLEditor_removeInlineProperty.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_deleteTableColumn.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
testing/specialpowers/content/SpecialPowersChild.jsm
toolkit/components/satchel/test/test_form_autocomplete.html
toolkit/components/satchel/test/test_form_autocomplete_with_list.html
toolkit/components/satchel/test/test_input_valid_state_with_autocomplete.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
widget/tests/window_composition_text_querycontent.xhtml
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
@@ -57,34 +57,54 @@ async function checkIsFormCleared(patch 
     const expectedValue = patch[elem.id] || "";
     checkFieldValue(elem, expectedValue);
     await checkFieldHighlighted(elem, false);
     await checkFieldPreview(elem, "");
   }
 }
 
 async function confirmClear(selector) {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
   info("Await for clearing input");
-  let promise = new Promise(resolve =>
+  let promise = new Promise(resolve => {
+    let beforeInputFired = false;
+    document.querySelector(selector).addEventListener("beforeinput", (event) => {
+      beforeInputFired = true;
+      ok(event instanceof InputEvent,
+         '"beforeinput" event should be dispatched with InputEvent interface');
+      is(event.cancelable, true,
+         '"beforeinput" event should be cancelable');
+      is(event.bubbles, true,
+         '"beforeinput" event should always bubble');
+      is(event.inputType, "insertReplacementText",
+         'inputType value of "beforeinput" should be "insertReplacementText"');
+      is(event.data, "",
+         'data value of "beforeinput" should be empty string');
+      is(event.dataTransfer, null,
+         'dataTransfer value of "beforeinput" should be null');
+    }, {once: true});
     document.querySelector(selector).addEventListener("input", (event) => {
+      todo(beforeInputFired, '"beforeinput" event should\'ve been fired before "input"');
       ok(event instanceof InputEvent,
          '"input" event should be dispatched with InputEvent interface');
       is(event.cancelable, false,
          '"input" event should be never cancelable');
       is(event.bubbles, true,
          '"input" event should always bubble');
       is(event.inputType, "insertReplacementText",
-         'inputType value should be "insertReplacementText"');
+         'inputType value of "input" should be "insertReplacementText"');
       is(event.data, "",
-         "data value should be empty string");
+         'data value of "input" should be empty string');
       is(event.dataTransfer, null,
-         "dataTransfer value should be null");
+         'dataTransfer value of "input" should be null');
       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/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -125,46 +125,94 @@ async function checkFieldPreview(elem, e
 
 function checkFieldValue(elem, expectedValue) {
   if (typeof elem === "string") {
     elem = document.querySelector(elem);
   }
   is(elem.value, String(expectedValue), "Checking " + elem.id + " field");
 }
 
-function triggerAutofillAndCheckProfile(profile) {
+async function triggerAutofillAndCheckProfile(profile) {
   const adaptedProfile = _getAdaptedProfile(profile);
   const promises = [];
 
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
   for (const [fieldName, value] of Object.entries(adaptedProfile)) {
     info(`triggerAutofillAndCheckProfile: ${fieldName}`);
     const element = document.getElementById(fieldName);
     const expectingEvent =
       document.activeElement == element ? "input" : "change";
     const checkFieldAutofilled = Promise.all([
-      new Promise(resolve =>
+      new Promise(resolve => {
+        let beforeInputFired = false;
+        element.addEventListener(
+          "beforeinput",
+          event => {
+            beforeInputFired = true;
+            is(
+              event.inputType,
+              "insertReplacementText",
+              'inputType value should be "insertReplacementText"'
+            );
+            is(
+              event.data,
+              String(value),
+              `data value of "beforeinput" should be "${value}"`
+            );
+            is(
+              event.dataTransfer,
+              null,
+              'dataTransfer of "beforeinput" should be null'
+            );
+            is(
+              event.cancelable,
+              true,
+              `"beforeinput" event should be cancelable on ${element.tagName}`
+            );
+            is(
+              event.bubbles,
+              true,
+              `"beforeinput" event should always bubble on ${element.tagName}`
+            );
+            resolve();
+          },
+          { once: true }
+        );
         element.addEventListener(
           "input",
           event => {
             if (element.tagName == "INPUT" && element.type == "text") {
+              todo(
+                beforeInputFired,
+                `"beforeinput" event should've been fired before "input" event on ${
+                  element.tagName
+                }`
+              );
               ok(
                 event instanceof InputEvent,
                 `"input" event should be dispatched with InputEvent interface on ${
                   element.tagName
                 }`
               );
               is(
                 event.inputType,
                 "insertReplacementText",
                 'inputType value should be "insertReplacementText"'
               );
               is(event.data, String(value), `data value should be "${value}"`);
               is(event.dataTransfer, null, "dataTransfer should be null");
             } else {
               ok(
+                !beforeInputFired,
+                `"beforeinput" event shouldn't be fired on ${element.tagName}`
+              );
+              ok(
                 event instanceof Event && !(event instanceof UIEvent),
                 `"input" event should be dispatched with Event interface on ${
                   element.tagName
                 }`
               );
             }
             is(
               event.cancelable,
@@ -174,18 +222,18 @@ function triggerAutofillAndCheckProfile(
             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.
--- 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
@@ -41,28 +41,52 @@ let MOCK_STORAGE = [{
   tel: "+14186917110",
   country: "CA",
   "address-level1": "Qu├ębec",
 }];
 
 function checkElementFilled(element, expectedvalue) {
   return [
     new Promise(resolve => {
+      let beforeInputFired = false;
+      let oldValue = element.value;
+      element.addEventListener("beforeinput", function onBeforeInput(event) {
+        ok(true, "Checking " + element.name + " field fires beforeinput event");
+        beforeInputFired = true;
+        ok(event instanceof InputEvent,
+           `"beforeinput" event should be dispatched with InputEvent interface on ${element.name}`);
+        is(event.inputType, "insertReplacementText",
+           'inputType value of "beforeinput" event should be "insertReplacementText"');
+        is(event.data, expectedvalue,
+           'data value of "beforeinput" event should be same as expected value');
+        is(event.dataTransfer, null,
+           'dataTransfer value of "beforeinput" event should be null');
+        is(event.cancelable, true,
+           `"beforeinput" event should be cancelable on ${element.name}`);
+        is(event.bubbles, true,
+           `"input" event should always bubble on ${element.name}`);
+        is(element.value, oldValue,
+           'value of the element should not be modified at "beforeinput" event yet');
+      }, {once: true});
       element.addEventListener("input", function onInput(event) {
         ok(true, "Checking " + element.name + " field fires input event");
         if (element.tagName == "INPUT" && element.type == "text") {
+          todo(beforeInputFired, `"beforeinput" event shoud've been fired on ${element.name} before "input" event`);
           ok(event instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface on ${element.name}`);
           is(event.inputType, "insertReplacementText",
              "inputType value should be \"insertReplacementText\"");
-          is(event.data, element.value,
-             "data value should be same as value of the input element");
+          is(event.data, expectedvalue,
+             "data value should be same as expected value");
           is(event.dataTransfer, null,
              "dataTransfer value should be null");
+          is(element.value, expectedvalue,
+             'value of the element should be modified at "input" event');
         } else {
+          ok(!beforeInputFired, `"beforeinput" event shoudn't be fired on ${element.name} before "input" event`);
           ok(event instanceof Event && !(event instanceof UIEvent),
              `"input" event should be dispatched with Event interface on ${element.name}`);
         }
         is(event.cancelable, false,
            `"input" event should be never cancelable on ${element.name}`);
         is(event.bubbles, true,
            `"input" event should always bubble on ${element.name}`);
         resolve();
@@ -113,17 +137,18 @@ async function setupAddressStorage() {
   }
 }
 
 initPopupListener();
 
 add_task(async function setup() {
   // This test relies on being able to fill a Canadian address which isn't possible
   // without `supportedCountries` allowing Canada
-  await SpecialPowers.pushPrefEnv({"set": [["extensions.formautofill.supportedCountries", "US,CA"]]});
+  await SpecialPowers.pushPrefEnv({"set": [["extensions.formautofill.supportedCountries", "US,CA"],
+                                           ["dom.input_events.beforeinput.enabled", true]]});
 
   await setupAddressStorage();
 });
 
 // Autofill the address with address level 1 code.
 add_task(async function autofill_with_level1_code() {
   await setInput("#organization-en", "Mozilla Toront");
   synthesizeKey("KEY_ArrowDown");
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xhtml
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xhtml
@@ -54,27 +54,27 @@ function finish()
   window.close();
 }
 
 function onunload()
 {
   SimpleTest.finish();
 }
 
-function checkInputEvent(aEvent, aIsComposing, aInputType, aData, aDescription) {
-  if (aEvent.type != "input") {
+function checkInputEvent(aEvent, aCancelable, aIsComposing, aInputType, aData, aDescription) {
+  if (aEvent.type !== "input" && aEvent.type !== "beforeinput") {
     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}`);
-  is(aEvent.inputType, aInputType, `${aDescription}inputType should be "${aInputType}"`);
-  is(aEvent.data, aData, `${aDescription}data should be "${aData}"`);
-  is(aEvent.dataTransfer, null, `${aDescription}dataTransfer should be null`);
+  ok(aEvent instanceof InputEvent, `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface`);
+  is(aEvent.cancelable, aCancelable, `${aDescription}"${aEvent.type}" event should ${aCancelable ? "be" : "not be"} cancelable`);
+  is(aEvent.bubbles, true, `${aDescription}"${aEvent.type}" event should always bubble`);
+  is(aEvent.isComposing, aIsComposing, `${aDescription}isComposing of "${aEvent.type}" event should be ${aIsComposing}`);
+  is(aEvent.inputType, aInputType, `${aDescription}inputType of "${aEvent.type}" event should be "${aInputType}"`);
+  is(aEvent.data, aData, `${aDescription}data of "${aEvent.type}" event should be "${aData}"`);
+  is(aEvent.dataTransfer, null, `${aDescription}dataTransfer of "${aEvent.type}" event should be null`);
 }
 
 const kIsMac = (navigator.platform.indexOf("Mac") == 0);
 
 var iframe = document.getElementById("iframe");
 var childWindow = iframe.contentWindow;
 var textareaInFrame;
 var input = document.getElementById("input");
@@ -194,222 +194,242 @@ function runBeginInputTransactionMethodT
   is(events.length, 1,
      description + "compositionstart event should be fired by TIP1.startComposition()");
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during flushPendingComposition().
   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 a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", 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 compositionupdate event handler during a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("text", 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 text event handler during a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.flushPendingComposition();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   is(events.length, 4,
-     description + "compositionstart, compositionupdate, text and input events should be fired by TIP1.flushPendingComposition()");
+     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], false, true, "insertCompositionText", composingStr, description);
   is(events[2].type, "text",
-     description + "events[2] should be text");
+     description + "events[3] should be text");
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], true, "insertCompositionText", composingStr, description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, 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();
   input.addEventListener("text", 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 text event handler during a call of TIP1.commitComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.commitComposition();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.commitComposition();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.commitComposition();");
-  }, false);
+  }, {once: true});
   TIP1.commitComposition();
   is(events.length, 3,
-     description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
+     description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
+  todo_is(events[1].type, "beforeinput",
+     description + "events[1] should be beforeinput");
+  checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description);
   is(events[1].type, "compositionend",
-     description + "events[1] should be compositionend");
+     description + "events[2] should be compositionend");
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertCompositionText", composingStr, description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertCompositionText", composingStr, 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\");");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", 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 during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
   input.addEventListener("text", 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 during text event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", 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 during compositionend event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction during beforeinput event handler TIP1.commitCompositionWith(\"bar\");");
+  }, {once: true});
   input.addEventListener("input", 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 during input event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.commitCompositionWith("bar");
   is(events.length, 5,
-     description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
+     description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
   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");
+  todo_is(events[3].type, "beforeinput",
+     description + "events[3] should be beforeinput");
+  checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description);
   is(events[3].type, "compositionend",
-     description + "events[3] should be compositionend");
+     description + "events[4] should be compositionend");
   is(events[4].type, "input",
-     description + "events[4] should be input");
-  checkInputEvent(events[4], false, "insertCompositionText", "bar", description);
+     description + "events[5] should be input");
+  checkInputEvent(events[4], false, false, "insertCompositionText", "bar", 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) {
     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 compositionupdate event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("text", 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 text event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.cancelComposition();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
   TIP1.cancelComposition();
   is(events.length, 4,
-     description + "compositionupdate, text, compositionend and input events should be fired by TIP1.cancelComposition()");
+     description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], false, true, "insertCompositionText", "", description);
   is(events[2].type, "compositionend",
-     description + "events[2] should be compositionend");
+     description + "events[3] should be compositionend");
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], false, "insertCompositionText", "", description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, false, "insertCompositionText", "", 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),
        description + "TIP2 shouldn't be able to begin input transaction from keydown event handler during a call of TIP1.keydown();");
-  }, false);
+  }, {once: true});
   input.addEventListener("keypress", 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 keypress event handler during a call of TIP1.keydown();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.keydown();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.keydown();");
-  }, false);
+  }, {once: true});
   input.addEventListener("keyup", 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 keyup event handler during a call of TIP1.keyup();");
-  }, false);
+  }, {once: true});
   var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
   TIP1.keydown(keyA);
   TIP1.keyup(keyA);
   is(events.length, 4,
-     description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
+     description + "keydown, keypress, beforeinput, 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");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], true, false, "insertText", "a", description);
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertText", "a", description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertText", "a", description);
   is(events[3].type, "keyup",
-     description + "events[3] should be keyup");
+     description + "events[4] 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);
     ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.startComposition();");
@@ -419,924 +439,1042 @@ function runBeginInputTransactionMethodT
   is(events.length, 1,
      description + "compositionstart event should be fired by TIP1.startComposition()");
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during flushPendingComposition().
   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 a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", 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 compositionupdate event handler during a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("text", 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 text event handler during a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.flushPendingComposition();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.flushPendingComposition();");
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransactionForTests(window);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   is(events.length, 4,
-     description + "compositionstart, compositionupdate, text and input events should be fired by TIP1.flushPendingComposition()");
+     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
   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");
+  todo_is(events[3].type, "beforeinput",
+     description + "events[3] should be beforeinput");
+  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description);
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], true, "insertCompositionText", composingStr, description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, 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();
   input.addEventListener("text", 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 text event handler during a call of TIP1.commitComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.commitComposition();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.commitComposition();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.commitComposition();");
-  }, false);
+  }, {once: true});
   TIP1.commitComposition();
   is(events.length, 3,
-     description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
+     description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
+  todo_is(events[1].type, "beforeinput",
+     description + "events[1] should be beforeinput");
+  checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description);
   is(events[1].type, "compositionend",
-     description + "events[1] should be compositionend");
+     description + "events[2] should be compositionend");
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertCompositionText", composingStr, description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertCompositionText", composingStr, 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\");");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", 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 during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
   input.addEventListener("text", 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 during text event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", 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 during compositionend event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests during beforeinput event handler TIP1.commitCompositionWith(\"bar\");");
+  }, {once: true});
   input.addEventListener("input", 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 during input event handler TIP1.commitCompositionWith(\"bar\");");
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransactionForTests(window);
   TIP1.commitCompositionWith("bar");
   is(events.length, 5,
-     description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
+     description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
   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");
+  todo_is(events[3].type, "beforeinput",
+     description + "events[3] should be beforeinput");
+  checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description);
   is(events[3].type, "compositionend",
-     description + "events[3] should be compositionend");
+     description + "events[4] should be compositionend");
   is(events[4].type, "input",
-     description + "events[4] should be input");
-  checkInputEvent(events[4], false, "insertCompositionText", "bar", description);
+     description + "events[5] should be input");
+  checkInputEvent(events[4], false, false, "insertCompositionText", "bar", 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) {
     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 compositionupdate event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("text", 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 text event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.cancelComposition();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.cancelComposition();");
-  }, false);
+  }, {once: true});
   TIP1.cancelComposition();
   is(events.length, 4,
-     description + "compositionupdate, text, compositionend and input events should be fired by TIP1.cancelComposition()");
+     description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], false, true, "insertCompositionText", "", description);
   is(events[2].type, "compositionend",
-     description + "events[2] should be compositionend");
+     description + "events[3] should be compositionend");
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], false, "insertCompositionText", "", description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, false, "insertCompositionText", "", 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),
        description + "TIP2 shouldn't be able to begin input transaction for tests for tests from keydown event handler during a call of TIP1.keydown();");
-  }, false);
+  }, {once: true});
   input.addEventListener("keypress", 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 keypress event handler during a call of TIP1.keydown();");
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.keydown();");
+  }, {once: true});
   input.addEventListener("input", 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 input event handler during a call of TIP1.keydown();");
-  }, false);
+  }, {once: true});
   input.addEventListener("keyup", 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 keyup event handler during a call of TIP1.keyup();");
-  }, false);
+  }, {once: true});
   var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
   TIP1.keydown(keyA);
   TIP1.keyup(keyA);
   is(events.length, 4,
-     description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
+     description + "keydown, keypress, beforeinput, 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");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], true, false, "insertText", "a", description);
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertText", "a", description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertText", "a", description);
   is(events[3].type, "keyup",
-     description + "events[3] should be keyup");
+     description + "events[4] 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);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during startComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during startComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.startComposition();
   is(events.length, 1,
      description + "compositionstart event should be fired by TIP1.startComposition()");
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition().
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransaction(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during flushPendingComposition()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   is(events.length, 4,
-     description + "compositionstart, compositionupdate, text and input events should be fired by TIP1.flushPendingComposition()");
+     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], false, true, "insertCompositionText", composingStr, description);
   is(events[2].type, "text",
-     description + "events[2] should be text");
+     description + "events[3] should be text");
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], true, "insertCompositionText", composingStr, description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, 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();
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransaction(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitComposition()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.commitComposition();
   is(events.length, 3,
-     description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
+     description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
+  todo_is(events[1].type, "beforeinput",
+     description + "events[1] should be beforeinput");
+  checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description);
   is(events[1].type, "compositionend",
-     description + "events[1] should be compositionend");
+     description + "events[2] should be compositionend");
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertCompositionText", composingStr, description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertCompositionText", composingStr, 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);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransaction(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitCompositionWith(\"bar\")");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.commitCompositionWith("bar");
   is(events.length, 5,
-     description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
+     description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
   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");
+  todo_is(events[3].type, "beforeinput",
+     description + "events[3] should be beforeinput");
+  checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description);
   is(events[3].type, "compositionend",
-     description + "events[3] should be compositionend");
+     description + "events[4] should be compositionend");
   is(events[4].type, "input",
-     description + "events[4] should be input");
-  checkInputEvent(events[4], false, "insertCompositionText", "bar", description);
+     description + "events[5] should be input");
+  checkInputEvent(events[4], false, false, "insertCompositionText", "bar", 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) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransaction(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during cancelComposition()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.cancelComposition();
   is(events.length, 4,
-     description + "compositionupdate, text, compositionend and input events should be fired by TIP1.cancelComposition()");
+     description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], false, true, "insertCompositionText", "", description);
   is(events[2].type, "compositionend",
-     description + "events[2] should be compositionend");
+     description + "events[3] should be compositionend");
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], false, "insertCompositionText", "", description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, false, "insertCompositionText", "", 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 {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keydown\" should throw an exception during keydown()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keydown\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("keypress", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keypress\" should throw an exception during keydown()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keypress\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransaction(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during keydown()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during keydown()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("keyup", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keyup\" should throw an exception during keyup()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keyup\" should cause NS_ERROR_ALREADY_INITIALIZED during keyup()");
     }
-  }, false);
+  }, {once: true});
   var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
   TIP1.keydown(keyA);
   TIP1.keyup(keyA);
   is(events.length, 4,
-     description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
+     description + "keydown, keypress, beforeinput, 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");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], true, false, "insertText", "a", description);
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertText", "a", description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertText", "a", description);
   is(events[3].type, "keyup",
-     description + "events[3] should be keyup");
+     description + "events[4] 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);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during startComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during startComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.startComposition();
   is(events.length, 1,
      description + "compositionstart event should be fired by TIP1.startComposition()");
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition().
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during flushPendingComposition()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during flushPendingComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   is(events.length, 4,
-     description + "compositionstart, compositionupdate, text and input events should be fired by TIP1.flushPendingComposition()");
+     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
   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");
+  todo_is(events[3].type, "beforeinput",
+     description + "events[3] should be beforeinput");
+  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description);
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], true, "insertCompositionText", composingStr, description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, 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();
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitComposition()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.commitComposition();
   is(events.length, 3,
-     description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
+     description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
+  todo_is(events[1].type, "beforeinput",
+     description + "events[1] should be beforeinput");
+  checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description);
   is(events[1].type, "compositionend",
-     description + "events[1] should be compositionend");
+     description + "events[2] should be compositionend");
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertCompositionText", composingStr, description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertCompositionText", composingStr, 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);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionupdate", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitCompositionWith(\"bar\")");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitCompositionWith(\"bar\")");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
     }
-  }, false);
+  }, {once: true});
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.commitCompositionWith("bar");
   is(events.length, 5,
-     description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
+     description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
   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");
+  todo_is(events[3].type, "beforeinput",
+     description + "events[3] should be beforeinput");
+  checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description);
   is(events[3].type, "compositionend",
-     description + "events[3] should be compositionend");
+     description + "events[4] should be compositionend");
   is(events[4].type, "input",
-     description + "events[4] should be input");
-  checkInputEvent(events[4], false, "insertCompositionText", "bar", description);
+     description + "events[5] should be input");
+  checkInputEvent(events[4], false, false, "insertCompositionText", "bar", 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) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("text", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("compositionend", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during cancelComposition()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during cancelComposition()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
     }
-  }, false);
+  }, {once: true});
   TIP1.cancelComposition();
   is(events.length, 4,
-     description + "compositionupdate, text, compositionend and input events should be fired by TIP1.cancelComposition()");
+     description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], false, true, "insertCompositionText", "", description);
   is(events[2].type, "compositionend",
-     description + "events[2] should be compositionend");
+     description + "events[3] should be compositionend");
   is(events[3].type, "input",
-     description + "events[3] should be input");
-  checkInputEvent(events[3], false, "insertCompositionText", "", description);
+     description + "events[4] should be input");
+  checkInputEvent(events[3], false, false, "insertCompositionText", "", 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 {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keydown\" should throw an exception during keydown()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keydown\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("keypress", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keypress\" should throw an exception during keydown()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keypress\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
     }
-  }, false);
+  }, {once: true});
+  input.addEventListener("beforeinput", function (aEvent) {
+    events.push(aEvent);
+    try {
+      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
+      ok(false,
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during keydown()");
+    } catch (e) {
+      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
+         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
+    }
+  }, {once: true});
   input.addEventListener("input", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during keydown()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
     }
-  }, false);
+  }, {once: true});
   input.addEventListener("keyup", function (aEvent) {
     events.push(aEvent);
-    input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
       ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keyup\" should throw an exception during keyup()");
     } catch (e) {
       ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keyup\" should cause NS_ERROR_ALREADY_INITIALIZED during keyup()");
     }
-  }, false);
+  }, {once: true});
   var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
   TIP1.keydown(keyA);
   TIP1.keyup(keyA);
   is(events.length, 4,
-     description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
+     description + "keydown, keypress, beforeinput, 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");
+  todo_is(events[2].type, "beforeinput",
+     description + "events[2] should be beforeinput");
+  checkInputEvent(events[2], true, false, "insertText", "a", description);
   is(events[2].type, "input",
-     description + "events[2] should be input");
-  checkInputEvent(events[2], false, "insertText", "a", description);
+     description + "events[3] should be input");
+  checkInputEvent(events[2], false, false, "insertText", "a", description);
   is(events[3].type, "keyup",
-     description + "events[3] should be keyup");
+     description + "events[4] 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),
      description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition");
   try {
@@ -1584,17 +1722,17 @@ function runCompositionTests()
      description + "compositionupdate caused by flushPendingComposition() should have new composition string in its data");
   is(input.value, "FOo",
      description + "modifying composition clause shouldn't cause modifying the focused editor");
 
   // Committing the composition string
   reset();
   TIP.commitComposition();
   is(events.length, 1,
-     description + "commitComposition() should cause compositionend but shoudn't cause compositionupdate");
+     description + "commitComposition() should cause compositionend but shouldn't cause compositionupdate");
   is(events[0].type, "compositionend",
      description + "commitComposition() should cause compositionend");
   is(events[0].data, "FOo",
      description + "compositionend caused by commitComposition() should have the committed string in its data");
   is(input.value, "FOo",
      description + "commitComposition() shouldn't cause modifying the focused editor");
 
   // Starting new composition without a call of startComposition()
@@ -3823,188 +3961,190 @@ function runCommitCompositionTests()
   TIP.flushPendingComposition();
   doCommitWithNullCheck(undefined);
   is(input.value, "",
      description + "doCommitWithNullCheck(undefined) should commit the composition with empty string");
 }
 
 function runUnloadTests1()
 {
-  var description = "runUnloadTests1(): ";
-
-  var TIP1 = createTIP();
-  ok(TIP1.beginInputTransactionForTests(childWindow),
-     description + "TIP1.beginInputTransactionForTests() should succeed");
-
-  var oldSrc = iframe.src;
-  var parentWindow = window;
-
-  iframe.addEventListener("load", function (aEvent) {
-    ok(true, description + "dummy page is loaded");
-    iframe.removeEventListener("load", arguments.callee, true);
-    childWindow = iframe.contentWindow;
-    textareaInFrame = null;
-    iframe.addEventListener("load", function () {
-      ok(true, description + "old iframe is restored");
-      // And also restore the iframe information with restored contents.
-      iframe.removeEventListener("load", arguments.callee, true);
+  return new Promise(resolve => {
+    let description = "runUnloadTests1(): ";
+
+    let TIP1 = createTIP();
+    ok(TIP1.beginInputTransactionForTests(childWindow),
+       description + "TIP1.beginInputTransactionForTests() should succeed");
+
+    let oldSrc = iframe.src;
+    let parentWindow = window;
+
+    iframe.addEventListener("load", function (aEvent) {
+      ok(true, description + "dummy page is loaded");
       childWindow = iframe.contentWindow;
-      textareaInFrame = iframe.contentDocument.getElementById("textarea");
-      SimpleTest.executeSoon(continueTest);
-    }, true);
-
-    // The composition should be committed internally.  So, another TIP should
-    // be able to steal the rights to using TextEventDispatcher.
-    var TIP2 = createTIP();
-    ok(TIP2.beginInputTransactionForTests(parentWindow),
-       description + "TIP2.beginInputTransactionForTests() should succeed");
-
-    input.focus();
-    input.value = "";
-
-    TIP2.setPendingCompositionString("foo");
-    TIP2.appendClauseToPendingComposition(3, TIP2.ATTR_RAW_CLAUSE);
-    TIP2.setCaretInPendingComposition(3);
-    TIP2.flushPendingComposition();
-    is(input.value, "foo",
-       description + "the input in the parent document should have composition string");
-
-    TIP2.cancelComposition();
-
-    // Restore the old iframe content.
-    iframe.src = oldSrc;
-  }, true);
-
-  // Start composition in the iframe.
-  textareaInFrame.value = "";
-  textareaInFrame.focus();
-
-  TIP1.setPendingCompositionString("foo");
-  TIP1.appendClauseToPendingComposition(3, TIP1.ATTR_RAW_CLAUSE);
-  TIP1.setCaretInPendingComposition(3);
-  TIP1.flushPendingComposition();
-  is(textareaInFrame.value, "foo",
-     description + "the textarea in the iframe should have composition string");
-
-  // Load different web page on the frame.
-  iframe.src = "data:text/html,<body>dummy page</body>";
+      textareaInFrame = null;
+      iframe.addEventListener("load", function () {
+        ok(true, description + "old iframe is restored");
+        // And also restore the iframe information with restored contents.
+        childWindow = iframe.contentWindow;
+        textareaInFrame = iframe.contentDocument.getElementById("textarea");
+        SimpleTest.executeSoon(resolve);
+      }, {capture: true, once: true});
+
+      // The composition should be committed internally.  So, another TIP should
+      // be able to steal the rights to using TextEventDispatcher.
+      let TIP2 = createTIP();
+      ok(TIP2.beginInputTransactionForTests(parentWindow),
+         description + "TIP2.beginInputTransactionForTests() should succeed");
+
+      input.focus();
+      input.value = "";
+
+      TIP2.setPendingCompositionString("foo");
+      TIP2.appendClauseToPendingComposition(3, TIP2.ATTR_RAW_CLAUSE);
+      TIP2.setCaretInPendingComposition(3);
+      TIP2.flushPendingComposition();
+      is(input.value, "foo",
+         description + "the input in the parent document should have composition string");
+
+      TIP2.cancelComposition();
+
+      // Restore the old iframe content.
+      iframe.src = oldSrc;
+    }, {capture: true, once: true});
+
+    // Start composition in the iframe.
+    textareaInFrame.value = "";
+    textareaInFrame.focus();
+
+    TIP1.setPendingCompositionString("foo");
+    TIP1.appendClauseToPendingComposition(3, TIP1.ATTR_RAW_CLAUSE);
+    TIP1.setCaretInPendingComposition(3);
+    TIP1.flushPendingComposition();
+    is(textareaInFrame.value, "foo",
+       description + "the textarea in the iframe should have composition string");
+
+    // Load different web page on the frame.
+    iframe.src = "data:text/html,<body>dummy page</body>";
+  });
 }
 
 function runUnloadTests2()
 {
-  var description = "runUnloadTests2(): ";
-
-  var TIP = createTIP();
-  ok(TIP.beginInputTransactionForTests(childWindow),
-     description + "TIP.beginInputTransactionForTests() should succeed");
-
-  var oldSrc = iframe.src;
-  var parentWindow = window;
-
-  iframe.addEventListener("load", function (aEvent) {
-    ok(true, description + "dummy page is loaded");
-    iframe.removeEventListener("load", arguments.callee, true);
-    childWindow = iframe.contentWindow;
-    textareaInFrame = null;
-    iframe.addEventListener("load", function () {
-      ok(true, description + "old iframe is restored");
-      // And also restore the iframe information with restored contents.
-      iframe.removeEventListener("load", arguments.callee, true);
+  return new Promise(resolve => {
+    let description = "runUnloadTests2(): ";
+
+    let TIP = createTIP();
+    ok(TIP.beginInputTransactionForTests(childWindow),
+       description + "TIP.beginInputTransactionForTests() should succeed");
+
+    let oldSrc = iframe.src;
+    let parentWindow = window;
+
+    iframe.addEventListener("load", function (aEvent) {
+      ok(true, description + "dummy page is loaded");
       childWindow = iframe.contentWindow;
-      textareaInFrame = iframe.contentDocument.getElementById("textarea");
-      SimpleTest.executeSoon(continueTest);
-    }, true);
-
-    input.focus();
-    input.value = "";
-
-    // TIP should be still available in the same top level widget.
-    TIP.setPendingCompositionString("bar");
+      textareaInFrame = null;
+      iframe.addEventListener("load", function () {
+        ok(true, description + "old iframe is restored");
+        // And also restore the iframe information with restored contents.
+        childWindow = iframe.contentWindow;
+        textareaInFrame = iframe.contentDocument.getElementById("textarea");
+        SimpleTest.executeSoon(resolve);
+      }, {capture: true, once: true});
+
+      input.focus();
+      input.value = "";
+
+      // TIP should be still available in the same top level widget.
+      TIP.setPendingCompositionString("bar");
+      TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
+      TIP.setCaretInPendingComposition(3);
+      TIP.flushPendingComposition();
+      if (input.value == "") {
+        // XXX TextInputProcessor or TextEventDispatcher may have a bug.
+        todo_is(input.value, "bar",
+                description + "the input in the parent document should have composition string");
+      } else {
+        is(input.value, "bar",
+           description + "the input in the parent document should have composition string");
+      }
+
+      TIP.cancelComposition();
+
+      // Restore the old iframe content.
+      iframe.src = oldSrc;
+    }, {capture: true, once: true});
+
+    // Start composition in the iframe.
+    textareaInFrame.value = "";
+    textareaInFrame.focus();
+
+    TIP.setPendingCompositionString("foo");
     TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
     TIP.setCaretInPendingComposition(3);
     TIP.flushPendingComposition();
-    if (input.value == "") {
-      // XXX TextInputProcessor or TextEventDispatcher may have a bug.
-      todo_is(input.value, "bar",
-              description + "the input in the parent document should have composition string");
-    } else {
-      is(input.value, "bar",
-         description + "the input in the parent document should have composition string");
-    }
-
-    TIP.cancelComposition();
-
-    // Restore the old iframe content.
-    iframe.src = oldSrc;
-  }, true);
-
-  // Start composition in the iframe.
-  textareaInFrame.value = "";
-  textareaInFrame.focus();
-
-  TIP.setPendingCompositionString("foo");
-  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
-  TIP.setCaretInPendingComposition(3);
-  TIP.flushPendingComposition();
-  is(textareaInFrame.value, "foo",
-     description + "the textarea in the iframe should have composition string");
-
-  // Load different web page on the frame.
-  iframe.src = "data:text/html,<body>dummy page</body>";
+    is(textareaInFrame.value, "foo",
+       description + "the textarea in the iframe should have composition string");
+
+    // Load different web page on the frame.
+    iframe.src = "data:text/html,<body>dummy page</body>";
+  });
 }
 
-function* runCallbackTests(aForTests)
+async function runCallbackTests(aForTests)
 {
-  var description = "runCallbackTests(aForTests=" + aForTests + "): ";
+  let description = "runCallbackTests(aForTests=" + aForTests + "): ";
 
   input.value = "";
   input.focus();
   input.blur();
 
-  var TIP = createTIP();
-  var notifications = [];
-  var callContinueTest = false;
+  let TIP = createTIP();
+  let notifications = [];
+  let waitingNextNotification;
   function callback(aTIP, aNotification)
   {
     if (aTIP == TIP) {
       notifications.push(aNotification);
     }
     switch (aNotification.type) {
       case "request-to-commit":
         aTIP.commitComposition();
         break;
       case "request-to-cancel":
         aTIP.cancelComposition();
         break;
     }
-    if (callContinueTest) {
-      callContinueTest = false;
-      SimpleTest.executeSoon(continueTest);
+    if (waitingNextNotification) {
+      SimpleTest.executeSoon(waitingNextNotification);
+      waitingNextNotification = undefined;
     }
     return true;
   }
 
   function dumpUnexpectedNotifications(aExpectedCount)
   {
     if (notifications.length <= aExpectedCount) {
       return;
     }
-    for (var i = aExpectedCount; i < notifications.length; i++) {
+    for (let i = aExpectedCount; i < notifications.length; i++) {
       ok(false,
          description + "Unexpected notification: " + notifications[i].type);
     }
   }
 
   function waitUntilNotificationsReceived()
   {
-    if (notifications.length > 0) {
-      SimpleTest.executeSoon(continueTest);
-    } else {
-      callContinueTest = true;
-    }
+    return new Promise(resolve => {
+      if (notifications.length > 0) {
+        SimpleTest.executeSoon(resolve);
+      } else {
+        waitingNextNotification = resolve;
+      }
+    });
   }
 
   function checkPositionChangeNotification(aNotification, aDescription)
   {
     is(!aNotification || aNotification.type, "notify-position-change",
        aDescription + " should cause position change notification");
   }
 
@@ -4074,17 +4214,17 @@ function* runCallbackTests(aForTests)
   input.blur();
   is(notifications.length, 1,
      description + "input.blur() should cause a notification");
   is(notifications[0].type, "notify-blur",
      description + "input.blur() should cause \"notify-focus\"");
   dumpUnexpectedNotifications(1);
 
   input.focus();
-  yield waitUntilNotificationsReceived();
+  await waitUntilNotificationsReceived();
   notifications = [];
   TIP.setPendingCompositionString("foo");
   TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
   TIP.flushPendingComposition();
   is(notifications.length, 3,
      description + "creating composition string 'foo' should cause 3 notifications");
   checkTextChangeNotification(notifications[0], description + "creating composition string 'foo'",
                               { offset: 0, removedLength: 0, addedLength: 3,
@@ -4102,57 +4242,57 @@ function* runCallbackTests(aForTests)
      description + "synthesizeMouseAtCenter(input, {}) during composition should cause \"request-to-commit\"");
   checkTextChangeNotification(notifications[1], description + "synthesizeMouseAtCenter(input, {}) during composition",
                               { offset: 0, removedLength: 3, addedLength: 3,
                                 causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
   checkPositionChangeNotification(notifications[2], description + "synthesizeMouseAtCenter(input, {}) during composition");
   dumpUnexpectedNotifications(3);
 
   input.focus();
-  yield waitUntilNotificationsReceived();
+  await waitUntilNotificationsReceived();
   notifications = [];
   // XXX On macOS, window.moveBy() doesn't cause notify-position-change.
   //     Investigate this later (although, we cannot notify position change to
   //     native IME on macOS).
   if (!kIsMac) {
     window.moveBy(0, 10);
-    yield waitUntilNotificationsReceived();
+    await waitUntilNotificationsReceived();
     is(notifications.length, 1,
        description + "window.moveBy(0, 10) should cause a notification");
     checkPositionChangeNotification(notifications[0], description + "window.moveBy(0, 10)");
     dumpUnexpectedNotifications(1);
 
     notifications = [];
     window.moveBy(10, 0);
-    yield waitUntilNotificationsReceived();
+    await waitUntilNotificationsReceived();
     is(notifications.length, 1,
        description + "window.moveBy(10, 0) should cause a notification");
     checkPositionChangeNotification(notifications[0], description + "window.moveBy(10, 0)");
     dumpUnexpectedNotifications(1);
   }
 
   input.focus();
   input.value = "abc"
   notifications = [];
   input.selectionStart = input.selectionEnd = 0;
-  yield waitUntilNotificationsReceived();
+  await waitUntilNotificationsReceived();
   notifications = [];
-  var rightArrowKeyEvent =
+  let rightArrowKeyEvent =
     new KeyboardEvent("", { key: "ArrowRight", code: "ArrowRight", keyCode: KeyboardEvent.DOM_VK_RIGHT });
   TIP.keydown(rightArrowKeyEvent);
   TIP.keyup(rightArrowKeyEvent);
   is(notifications.length, 1,
      description + "ArrowRight key press should cause a notification");
   checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press", { offset: 1, text: "" });
   dumpUnexpectedNotifications(1);
 
   notifications = [];
-  var shiftKeyEvent =
+  let shiftKeyEvent =
     new KeyboardEvent("", { key: "Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT });
-  var leftArrowKeyEvent =
+  let leftArrowKeyEvent =
     new KeyboardEvent("", { key: "ArrowLeft", code: "ArrowLeft", keyCode: KeyboardEvent.DOM_VK_LEFT });
   TIP.keydown(shiftKeyEvent);
   TIP.keydown(leftArrowKeyEvent);
   TIP.keyup(leftArrowKeyEvent);
   TIP.keyup(shiftKeyEvent);
   is(notifications.length, 1,
      description + "ArrowLeft key press with Shift should cause a notification");
   checkSelectionChangeNotification(notifications[0], description + "ArrowLeft key press with Shift", { offset: 0, text: "a", reversed: true });
@@ -4166,60 +4306,47 @@ function* runCallbackTests(aForTests)
   TIP.keyup(rightArrowKeyEvent);
   TIP.keyup(shiftKeyEvent);
   is(notifications.length, 1,
      description + "ArrowRight key press with Shift should cause a notification");
   checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press with Shift", { offset: 1, text: "b" });
   dumpUnexpectedNotifications(1);
 
   notifications = [];
-  var TIP2 = createTIP();
+  let TIP2 = createTIP();
   if (aForTests) {
     TIP2.beginInputTransactionForTests(window, callback);
   } else {
     TIP2.beginInputTransaction(window, callback);
   }
   is(notifications.length, 1,
      description + "Initializing another TIP should cause a notification");
   is(notifications[0].type, "notify-end-input-transaction",
      description + "Initializing another TIP should cause \"notify-detached\"");
   dumpUnexpectedNotifications(1);
 }
 
-var gTestContinuation = null;
-
-function continueTest()
+async function runTests()
 {
-  if (!gTestContinuation) {
-    gTestContinuation = testBody();
-  }
-  var ret = gTestContinuation.next();
-  if (ret.done) {
-    finish();
-  }
-}
-
-function* testBody()
-{
+  textareaInFrame = iframe.contentDocument.getElementById("textarea");
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
   runBeginInputTransactionMethodTests();
   runReleaseTests();
   runCompositionTests();
   runCompositionWithKeyEventTests();
   runConsumingKeydownBeforeCompositionTests();
   runKeyTests();
   runErrorTests();
   runCommitCompositionTests();
-  yield* runCallbackTests(false);
-  yield* runCallbackTests(true);
-  yield runUnloadTests1();
-  yield runUnloadTests2();
-}
-
-function runTests()
-{
-  textareaInFrame = iframe.contentDocument.getElementById("textarea");
-  continueTest();
+  await runCallbackTests(false);
+  await runCallbackTests(true);
+  await runUnloadTests1();
+  await runUnloadTests2();
+
+  finish();
 }
 
 ]]>
 </script>
 
 </window>
--- a/dom/html/test/forms/test_MozEditableElement_setUserInput.html
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -10,118 +10,127 @@
 </div>
 <div id="content"></div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="application/javascript">
 SimpleTest.waitForExplicitFinish();
 // eslint-disable-next-line complexity
-SimpleTest.waitForFocus(() => {
+SimpleTest.waitForFocus(async () => {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
   let content = document.getElementById("content");
   /**
    * Test structure:
    *   element: the tag name to create.
    *   type: the type attribute value for the element.  If unnecessary omit it.
    *   input: the values calling setUserInput() with.
    *     before: used when calling setUserInput() before the element gets focus.
    *     after: used when calling setUserInput() after the element gets focus.
    *   result: the results of calling setUserInput().
    *     before: the element's expected value of calling setUserInput() before the element gets focus.
    *     after: the element's expected value of calling setUserInput() after the element gets focus.
+   *     fireBeforeInputEvent: true if "beforeinput" event should be fired.  Otherwise, false.
    *     fireInputEvent: true if "input" event should be fired.  Otherwise, false.
    */
   for (let test of [{element: "input", type: "hidden",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
                     {element: "input", type: "text",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     {element: "input", type: "search",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     {element: "input", type: "tel",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     {element: "input", type: "url",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     {element: "input", type: "email",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     {element: "input", type: "password",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     // "date" does not support setUserInput, but dispatches "input" event...
                     {element: "input", type: "date",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     // "month" does not support setUserInput, but dispatches "input" event...
                     {element: "input", type: "month",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     // "week" does not support setUserInput, but dispatches "input" event...
                     {element: "input", type: "week",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     // "time" does not support setUserInput, but dispatches "input" event...
                     {element: "input", type: "time",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     // "datetime-local" does not support setUserInput, but dispatches "input" event...
                     {element: "input", type: "datetime-local",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     {element: "input", type: "number",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
                     {element: "input", type: "range",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     // "color" does not support setUserInput, but dispatches "input" event...
                     {element: "input", type: "color",
                      input: {before: "#5C5C5C", after: "#FFFFFF"},
-                     result: {before: "#5c5c5c", after:"#ffffff", fireInputEvent: true}},
+                     result: {before: "#5c5c5c", after:"#ffffff", fireBeforeInputEvent: false, fireInputEvent: true}},
                     {element: "input", type: "checkbox",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     {element: "input", type: "radio",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
                     // "file" is not supported by setUserInput? But there is a path...
                     {element: "input", type: "file",
                      input: {before: "3", after: "6"},
-                     result: {before: "", after:"", fireInputEvent: true}},
+                     result: {before: "", after:"", fireBeforeInputEvent: false, fireInputEvent: true}},
                     {element: "input", type: "submit",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
                     {element: "input", type: "image",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
                     {element: "input", type: "reset",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
                     {element: "input", type: "button",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: false}},
+                     result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
                     {element: "textarea",
                      input: {before: "3", after: "6"},
-                     result: {before: "3", after:"6", fireInputEvent: true}}]) {
+                     result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}]) {
     let tag =
       test.type !== undefined ? `<${test.element} type="${test.type}">` :
                                 `<${test.element}>`;
     content.innerHTML =
       test.element !== "input" ? tag : `${tag}</${test.element}>`;
     content.scrollTop; // Flush pending layout.
     let target = content.firstChild;
 
-    let inputEvents = [];
+    let inputEvents = [], beforeInputEvents = [];
+    function onBeforeInput(aEvent) {
+      beforeInputEvents.push(aEvent);
+    }
     function onInput(aEvent) {
       inputEvents.push(aEvent);
     }
+    target.addEventListener("beforeinput", onBeforeInput);
     target.addEventListener("input", onInput);
 
     // Before setting focus, editor of the element may have not been created yet.
     let previousValue = target.value;
     SpecialPowers.wrap(target).setUserInput(test.input.before);
     if (target.value == previousValue && test.result.before != previousValue) {
       todo_is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
     } else {
@@ -130,61 +139,81 @@ SimpleTest.waitForFocus(() => {
     if (target.value == previousValue) {
       if (test.type === "date" || test.type === "time") {
         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 {
+      if (!test.result.fireBeforeInputEvent) {
+        is(beforeInputEvents.length, 0,
+           `No "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
       } else {
-        is(inputEvents.length, 0,
-           `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+        todo_is(beforeInputEvents.length, 1,
+           `Only one "beforeinput" 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 (!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 (SpecialPowers.wrap(target).isInputEventTarget) {
         if (test.type === "number" || test.type === "time") {
           todo(inputEvents[0] instanceof InputEvent,
                `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
         } else {
+          if (beforeInputEvents.length > 0 && test.result.fireBeforeInputEvent) {
+            is(beforeInputEvents[0].cancelable, true,
+               `"beforeinput" event for "insertReplacementText" should be cancelable when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+            is(beforeInputEvents[0].inputType, "insertReplacementText",
+               `inputType of "beforeinput"event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+            is(beforeInputEvents[0].data, test.input.before,
+               `data of "beforeinput" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+            is(beforeInputEvents[0].dataTransfer, null,
+               `dataTransfer of "beforeinput" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+          }
           ok(inputEvents[0] instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
           is(inputEvents[0].inputType, "insertReplacementText",
-             `inputType should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+             `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
           is(inputEvents[0].data, test.input.before,
-             `data should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+             `data of "input" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
           is(inputEvents[0].dataTransfer, null,
-             `dataTransfer should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+             `dataTransfer of "input" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
         }
       } else {
         ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
            `"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
       }
       is(inputEvents[0].cancelable, false,
          `"input" event should be never cancelable (${tag}, before getting focus)`);
       is(inputEvents[0].bubbles, true,
          `"input" event should always bubble (${tag}, before getting focus)`);
     }
 
+    beforeInputEvents = [];
     inputEvents = [];
     target.focus();
     previousValue = target.value;
     SpecialPowers.wrap(target).setUserInput(test.input.after);
     if (target.value == previousValue && test.result.after != previousValue) {
       todo_is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
     } else {
       is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
@@ -192,50 +221,69 @@ SimpleTest.waitForFocus(() => {
     if (target.value == previousValue) {
       if (test.type === "date" || test.type === "time") {
         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 {
+      if (!test.result.fireBeforeInputEvent) {
+        is(beforeInputEvents.length, 0,
+           `No "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
       } else {
-        is(inputEvents.length, 0,
-           `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+        todo_is(beforeInputEvents.length, 1,
+           `Only one "beforeinput" 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 (!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 (SpecialPowers.wrap(target).isInputEventTarget) {
         if (test.type === "number" || test.type === "time") {
           todo(inputEvents[0] instanceof InputEvent,
                `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
         } else {
+          if (beforeInputEvents.length > 0 && test.result.fireBeforeInputEvent) {
+            is(beforeInputEvents[0].cancelable, true,
+               `"beforeinput" event should be cancelable when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+            is(beforeInputEvents[0].inputType, "insertReplacementText",
+               `inputType of "beforeinput" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+            is(beforeInputEvents[0].data, test.input.after,
+               `data of "beforeinput" should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+            is(beforeInputEvents[0].dataTransfer, null,
+               `dataTransfer of "beforeinput" should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+          }
           ok(inputEvents[0] instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
           is(inputEvents[0].inputType, "insertReplacementText",
-             `inputType should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+             `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
           is(inputEvents[0].data, test.input.after,
-             `data should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+             `data of "input" event should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
           is(inputEvents[0].dataTransfer, null,
-             `dataTransfer should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+             `dataTransfer of "input" event should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
         }
       } else {
         ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
            `"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
       }
       is(inputEvents[0].cancelable, false,
          `"input" event should be never cancelable (${tag}, after getting focus)`);
       is(inputEvents[0].bubbles, true,
--- a/dom/html/test/forms/test_input_event.html
+++ b/dom/html/test/forms/test_input_event.html
@@ -37,279 +37,369 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script class="testbody" type="text/javascript">
 
   /** Test for input event. This is highly based on test_change_event.html **/
 
   const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
 
   let expectedInputType = "";
   let expectedData = null;
+  let expectedBeforeInputCancelable = false;
+  function checkBeforeInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"beforeinput" event should be dispatched with InputEvent interface ${aDescription}`);
+    is(aEvent.inputType, expectedInputType,
+       `inputType of "beforeinput" event should be "${expectedInputType}" ${aDescription}`);
+    is(aEvent.data, expectedData,
+       `data of "beforeinput" event should be ${expectedData} ${aDescription}`);
+    is(aEvent.dataTransfer, null,
+       `dataTransfer of "beforeinput" event should be null ${aDescription}`);
+    is(aEvent.cancelable, expectedBeforeInputCancelable,
+       `"beforeinput" event for "${expectedInputType}" should ${expectedBeforeInputCancelable ? "be" : "not be"} cancelable ${aDescription}`);
+    is(aEvent.bubbles, true,
+       `"beforeinput" event should always bubble ${aDescription}`);
+  }
   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.inputType, expectedInputType,
-         `inputType should be "${expectedInputType}" ${aDescription}`);
+         `inputType of "input" event should be "${expectedInputType}" ${aDescription}`);
       is(aEvent.data, expectedData,
-         `data should be ${expectedData} ${aDescription}`);
+         `data of "input" event should be ${expectedData} ${aDescription}`);
       is(aEvent.dataTransfer, null,
-         `dataTransfer should be null ${aDescription}`);
+         `dataTransfer of "input" event should be null ${aDescription}`);
     }
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
   }
 
   function checkIfInputIsEvent(aEvent, aDescription) {
     ok(aEvent instanceof Event && !(aEvent instanceof UIEvent),
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
   }
 
-  var textareaInput = 0;
+  let textareaInput = 0, textareaBeforeInput = 0;
+  document.getElementById("textarea").onbeforeinput = (aEvent) => {
+    ++textareaBeforeInput;
+    checkBeforeInputEvent(aEvent, "on textarea element");
+  };
   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];
+  let textTypes = ["text", "email", "search", "tel", "url", "password"];
+  let textBeforeInput = [0, 0, 0, 0, 0, 0];
+  let 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).onbeforeinput = (aEvent) => {
+      ++textBeforeInput[textTypes.indexOf(aEvent.target.type)];
+      checkBeforeInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+    };
     document.getElementById(id).oninput = (aEvent) => {
       ++textInput[textTypes.indexOf(aEvent.target.type)];
       checkIfInputIsInputEvent(aEvent, false, `on input element whose type is ${aEvent.target.type}`);
     };
   }
 
   // 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];
+  let nonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
+  let nonTextBeforeInput = [0, 0, 0, 0, 0, 0];
+  let 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).onbeforeinput = (aEvent) => {
+      ++nonTextBeforeInput[nonTextTypes.indexOf(aEvent.target.type)];
+    };
     document.getElementById(id).oninput = (aEvent) => {
-      ++NonTextInput[NonTextTypes.indexOf(aEvent.target.type)];
+      ++nonTextInput[nonTextTypes.indexOf(aEvent.target.type)];
       checkIfInputIsEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
     };
   }
 
-  var rangeInput = 0;
+  var rangeInput = 0, rangeBeforeInput = 0;
+  document.getElementById("input_range").onbeforeinput = (aEvent) => {
+    ++rangeBeforeInput;
+  };
   document.getElementById("input_range").oninput = (aEvent) => {
     ++rangeInput;
     checkIfInputIsEvent(aEvent, "on input element whose type is range");
   };
 
-  var numberInput = 0;
+  var numberInput = 0, numberBeforeInput = 0;
+  document.getElementById("input_number").onbeforeinput = (aEvent) => {
+    ++numberBeforeInput;
+  };
   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("beforeinput", function (aEvent) {
+      ok(false, "beforeinput event shouldn't be dispatched on file input.");
+    });
     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);
+    SimpleTest.executeSoon(testUserInput2);
   }
 
   function testUserInput2() {
     // Some generic checks for types that support the input event.
     for (var i = 0; i < textTypes.length; ++i) {
       input = document.getElementById("input_" + textTypes[i]);
       input.focus();
       expectedInputType = "insertLineBreak";
       expectedData = null;
+      expectedBeforeInputCancelable = true;
       synthesizeKey("KEY_Enter");
+      is(textBeforeInput[i], 0, "beforeinput event shouldn't be dispatched on " + textTypes[i] + " input element");
       is(textInput[i], 0, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
 
       expectedInputType = "insertText";
       expectedData = "m";
+      expectedBeforeInputCancelable = true;
       sendString("m");
+      todo_is(textBeforeInput[i], 1, textTypes[i] + " input element should have dispatched beforeinput event.");
       is(textInput[i], 1, textTypes[i] + " input element should have dispatched input event.");
       expectedInputType = "insertLineBreak";
       expectedData = null;
+      expectedBeforeInputCancelable = true;
       synthesizeKey("KEY_Enter");
+      todo_is(textBeforeInput[i], 1, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
       is(textInput[i], 1, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
 
       expectedInputType = "deleteContentBackward";
       expectedData = null;
+      expectedBeforeInputCancelable = true;
       synthesizeKey("KEY_Backspace");
+      todo_is(textBeforeInput[i], 2, textTypes[i] + " input element should have dispatched beforeinput event.");
       is(textInput[i], 2, textTypes[i] + " input element should have dispatched input event.");
     }
 
     // Some scenarios of value changing from script and from user input.
     input = document.getElementById("input_text");
     input.focus();
     expectedInputType = "insertText";
     expectedData = "f";
+    expectedBeforeInputCancelable = true;
     sendString("f");
+    todo_is(textBeforeInput[0], 3, "beforeinput event should have been dispatched");
     is(textInput[0], 3, "input event should have been dispatched");
     input.blur();
+    todo_is(textBeforeInput[0], 3, "input event should not have been dispatched");
     is(textInput[0], 3, "input event should not have been dispatched");
 
     input.focus();
     input.value = 'foo';
+    todo_is(textBeforeInput[0], 3, "beforeinput event should not have been dispatched");
     is(textInput[0], 3, "input event should not have been dispatched");
     input.blur();
+    todo_is(textBeforeInput[0], 3, "beforeinput event should not have been dispatched");
     is(textInput[0], 3, "input event should not have been dispatched");
 
     input.focus();
     expectedInputType = "insertText";
     expectedData = "f";
+    expectedBeforeInputCancelable = true;
     sendString("f");
+    todo_is(textBeforeInput[0], 4, "beforeinput event should have been dispatched");
     is(textInput[0], 4, "input event should have been dispatched");
     input.value = 'bar';
+    todo_is(textBeforeInput[0], 4, "beforeinput event should not have been dispatched");
     is(textInput[0], 4, "input event should not have been dispatched");
     input.blur();
+    todo_is(textBeforeInput[0], 4, "beforeinput event should not have been dispatched");
     is(textInput[0], 4, "input event should not have been dispatched");
 
     // Same for textarea.
     var textarea = document.getElementById("textarea");
     textarea.focus();
     expectedInputType = "insertText";
     expectedData = "f";
+    expectedBeforeInputCancelable = true;
     sendString("f");
+    todo_is(textareaBeforeInput, 1, "beforeinput event should have been dispatched");
     is(textareaInput, 1, "input event should have been dispatched");
     textarea.blur();
+    todo_is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
     is(textareaInput, 1, "input event should not have been dispatched");
 
     textarea.focus();
     textarea.value = 'foo';
+    todo_is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
     is(textareaInput, 1, "input event should not have been dispatched");
     textarea.blur();
+    todo_is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
     is(textareaInput, 1, "input event should not have been dispatched");
 
     textarea.focus();
     expectedInputType = "insertText";
     expectedData = "f";
+    expectedBeforeInputCancelable = true;
     sendString("f");
+    todo_is(textareaBeforeInput, 2, "beforeinput event should have been dispatched");
     is(textareaInput, 2, "input event should have been dispatched");
     textarea.value = 'bar';
+    todo_is(textareaBeforeInput, 2, "beforeinput event should not have been dispatched");
     is(textareaInput, 2, "input event should not have been dispatched");
     expectedInputType = "deleteContentBackward";
     expectedData = null;
+    expectedBeforeInputCancelable = true;
     synthesizeKey("KEY_Backspace");
+    todo_is(textareaBeforeInput, 3, "beforeinput event should have been dispatched");
     is(textareaInput, 3, "input event should have been dispatched");
     textarea.blur();
+    todo_is(textareaBeforeInput, 3, "beforeinput event should not have been dispatched");
     is(textareaInput, 3, "input event should not have been dispatched");
 
     // Non-text input tests:
-    for (var i = 0; i < NonTextTypes.length; ++i) {
+    for (var i = 0; i < nonTextTypes.length; ++i) {
       // Button, submit, image and reset input type tests.
       if (i < 4) {
-        input = document.getElementById("input_" + NonTextTypes[i]);
+        input = document.getElementById("input_" + nonTextTypes[i]);
         input.focus();
         input.click();
-        is(NonTextInput[i], 0, "input event doesn't apply");
+        is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply");
+        is(nonTextInput[i], 0, "input event doesn't apply");
         input.blur();
-        is(NonTextInput[i], 0, "input event doesn't apply");
+        is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply");
+        is(nonTextInput[i], 0, "input event doesn't apply");
       }
       // For radio and checkboxes, input event should be dispatched.
       else {
-        input = document.getElementById("input_" + NonTextTypes[i]);
+        input = document.getElementById("input_" + nonTextTypes[i]);
         input.focus();
         input.click();
-        is(NonTextInput[i], 1, "input event should have been dispatched");
+        is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched");
+        is(nonTextInput[i], 1, "input event should have been dispatched");
         input.blur();
-        is(NonTextInput[i], 1, "input event should not have been dispatched");
+        is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched");
+        is(nonTextInput[i], 1, "input event should not have been dispatched");
 
         // Test that input event is not dispatched if click event is cancelled.
         function preventDefault(e) {
           e.preventDefault();
         }
         input.addEventListener("click", preventDefault);
         input.click();
-        is(NonTextInput[i], 1, "input event shouldn't be dispatched if click event is cancelled");
+        is(nonTextBeforeInput[i], 0, "beforeinput event shouldn't be dispatched if click event is cancelled");
+        is(nonTextInput[i], 1, "input event shouldn't be dispatched if click event is cancelled");
         input.removeEventListener("click", preventDefault);
       }
     }
 
     // Type changes.
     var input = document.createElement('input');
     input.type = 'text';
     input.value = 'foo';
+    input.onbeforeinput = function () {
+      ok(false, "we shouldn't get a beforeinput event when the type changes");
+    };
     input.oninput = function() {
       ok(false, "we shouldn't get an input event when the type changes");
     };
     input.type = 'range';
     isnot(input.value, 'foo');
 
     // Tests for type='range'.
     var range = document.getElementById("input_range");
 
     range.focus();
     sendString("a");
     range.blur();
+    is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on range input " +
+                            "element for key changes that don't change its value");
     is(rangeInput, 0, "input event shouldn't be dispatched on range input " +
                       "element for key changes that don't change its value");
 
     range.focus();
     synthesizeKey("KEY_Home");
+    is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched even for key changes");
     is(rangeInput, 1, "input event should be dispatched for key changes");
     range.blur();
+    is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on blur");
     is(rangeInput, 1, "input event shouldn't be dispatched on blur");
 
     range.focus();
     var bcr = range.getBoundingClientRect();
     var centerOfRangeX = bcr.width / 2;
     var centerOfRangeY = bcr.height / 2;
     synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" });
+    is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on mousedown if the value changes");
     is(rangeInput, 2, "Input event should be dispatched on mousedown if the value changes");
     synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" });
+    is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched during a drag");
     is(rangeInput, 3, "Input event should be dispatched during a drag");
     synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" });
+    is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched at the end of a drag");
     is(rangeInput, 4, "Input event should be dispatched at the end of a drag");
 
     // Tests for type='number'.
     // We only test key events here since input events for mouse event changes
     // are tested in test_input_number_mouse_events.html
     var number = document.getElementById("input_number");
 
     if (isDesktop) { // up/down arrow keys not supported on android/b2g
       number.value = "";
       number.focus();
       // <input type="number">'s inputType value hasn't been decided, see
       // https://github.com/w3c/input-events/issues/88
       expectedInputType = "";
       expectedData = null;
+      expectedBeforeInputCancelable = false;
       synthesizeKey("KEY_ArrowUp");
+      todo_is(numberBeforeInput, 1, "beforeinput event should be dispatched for up/down arrow key keypress");
       is(numberInput, 1, "input event should be dispatched for up/down arrow key keypress");
       is(number.value, "1", "sanity check value of number control after keypress");
 
       synthesizeKey("KEY_ArrowDown", {repeat: 3});
+      todo_is(numberBeforeInput, 4, "beforeinput event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
       is(numberInput, 4, "input event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
       is(number.value, "-2", "sanity check value of number control after multiple keydown events");
 
       number.blur();
+      todo_is(numberBeforeInput, 4, "beforeinput event shouldn't be dispatched on blur");
       is(numberInput, 4, "input event shouldn't be dispatched on blur");
     }
 
     MockFilePicker.cleanup();
     SimpleTest.finish();
   }
 
-  addLoadEvent(testUserInput);
+  SimpleTest.waitForExplicitFinish();
+  SimpleTest.waitForFocus(async () => {
+    await SpecialPowers.pushPrefEnv({
+      set: [["dom.input_events.beforeinput.enabled", true]],
+    });
+    testUserInput();
+  });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/tests/mochitest/general/test_clipboard_events.html
+++ b/dom/tests/mochitest/general/test_clipboard_events.html
@@ -34,17 +34,18 @@ var cachedCutData, cachedCopyData, cache
 
 add_task(async function initialize_for_tests() {
   await SimpleTest.promiseFocus();
 
   await new Promise(resolve => {
     SpecialPowers.pushPrefEnv({
       // NOTE: These tests operate under the assumption that the protected mode of
       // DataTransfer is enabled.
-      "set": [["dom.events.dataTransfer.protected.enabled", true]]
+      "set": [["dom.events.dataTransfer.protected.enabled", true],
+              ["dom.input_events.beforeinput.enabled", true]]
     }, resolve);
   });
 
   // Test that clearing and reading the clipboard works.  A random number
   // is used to make sure that leftover clipboard values from a previous
   // test run don't cause a false-positive test.
   var cb_text = "empty_" + Math.random();
 
@@ -186,172 +187,271 @@ add_task(async function test_input_oncop
 
   // Setup an oncopy event handler, fire copy.  Ensure that the event
   // handler was called, and the clipboard contents have been set to 'PUT TE',
   // which is the part that is selected below.
   selectContentInput();
   contentInput.focus();
   contentInput.setSelectionRange(2, 8);
 
-  var oncopy_fired = false;
-  var oninput_fired = false;
-  contentInput.oncopy = function() { oncopy_fired = true; };
-  contentInput.oninput = function () { oninput_fired = true; };
+  let oncopy_fired = false;
+  let onbeforeinput_fired = false;
+  let oninput_fired = false;
+  contentInput.oncopy = () => { oncopy_fired = true; };
+  contentInput.onbeforeinput = () => { onbeforeinput = true; };
+  contentInput.oninput = () => { oninput_fired = true; };
   try {
     await putOnClipboard("PUT TE", () => {
       synthesizeKey("c", {accelKey: 1});
     }, "copy on plaintext editor set clipboard correctly");
     ok(oncopy_fired, "copy event firing on plaintext editor");
+    ok(!onbeforeinput_fired, "beforeinput event shouldn't be fired on plaintext editor by copy");
     ok(!oninput_fired, "input event shouldn't be fired on plaintext editor by copy");
   } finally {
     contentInput.oncopy = null;
+    contentInput.onbeforeinput = null;
     contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_oncut() {
   await reset();
 
   // Setup an oncut event handler, and fire cut.  Ensure that the event
   // handler was fired, the clipboard contains the INPUT TEXT, and
   // that the input itself is empty.
   selectContentInput();
-  var oncut_fired = false;
-  var oninput_count = 0;
-  var inputType = "";
-  var data, dataTransfer;
-  contentInput.oncut = function() { oncut_fired = true; };
-  contentInput.oninput = function (aEvent) {
-    oninput_count++;
-    inputType = aEvent.inputType;
-    data = aEvent.data;
-    dataTransfer = aEvent.dataTransfer;
-  };
+  let oncut_fired = false;
+  let beforeInputEvents = [];
+  let inputEvents = [];
+  contentInput.oncut = () => { oncut_fired = true; };
+  contentInput.onbeforeinput = (aEvent) => { beforeInputEvents.push(aEvent); }
+  contentInput.oninput = (aEvent) => { inputEvents.push(aEvent); }
   try {
     await putOnClipboard("INPUT TEXT", () => {
       synthesizeKey("x", {accelKey: 1});
     }, "cut on plaintext editor set clipboard correctly");
     ok(oncut_fired, "cut event firing on plaintext editor");
-    is(oninput_count, 1, "input event should be fired once by cut");
-    is(inputType, "deleteByCut", "inputType of the input event should be \"deleteByCut\"");
-    is(data, null, "data of the input event should be null when inputType is \"deleteByCut\"");
-    is(dataTransfer, null, "dataTransfer of the input event should be null when inputType is \"deleteByCut\"");
+    todo_is(beforeInputEvents.length, 1, '"beforeinput" event should be fired once by cut');
+    if (beforeInputEvents.length > 0) {
+      is(beforeInputEvents[0].inputType, "deleteByCut", '"inputType" of "beforeinput" event should be "deleteByCut"');
+      is(beforeInputEvents[0].cancelable, true, '"beforeinput" event for "deleteByCut" should be cancelable');
+      is(beforeInputEvents[0].data, null, '"data" of "beforeinput" event for "deleteByCut" should be null');
+      is(beforeInputEvents[0].dataTransfer, null, '"dataTransfer" of "beforeinput" event for "deleteByCut" should be null');
+    }
+    is(inputEvents.length, 1, '"input" event should be fired once by cut');
+    if (inputEvents.length > 0) {
+      is(inputEvents[0].inputType, "deleteByCut", '"inputType" of "input" event should be "deleteByCut"');
+      is(inputEvents[0].cancelable, false, '"input" event for "deleteByCut" should not be cancelable');
+      is(inputEvents[0].data, null, '"data" of "input" event for "deleteByCut" should be null');
+      is(inputEvents[0].dataTransfer, null, '"dataTransfer" of "input" event for "deleteByCut" should be null');
+    }
     is(contentInput.value, "",
       "cut on plaintext editor emptied editor");
   } finally {
     contentInput.oncut = null;
+    contentInput.onbeforeinput = null;
     contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_onpaste() {
   await reset();
 
   // Setup an onpaste event handler, and fire paste.  Ensure that the event
   // handler was fired, the clipboard contents didn't change, and that the
   // input value did change (ie. paste succeeded).
   selectContentInput();
-  var onpaste_fired = false;
-  var oninput_count = 0;
-  var inputType = "";
-  var data, dataTransfer;
-  contentInput.onpaste = function() { onpaste_fired = true; };
-  contentInput.oninput = function(aEvent) {
-    oninput_count++;
-    inputType = aEvent.inputType;
-    data = aEvent.data;
-    dataTransfer = aEvent.dataTransfer;
-  };
+  let onpaste_fired = false;
+  let beforeInputEvents = [];
+  let inputEvents = [];
+  contentInput.onpaste = () => { onpaste_fired = true; };
+  contentInput.onbeforeinput = (aEvent) => { beforeInputEvents.push(aEvent); }
+  contentInput.oninput = (aEvent) => { inputEvents.push(aEvent); }
 
   try {
     synthesizeKey("v", {accelKey: 1});
     ok(onpaste_fired, "paste event firing on plaintext editor");
     is(getClipboardText(), clipboardInitialValue,
       "paste on plaintext editor did not modify clipboard contents");
-    is(oninput_count, 1, "input event should be fired once by cut");
-    is(inputType, "insertFromPaste", "inputType of the input event should be \"insertFromPaste\"");
-    is(data, clipboardInitialValue, `data of the input event should be ${clipboardInitialValue}`);
-    is(dataTransfer, null, "dataTransfer of the input event should be null even when inputType is \"insertFromPaste\"");
+    todo_is(beforeInputEvents.length, 1, '"beforeinput" event should be fired once by paste');
+    if (beforeInputEvents.length > 0) {
+      is(beforeInputEvents[0].inputType, "insertFromPaste", '"inputType" of "beforeinput" event should be "insertFromPaste"');
+      is(beforeInputEvents[0].cancelable, true, '"beforeinput" event for "insertFromPaste" should be cancelable');
+      is(beforeInputEvents[0].data, clipboardInitialValue, `"data" of "beforeinput" event for "insertFromPaste" should be "${clipboardInitialValue}"`);
+      is(beforeInputEvents[0].dataTransfer, null, '"dataTransfer" of "beforeinput" event for "insertFromPaste" should be null');
+    }
+    is(inputEvents.length, 1, '"input" event should be fired once by paste');
+    if (inputEvents.length > 0) {
+      is(inputEvents[0].inputType, "insertFromPaste", '"inputType" of "input" event should be "insertFromPaste"');
+      is(inputEvents[0].cancelable, false, '"input" event for "insertFromPaste" should not be cancelable');
+      is(inputEvents[0].data, clipboardInitialValue, `"data" of "input" event for "insertFromPaste" should be "${clipboardInitialValue}"`);
+      is(inputEvents[0].dataTransfer, null, '"dataTransfer" of "input" event for "insertFromPaste" should be null');
+    }
     is(contentInput.value, clipboardInitialValue,
       "paste on plaintext editor did modify editor value");
   } finally {
     contentInput.onpaste = null;
+    contentInput.onbeforeinput = null;
     contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_oncopy_abort() {
   await reset();
 
   // Setup an oncopy event handler, fire copy.  Ensure that the event
   // handler was called, and that the clipboard value did NOT change.
   selectContentInput();
-  var oncopy_fired = false;
-  contentInput.oncopy = function() { oncopy_fired = true; return false; };
+  let oncopy_fired = false;
+  contentInput.oncopy = () => { oncopy_fired = true; return false; };
+  contentInput.onbeforeinput = () => {
+    ok(false, '"beforeinput" event should not be fired by copy but canceled');
+  };
   contentInput.oninput = function() {
-    ok(false, "input event shouldn't be fired by copy but canceled");
+    ok(false, '"input" event should not be fired by copy but canceled');
   };
   try {
     await wontPutOnClipboard(clipboardInitialValue, () => {
       synthesizeKey("c", {accelKey: 1});
     }, "aborted copy on plaintext editor did not modify clipboard");
     ok(oncopy_fired, "copy event (to-be-cancelled) firing on plaintext editor");
   } finally {
     contentInput.oncopy = null;
+    contentInput.onbeforeinput = null;
     contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_oncut_abort() {
   await reset();
 
   // Setup an oncut event handler, and fire cut.  Ensure that the event
   // handler was fired, the clipboard contains the INPUT TEXT, and
   // that the input itself is empty.
   selectContentInput();
-  var oncut_fired = false;
-  contentInput.oncut = function() { oncut_fired = true; return false; };
-  contentInput.oninput = function() {
-    ok(false, "input event shouldn't be fired by cut but canceled");
+  let oncut_fired = false;
+  contentInput.oncut = () => { oncut_fired = true; return false; };
+  contentInput.onbeforeinput = () => {
+    ok(false, '"beforeinput" event should not be fired by cut but canceled by "cut" event listener');
+  };
+  contentInput.oninput = () => {
+    ok(false, '"input" event should not be fired by cut but canceled by "cut" event listener');
   };
   try {
     await wontPutOnClipboard(clipboardInitialValue, () => {
       synthesizeKey("x", {accelKey: 1});
     }, "aborted cut on plaintext editor did not modify clipboard");
     ok(oncut_fired, "cut event (to-be-cancelled) firing on plaintext editor");
     is(contentInput.value, "INPUT TEXT",
       "aborted cut on plaintext editor did not modify editor contents");
   } finally {
     contentInput.oncut = null;
+    contentInput.onbeforeinput = null;
+    contentInput.oninput = null;
+  }
+});
+
+add_task(async function test_input_oncut_beforeinput_abort() {
+  await reset();
+
+  // Setup an oncut event handler, and fire cut.  Ensure that the event
+  // handler was fired, the clipboard contains the INPUT TEXT, and
+  // that the input itself is empty.
+  selectContentInput();
+  let oncut_fired = false;
+  let beforeInputEvents = [];
+  let inputEvents = [];
+  contentInput.oncut = () => { oncut_fired = true; };
+  contentInput.onbeforeinput = (aEvent) => { beforeInputEvents.push(aEvent); aEvent.preventDefault(); }
+  contentInput.oninput = (aEvent) => { inputEvents.push(aEvent); }
+  try {
+    await putOnClipboard("INPUT TEXT", () => {
+      synthesizeKey("x", {accelKey: 1});
+    }, "cut on plaintext editor set clipboard correctly");
+    ok(oncut_fired, "cut event firing on plaintext editor");
+    todo_is(beforeInputEvents.length, 1, '"beforeinput" event should be fired once by cut');
+    if (beforeInputEvents.length > 0) {
+      is(beforeInputEvents[0].inputType, "deleteByCut", '"inputType" of "beforeinput" event should be "deleteByCut"');
+      is(beforeInputEvents[0].cancelable, true, '"beforeinput" event for "deleteByCut" should be cancelable');
+      is(beforeInputEvents[0].data, null, '"data" of "beforeinput" event for "deleteByCut" should be null');
+      is(beforeInputEvents[0].dataTransfer, null, '"dataTransfer" of "beforeinput" event for "deleteByCut" should be null');
+    }
+    todo_is(inputEvents.length, 0, '"input" event should not be fired by cut if "beforeinput" event is canceled');
+    todo_is(contentInput.value, "INPUT TEXT",
+      'cut on plaintext editor should not change editor since "beforeinput" event was canceled');
+  } finally {
+    contentInput.oncut = null;
+    contentInput.onbeforeinput = null;
     contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_onpaste_abort() {
   await reset();
 
   // Setup an onpaste event handler, and fire paste.  Ensure that the event
   // handler was fired, the clipboard contents didn't change, and that the
   // input value did change (ie. paste succeeded).
   selectContentInput();
-  var onpaste_fired = false;
-  contentInput.onpaste = function() { onpaste_fired = true; return false; };
-  contentInput.oninput = function() {
-    ok(false, "input event shouldn't be fired by paste but canceled");
+  let onpaste_fired = false;
+  contentInput.onpaste = () => { onpaste_fired = true; return false; };
+  contentInput.onbeforeinput = () => {
+    ok(false, '"beforeinput" event should not be fired by paste but canceled');
+  };
+  contentInput.oninput = () => {
+    ok(false, '"input" event should not be fired by paste but canceled');
   };
   try {
     synthesizeKey("v", {accelKey: 1});
     ok(onpaste_fired,
       "paste event (to-be-cancelled) firing on plaintext editor");
     is(getClipboardText(), clipboardInitialValue,
       "aborted paste on plaintext editor did not modify clipboard");
     is(contentInput.value, "INPUT TEXT",
       "aborted paste on plaintext editor did not modify modified editor value");
   } finally {
     contentInput.onpaste = null;
+    contentInput.onbeforeinput = null;
+    contentInput.oninput = null;
+  }
+});
+
+add_task(async function test_input_onpaste_beforeinput_abort() {
+  await reset();
+
+  // Setup an onpaste event handler, and fire paste.  Ensure that the event
+  // handler was fired, the clipboard contents didn't change, and that the
+  // input value did change (ie. paste succeeded).
+  selectContentInput();
+  let onpaste_fired = false;
+  let beforeInputEvents = [];
+  let inputEvents = [];
+  contentInput.onpaste = () => { onpaste_fired = true; };
+  contentInput.onbeforeinput = (aEvent) => { beforeInputEvents.push(aEvent); aEvent.preventDefault(); }
+  contentInput.oninput = (aEvent) => { inputEvents.push(aEvent); }
+
+  try {
+    synthesizeKey("v", {accelKey: 1});
+    ok(onpaste_fired, "paste event firing on plaintext editor");
+    is(getClipboardText(), clipboardInitialValue,
+      "paste on plaintext editor did not modify clipboard contents");
+    todo_is(beforeInputEvents.length, 1, '"beforeinput" event should be fired once by paste');
+    if (beforeInputEvents.length > 0) {
+      is(beforeInputEvents[0].inputType, "insertFromPaste", '"inputType" of "beforeinput" event should be "insertFromPaste"');
+      is(beforeInputEvents[0].cancelable, true, '"beforeinput" event for "insertFromPaste" should be cancelable');
+      is(beforeInputEvents[0].data, clipboardInitialValue, `"data" of "beforeinput" event for "insertFromPaste" should be "${clipboardInitialValue}"`);
+      is(beforeInputEvents[0].dataTransfer, null, '"dataTransfer" of "beforeinput" event for "insertFromPaste" should be null');
+    }
+    todo_is(inputEvents.length, 0, '"input" event should not be fired by paste when "beforeinput" is canceled');
+    todo_is(contentInput.value, "INPUT TEXT",
+      "paste on plaintext editor did modify editor value");
+  } finally {
+    contentInput.onpaste = null;
+    contentInput.onbeforeinput = null;
     contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_cut_dataTransfer() {
   await reset();
 
   // Cut using event.dataTransfer. The event is not cancelled so the default
--- a/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
+++ b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
@@ -17,16 +17,20 @@
 <div id="clickaway" style="position: absolute; top: 250px; width: 10px; height: 10px; z-index: 100;"></div>
 <img src="green.png"><!-- for ensuring to load the image at first test of <img> case -->
 <pre id="test">
 <script type="application/javascript">
 "use strict";
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(async function() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
   document.execCommand("enableAbsolutePositionEditing", false, true);
   ok(document.queryCommandState("enableAbsolutePositionEditing"),
      "Absolute positioned element editor should be enabled by the call of execCommand");
 
   let outOfEditor = document.getElementById("clickaway");
 
   function cancel(e) { e.stopPropagation(); }
   let content = document.getElementById("content");
@@ -67,53 +71,83 @@ 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 beforeInputEventExpected = true;
+      let beforeInputFired = false;
       let inputEventExpected = true;
-      function onInput(aEvent) {
-        if (!inputEventExpected) {
-          ok(false, "\"input\" event shouldn't be fired after stopping resizing");
+      let inputFired = false;
+      function onBeforeInput(aEvent) {
+        beforeInputFired = true;
+        aEvent.preventDefault();  // For making sure this preventDefault() call does not cancel the operation.
+        if (!beforeInputEventExpected) {
+          ok(false, '"beforeinput" event should not be fired after stopping resizing');
           return;
         }
         ok(aEvent instanceof InputEvent,
-           '"input" event should be dispatched with InputEvent interface');
+           '"beforeinput" event for position changing of absolute position should be dispatched with InputEvent interface');
         is(aEvent.cancelable, false,
-           '"input" event should be never cancelable');
+           '"beforeinput" event for position changing of absolute position container should not be cancelable');
         is(aEvent.bubbles, true,
-           '"input" event should always bubble');
+           '"beforeinput" event for position changing of absolute position should always bubble');
         is(aEvent.inputType, "",
-           "inputType should be empty string when an element is moved");
+           'inputType of "beforeinput" event for position changing of absolute position should be empty string');
         is(aEvent.data, null,
-           "data should be null when an element is moved");
+           'data of "beforeinput" event for position changing of absolute position should be null');
         is(aEvent.dataTransfer, null,
-           "dataTransfer should be null when an element is moved");
+           'dataTransfer of "beforeinput" event for position changing of absolute position should be null');
+      }
+      function onInput(aEvent) {
+        inputFired = true;
+        if (!inputEventExpected) {
+          ok(false, '"input" event should not be fired after stopping resizing');
+          return;
+        }
+        ok(aEvent instanceof InputEvent,
+           '"input" event for position changing of absolute position container should be dispatched with InputEvent interface');
+        is(aEvent.cancelable, false,
+           '"input" event for position changing of absolute position container should be never cancelable');
+        is(aEvent.bubbles, true,
+           '"input" event for position changing of absolute position should always bubble');
+        is(aEvent.inputType, "",
+           'inputType of "input" event for position changing of absolute position should be empty string');
+        is(aEvent.data, null,
+           'data of "input" event for position changing of absolute position should be null');
+        is(aEvent.dataTransfer, null,
+           'dataTransfer of "input" event for position changing of absolute position should be null');
       }
 
+      content.addEventListener("beforeinput", onBeforeInput);
       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"});
 
+      todo(beforeInputFired, `${description}"beforeinput" event should be fired by moving absolute position container`);
+      ok(inputFired, `${description}"input" event should be fired by moving absolute position container`);
+
+      beforeInputEventExpected = false;
       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("beforeinput", onBeforeInput);
       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");
     }
 
--- a/editor/libeditor/tests/test_bug520189.html
+++ b/editor/libeditor/tests/test_bug520189.html
@@ -531,24 +531,34 @@ function runTest(test) {
     var editor, win;
     if ("isIFrame" in test) {
       win = elem.contentDocument.defaultView;
     } else {
       getSelection().collapse(elem, 0);
       win = window;
     }
     editor = SpecialPowers.wrap(win).docShell.editor;
+    let beforeInputEvent = null;
     let inputEvent = null;
+    win.addEventListener("beforeinput", aEvent => { beforeInputEvent = aEvent; }, {once: true});
     win.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true});
     editor.pasteTransferable(trans);
-    is(inputEvent.type, "input", "input event should be fired");
-    is(inputEvent.inputType, "insertFromPaste", "inputType should be insertFromPaste");
-    is(inputEvent.data, null, "data should be null");
-    is(inputEvent.dataTransfer.getData("text/html"), test.payload, "dataTransfer should have the HTML data");
-    is(inputEvent.dataTransfer.getData("text/plain"), "", "dataTransfer shouldn't have plain text");
+    todo_isnot(beforeInputEvent, null, '"beforeinput" event should be fired');
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, '"beforeinput" event for "insertFromPaste" should be cancelable');
+      is(beforeInputEvent.inputType, "insertFromPaste", `inputType of "beforeinput" event should be "insertFromPaste"`);
+      is(beforeInputEvent.data, null, 'data of "beforeinput" event should be null');
+      is(beforeInputEvent.dataTransfer.getData("text/html"), test.payload, 'dataTransfer of "beforeinput" event should have the HTML data');
+      is(beforeInputEvent.dataTransfer.getData("text/plain"), "", 'dataTransfer of "beforeinput" event should not have have plain text');
+    }
+    is(inputEvent.type, "input", '"input" event should be fired');
+    is(inputEvent.inputType, "insertFromPaste", `inputType of "input" event should be "insertFromPaste"`);
+    is(inputEvent.data, null, 'data of "input" event should be null');
+    is(inputEvent.dataTransfer.getData("text/html"), test.payload, 'dataTransfer of "input" event should have the HTML data');
+    is(inputEvent.dataTransfer.getData("text/plain"), "", 'dataTransfer of "input" event should not have have plain text');
   } else {
     var clipboard = SpecialPowers.Services.clipboard;
 
     clipboard.setData(trans, null, SpecialPowers.Ci.nsIClipboard.kGlobalClipboard);
 
     synthesizeKey("V", {accelKey: true});
   }
 
@@ -568,15 +578,16 @@ function runTest(test) {
     }
   }
 }
 
 SimpleTest.waitForExplicitFinish();
 
 addLoadEvent(function() {
   SpecialPowers.pushPrefEnv(
-    { "set": [["layout.css.moz-document.content.enabled", true]]},
+    { "set": [["layout.css.moz-document.content.enabled", true],
+              ["dom.input_events.beforeinput.enabled", true]]},
     doNextTest);
 });
 </script>
 </pre>
 </body>
 </html>
--- a/editor/libeditor/tests/test_bug596333.html
+++ b/editor/libeditor/tests/test_bug596333.html
@@ -46,23 +46,32 @@ function getLoadContext() {
 function paste(str) {
   var Cc = SpecialPowers.Cc;
   var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
   trans.init(getLoadContext());
   var s = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
   s.data = str;
   trans.setTransferData("text/unicode", s, str.length * 2);
 
+  let beforeInputEvent = null;
   let inputEvent = null;
+  window.addEventListener("beforeinput", aEvent => { beforeInputEvent = aEvent; }, {once: true});
   window.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true});
   getEditor().pasteTransferable(trans);
-  is(inputEvent.type, "input", "input event should be fired");
-  is(inputEvent.inputType, "insertFromPaste", "inputType should be insertFromPaste");
-  is(inputEvent.data, str, `data should be "${str}"`);
-  is(inputEvent.dataTransfer, null, "dataTransfer should be null on <textarea>");
+  todo_isnot(beforeInputEvent, null, '"beforeinput" event should be fired');
+  if (beforeInputEvent) {
+    is(beforeInputEvent.cancelable, true, '"beforeinput" event for "insertFromPaste" should be cancelable');
+    is(beforeInputEvent.inputType, "insertFromPaste", 'inputType of "beforeinput" event should be "insertFromPaste"');
+    is(beforeInputEvent.data, str, `data of "beforeinput" event should be "${str}"`);
+    is(beforeInputEvent.dataTransfer, null, 'dataTransfer of "beforeinput" event should be null on <textarea>');
+  }
+  is(inputEvent.type, "input", '"input" event should be fired');
+  is(inputEvent.inputType, "insertFromPaste", '"inputType of "input" event should be "insertFromPaste"');
+  is(inputEvent.data, str, `data of "input" event should be "${str}"`);
+  is(inputEvent.dataTransfer, null, 'dataTransfer of "input" event should be null on <textarea>');
 }
 
 function runOnFocus() {
   var edit = document.getElementById("edit");
 
   gMisspeltWords = ["haz", "cheezburger"];
   ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
      "All misspellings before editing are accounted for.");
@@ -101,17 +110,21 @@ function runOnFocus() {
 
           SimpleTest.finish();
         });
       });
     });
   });
 }
 
-function runTest() {
+async function runTest() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
   var edit = document.getElementById("edit");
   edit.focus();
 
   onSpellCheck = SpecialPowers.Cu.import(
     "resource://testing-common/AsyncSpellCheckTestHelper.jsm", null).onSpellCheck;
   onSpellCheck(edit, runOnFocus);
 }
 </script>
--- a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
+++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
@@ -16,462 +16,1272 @@
 </div>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="application/javascript">
+"use strict";
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(runTests, window);
 
 const kIsWin = navigator.platform.indexOf("Win") == 0;
 const kIsMac = navigator.platform.indexOf("Mac") == 0;
 
-function runTests() {
+// TODO: When we remove `beforeInputEvent[0]` instance check, we should enable complexity check again.
+/* eslint-disable complexity */
+async function runTests() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
+  const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word");
+
   function doTests(aDocument, aWindow, aDescription) {
     aDescription += ": ";
     aWindow.focus();
 
-    var body = aDocument.body;
-    var selection = aWindow.getSelection();
+    let body = aDocument.body;
+    let selection = aWindow.getSelection();
 
-    var eventTarget = aDocument.getElementById("eventTarget");
+    function getHTMLEditor() {
+      let editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession;
+      if (!editingSession) {
+        return null;
+      }
+      let editor = editingSession.getEditorForWindow(aWindow);
+      if (!editor) {
+        return null;
+      }
+      return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+    }
+    let htmlEditor = getHTMLEditor();
+
+    let eventTarget = aDocument.getElementById("eventTarget");
     // The event target must be focusable because it's the editing host.
     eventTarget.focus();
 
-    var editTarget = aDocument.getElementById("editTarget");
+    let editTarget = aDocument.getElementById("editTarget");
     if (!editTarget) {
       editTarget = eventTarget;
     }
 
     // Root element never can be edit target.  If the editTarget is the root
     // element, replace with its body.
+    let isEditTargetIsDescendantOfEditingHost = false;
     if (editTarget == aDocument.documentElement) {
       editTarget = body;
+      isEditTargetIsDescendantOfEditingHost = true;
     }
 
     editTarget.innerHTML = "";
 
     // If the editTarget isn't its editing host, move caret to the start of it.
     if (eventTarget != editTarget) {
       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");
+    let cancelBeforeInput = false;
+    let todoMultipleInputEvents = false;
+    let beforeInputEvent = null;
+    let inputEvent = null;
+    let action = "";
+    let beforeInputHandler = (aEvent) => {
+      if (cancelBeforeInput) {
+        aEvent.preventDefault();
+      }
+      if (todoMultipleInputEvents && beforeInputEvent) {
+        todo(!beforeInputEvent, `${aDescription}Multiple "beforeinput" events are fired at ${action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+        return;
+      }
+      ok(!beforeInputEvent, `${aDescription}Multiple "beforeinput" events are fired at ${action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+      ok(aEvent.isTrusted, `${aDescription}"beforeinput" event at ${action} must be trusted`);
+      is(aEvent.target, eventTarget, `${aDescription}"beforeinput" event at ${action} is fired on unexpected element: ${aEvent.target.tagName}`);
+      ok(aEvent instanceof InputEvent, `${aDescription}"beforeinput" event at ${action} should be dispatched with InputEvent interface`);
+      ok(aEvent.bubbles, `${aDescription}"beforeinput" event at ${action} must be bubbles`);
+      beforeInputEvent = aEvent;
+    };
+    let inputHandler = (aEvent) => {
+      if (todoMultipleInputEvents && inputEvent) {
+        todo(!inputEvent, `${aDescription}Multiple "input" events are fired at ${action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+        return;
+      }
+      ok(!inputEvent, `${aDescription}Multiple "input" events are fired at ${action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+      ok(aEvent.isTrusted, `${aDescription}"input" event at ${action} must be trusted`);
+      is(aEvent.target, eventTarget, `${aDescription}"input" event at ${action} is fired on unexpected element: ${aEvent.target.tagName}`);
+      ok(aEvent instanceof InputEvent, `${aDescription}"input" event at ${action} should be dispatched with InputEvent interface`);
+      ok(!aEvent.cancelable, `${aDescription}"input" event at ${action} must not be cancelable`);
+      ok(aEvent.bubbles, `${aDescription}"input" event at ${action} must be bubbles`);
       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)");
+         `${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` +
+         `the current time but it differed by ${duration}ms)`);
       inputEvent = aEvent;
     };
 
-    aWindow.addEventListener("input", handler, true);
+    aWindow.addEventListener("beforeinput", beforeInputHandler, true);
+    aWindow.addEventListener("input", inputHandler, true);
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'inserting "a"';
+    synthesizeKey("a", {}, aWindow);
+    is(editTarget.innerHTML, "a", `${aDescription}"a" should've been inserted by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertText"`);
+      is(beforeInputEvent.data, "a", `${aDescription}data of "beforeinput" event for ${action} should be "a"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertText", `${aDescription}inputType of "input" event for ${action} should be "insertText"`);
+    is(inputEvent.data, "a", `${aDescription}data of "input" event for ${action} should be "a"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'inserting "b"';
+    synthesizeKey("b", {}, aWindow);
+    todo_is(editTarget.innerHTML, "a", `${aDescription}"a" shouldn't have been modified by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("a", { }, aWindow);
-    is(editTarget.innerHTML, "a", aDescription + "wrong element was edited");
-    ok(inputEvent, aDescription + "input event wasn't fired by 'a' key");
-    ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event");
-    is(inputEvent.inputType, "insertText",
-       aDescription + 'inputType should be "insertText" when typing "a"');
-    is(inputEvent.data, "a",
-       aDescription + 'data should be "a" when typing "a"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when typing "a"');
+    editTarget.innerHTML = "ab";
+    selection.collapse(editTarget.firstChild, 2);
+    action = 'removing "a" with "Backspace" (with collapsed selection)';
+    synthesizeKey("KEY_Backspace", {}, aWindow);
+    todo_is(editTarget.innerHTML, "ab", `${aDescription}"a" shouldn't have been modified by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("KEY_Backspace", { }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key");
-    ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event");
-    is(inputEvent.inputType, "deleteContentBackward",
-       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Backspace" with collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Backspace" with collapsed selection');
+    editTarget.innerHTML = "a";
+    selection.collapse(editTarget.firstChild, 1);
+    synthesizeKey("KEY_Backspace", {}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}"a" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentBackward",
+        `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteContentBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'typing "Backspace" in empty editor';
+    synthesizeKey("KEY_Backspace", {}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}$shouldn't change empty editor by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should be fired at ${action} even if it won't remove any content`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentBackward",
+        `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    if (!isEditTargetIsDescendantOfEditingHost) {
+      ok(!inputEvent, `${aDescription}"input" event shouldn't be fired at ${action}`);
+    } else {
+      todo(!inputEvent, `${aDescription}"input" event should be fired at ${action} but we replace the padding <br> element`);
+    }
+
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("B", { shiftKey: true }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by 'B' key");
-    ok(inputEvent.isTrusted, aDescription + "input event by 'B' key wasn't trusted event");
-    is(inputEvent.inputType, "insertText",
-       aDescription + 'inputType should be "insertText" when typing "B"');
-    is(inputEvent.data, "B",
-       aDescription + 'data should be "B" when typing "B"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when typing "B"');
+    action = 'typing "B"';
+    synthesizeKey("B", {shiftKey: true}, aWindow);
+    // XXX This inconsistency must be a bug.
+    if (!isEditTargetIsDescendantOfEditingHost) {
+      is(editTarget.innerHTML, "B<br>", `${aDescription}"B" should've been inserted by ${action}`);
+    } else {
+      is(editTarget.innerHTML, "B", `${aDescription}"B" should've been inserted by ${action}`);
+    }
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertText"`);
+      is(beforeInputEvent.data, "B", `${aDescription}data of "beforeinput" event for ${action} should be "B"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertText", `${aDescription}inputType of "input" event for ${action} should be "insertText"`);
+    is(inputEvent.data, "B", `${aDescription}data of "input" event for ${action} should be "B"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("KEY_Enter", { }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Enter key");
-    ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event");
-    is(inputEvent.inputType, "insertParagraph",
-       aDescription + 'inputType should be "insertParagraph" when pressing "Enter"');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Enter"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Enter"');
+    action = 'typing "Enter"';
+    synthesizeKey("KEY_Enter", {}, aWindow);
+    // XXX This inconsistency must be a bug.
+    if (!isEditTargetIsDescendantOfEditingHost) {
+      todo_is(editTarget.innerHTML, "B<br>", `${aDescription}shouldn't modify the editor by ${action} since "beforeinput" was canceled`);
+    } else {
+      todo_is(editTarget.innerHTML, "B", `${aDescription}shouldn't modify the editor by ${action} since "beforeinput" was canceled`);
+    }
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("C", { shiftKey: true }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by 'C' key");
-    ok(inputEvent.isTrusted, aDescription + "input event by 'C' key wasn't trusted event");
-    is(inputEvent.inputType, "insertText",
-       aDescription + 'inputType should be "insertText" when typing "C"');
-    is(inputEvent.data, "C",
-       aDescription + 'data should be "C" when typing "C"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when typing "C"');
+    action = 'typing "Enter"';
+    editTarget.innerHTML = "B";
+    selection.collapse(editTarget.firstChild, 1);
+    synthesizeKey("KEY_Enter", {}, aWindow);
+    if (!isEditTargetIsDescendantOfEditingHost) {
+      is(editTarget.innerHTML, "<div>B</div><div><br></div>", `${aDescription}should insert new paragraph by ${action}`);
+    } else {
+      // XXX Perhaps, this is a bug since we shouldn't change behavior when contenteditable element is <html> or <body>.
+      is(editTarget.innerHTML, "B<br><br>", `${aDescription}should insert new paragraph by ${action}`);
+    }
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertParagraph", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertParagraph"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertParagraph", `${aDescription}inputType of "input" event for ${action} should be "insertParagraph"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("KEY_Enter", { }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Enter key (again)");
-    ok(inputEvent.isTrusted, aDescription + "input event by Enter key (again) wasn't trusted event");
-    is(inputEvent.inputType, "insertParagraph",
-       aDescription + 'inputType should be "insertParagraph" when pressing "Enter" again');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Enter" again');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Enter" again');
+    action = 'typing "C" in new paragraph';
+    synthesizeKey("C", {shiftKey: true}, aWindow);
+    if (!isEditTargetIsDescendantOfEditingHost) {
+      is(editTarget.innerHTML, "<div>B</div><div>C<br></div>", `${aDescription}should insert "C" into the new paragraph by ${action}`);
+    } else {
+      is(editTarget.innerHTML, "B<br>C<br>", `${aDescription}should insert "C" into the new paragraph by ${action}`);
+    }
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertText"`);
+      is(beforeInputEvent.data, "C", `${aDescription}data of "beforeinput" event for ${action} should be "C"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertText", `${aDescription}inputType of "input" event for ${action} should be "insertText"`);
+    is(inputEvent.data, "C", `${aDescription}data of "input" event for ${action} should be "C"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'typing "Enter" again';
+    synthesizeKey("KEY_Enter", {}, aWindow);
+    if (!isEditTargetIsDescendantOfEditingHost) {
+      is(editTarget.innerHTML, "<div>B</div><div>C</div><div><br></div>", `${aDescription}should insert new paragraph again by ${action}`);
+    } else {
+      is(editTarget.innerHTML, "B<br>C<br><br>", `${aDescription}should insert new paragraph again by ${action}`);
+    }
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertParagraph", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertParagraph"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertParagraph", `${aDescription}inputType of "input" event for ${action} should be "insertParagraph"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    beforeInputEvent = null;
     inputEvent = null;
     editTarget.innerHTML = "foo-bar";
-    ok(!inputEvent, aDescription + "input event was fired by setting value");
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event should not be fired when setting value`);
+    ok(!inputEvent, `${aDescription}"input" event should not be fired when setting value`);
 
+    beforeInputEvent = null;
     inputEvent = null;
     editTarget.innerHTML = "";
-    ok(!inputEvent, aDescription + "input event was fired by setting empty value");
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event should not be fired when setting empty value`);
+    ok(!inputEvent, `${aDescription}"input" event should not be fired when setting empty value`);
 
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey(" ", { }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Space key");
-    ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event");
-    is(inputEvent.inputType, "insertText",
-       aDescription + 'inputType should be "insertText" when typing " "');
-    is(inputEvent.data, " ",
-       aDescription + 'data should be " " when typing " "');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when typing " "');
+    action = 'inserting " " into empty editor';
+    synthesizeKey(" ", {}, aWindow);
+    is(editTarget.innerHTML, "&nbsp;", `${aDescription}" " should've been inserted by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertText"`);
+      is(beforeInputEvent.data, " ", `${aDescription}data of "beforeinput" event for ${action} should be " "`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertText", `${aDescription}inputType of "input" event for ${action} should be "insertText"`);
+    is(inputEvent.data, " ", `${aDescription}data of "input" event for ${action} should be " "`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'typing "Delete" at end';
+    synthesizeKey("KEY_Delete", {}, aWindow);
+    is(editTarget.innerHTML, "&nbsp;", `${aDescription}shouldn't modify the editor by ${action} since there is no content to remove`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should be fired at ${action} even if it won't remove any content`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(!inputEvent, `${aDescription}${action} should not fire "input" event since no content has been removed`);
+
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("KEY_Delete", { }, aWindow);
-    ok(!inputEvent, aDescription + "input event was fired by Delete key at the end");
+    action = 'typing "ArrowLeft"';
+    synthesizeKey("KEY_ArrowLeft", {}, aWindow);
+    ok(!beforeInputEvent, `${aDescription}${action} should not fire "beforeinput" event since no content has been modified`);
+    ok(!inputEvent, `${aDescription}${action} should not fire "input" event since no content has been modified`);
 
-    inputEvent = null;
-    synthesizeKey("KEY_ArrowLeft", { }, aWindow);
-    ok(!inputEvent, aDescription + "input event was fired by Left key");
-
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("KEY_Delete", { }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start");
-    ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event");
-    is(inputEvent.inputType, "deleteContentForward",
-       aDescription + 'inputType should be "deleteContentForward" when pressing "Delete" with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Delete" with collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Delete" with collapsed selection');
+    action = 'typing "Delete"';
+    synthesizeKey("KEY_Delete", {}, aWindow);
+    todo_is(editTarget.innerHTML, "&nbsp;", `${aDescription}"\u00A0" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    // Reset undo/redo transaction for the following undo/redo tests.
+    htmlEditor.enableUndo(false);
+    htmlEditor.enableUndo(true);
+
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    editTarget.innerHTML = "\u00A0";
+    selection.collapse(editTarget.firstChild, 0);
+    synthesizeKey("KEY_Delete", {}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}" " should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "input" event for ${action} should be "deleteContentForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // TODO: Check canceling "beforeinput" case of "historyUndo" here.
 
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("z", { accelKey: true }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Undo");
-    ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event");
-    is(inputEvent.inputType, "historyUndo",
-       aDescription + 'inputType should be "historyUndo" when doing "Undo"');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when doing "Undo"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when doing "Undo"');
+    action = 'doing "Undo"';
+    synthesizeKey("z", {accelKey: true}, aWindow);
+    is(editTarget.innerHTML, "&nbsp;", `${aDescription}" " should've been restored by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "historyUndo", `${aDescription}inputType of "beforeinput" event for ${action} should be "historyUndo"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "historyUndo", `${aDescription}inputType of "input" event for ${action} should be "historyUndo"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'doing "Undo" again';
+    synthesizeKey("z", {accelKey: true}, aWindow);
+    is(editTarget.innerHTML, "&nbsp;", `${aDescription}the editor shouldn't have been modified by ${action} since there is no undo transaction`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action} since there is no undo transaction`);
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since there is no undo transaction`);
+
+    // TODO: Check canceling "beforeinput" case of "historyRedo" here.
+
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("z", { accelKey: true, shiftKey: true }, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Redo");
-    ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event");
-    is(inputEvent.inputType, "historyRedo",
-       aDescription + 'inputType should be "historyRedo" when doing "Redo"');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when doing "Redo"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when doing "Redo"');
+    action = 'doing "Redo"';
+    synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}the padding <br> should've been restored by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "historyRedo", `${aDescription}inputType of "beforeinput" event for ${action} should be "historyRedo"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "historyRedo", `${aDescription}inputType of "input" event for ${action} should be "historyRedo"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'doing "Redo" again';
+    synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}the editor shouldn't have been modified by ${action} since there is no redo transaction`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action} since there is no redo transaction`);
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since there is no redo transaction`);
 
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = "inserting a line break";
     synthesizeKey("KEY_Enter", {shiftKey: true}, aWindow);
-    ok(inputEvent, aDescription + "input event wasn't fired by Shift + Enter key");
-    ok(inputEvent.isTrusted, aDescription + "input event by Shift + Enter key wasn't trusted event");
-    is(inputEvent.inputType, "insertLineBreak",
-       aDescription + 'inputType should be "insertLineBreak" when pressing Shift + "Enter"');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing Shift + "Enter"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing Shift + "Enter"');
+    todo_is(editTarget.innerHTML, "<br>", `${aDescription}shouldn't modify the editor by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    editTarget.innerHTML = "<br>";
+    selection.collapse(editTarget, 0);
+    synthesizeKey("KEY_Enter", {shiftKey: true}, aWindow);
+    is(editTarget.innerHTML, "<br><br>", `${aDescription}should insert new <br> element by ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertLineBreak", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertLineBreak"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertLineBreak", `${aDescription}inputType of "input" event for ${action} should be "insertLineBreak"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     // Backspace/Delete with non-collapsed selection.
     editTarget.innerHTML = "a";
     editTarget.focus();
     selection.selectAllChildren(editTarget);
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'removing "a" with "Backspace" (with selection)';
     synthesizeKey("KEY_Backspace", {}, aWindow);
-    ok(inputEvent,
-       aDescription + 'input event should be fired by pressing "Backspace" with non-collapsed selection');
-    ok(inputEvent.isTrusted,
-       aDescription + 'input event should be trusted when pressing "Backspace" with non-collapsed selection');
-    is(inputEvent.inputType, "deleteContentBackward",
-       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with non-collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Backspace" with non-collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Backspace" with non-collapsed selection');
+    todo_is(editTarget.innerHTML, "a", `${aDescription}"a" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
 
     editTarget.innerHTML = "a";
     editTarget.focus();
     selection.selectAllChildren(editTarget);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    synthesizeKey("KEY_Delete", {}, aWindow);
-    ok(inputEvent,
-       aDescription + 'input event should be fired by pressing "Delete" with non-collapsed selection');
-    ok(inputEvent.isTrusted,
-       aDescription + 'input event should be trusted when pressing "Delete" with non-collapsed selection');
-    is(inputEvent.inputType, "deleteContentForward",
-       aDescription + 'inputType should be "deleteContentBackward" when Delete "Backspace" with non-collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when Delete "Backspace" with non-collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when Delete "Backspace" with non-collapsed selection');
+    synthesizeKey("KEY_Backspace", {}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}"a" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentBackward",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteContentBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
-    // Delete to previous/next word boundary with collapsed selection.
     editTarget.innerHTML = "a";
     editTarget.focus();
     selection.selectAllChildren(editTarget);
-    selection.collapseToEnd();
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to previous word boundary with collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to previous word boundary with collapsed selection");
-    is(inputEvent.inputType, "deleteWordBackward",
-       aDescription + 'inputType should be "deleteWordBackward" when deleting to previous word boundary with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to previous word boundary with collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to previous word boundary with collapsed selection");
+    action = 'removing "a" with "Delete" (with selection)';
+    synthesizeKey("KEY_Delete", {}, aWindow);
+    todo_is(editTarget.innerHTML, "a", `${aDescription}"a" should've been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should be fired at ${action} even if it won't remove any content`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}${action} should not fire "input" event since "beforeinput" was canceled`);
 
     editTarget.innerHTML = "a";
     editTarget.focus();
     selection.selectAllChildren(editTarget);
-    selection.collapseToStart();
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to next word boundary with collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to next word boundary with collapsed selection");
-    is(inputEvent.inputType, "deleteWordForward",
-       aDescription + 'inputType should be "deleteWordForward" when deleting to next word boundary with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to next word boundary with collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to next word boundary with collapsed selection");
+    action = 'removing "a" with "Delete" (with selection)';
+    synthesizeKey("KEY_Delete", {}, aWindow);
+    is(editTarget.innerHTML, "<br>", `${aDescription}" " should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "input" event for ${action} should be "deleteContentForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
-    // Delete to previous/next word boundary with non-collapsed selection.
-    editTarget.innerHTML = "abc";
+    // Delete to previous/next word boundary with collapsed selection.
+    editTarget.innerHTML = "abc def";
     editTarget.focus();
-    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    selection.collapse(editTarget.firstChild, "abc def".length);
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'removing last word, "def", with backward deletion from its end';
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
+    todo_is(editTarget.innerHTML, "abc def", `${aDescription}"def" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    editTarget.innerHTML = "abc def";
+    editTarget.focus();
+    selection.collapse(editTarget.firstChild, "abc def".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
     SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to previous word boundary with non-collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to previous word boundary with non-collapsed selection");
-    if (kIsWin) {
-      // Only on Windows, we collapse selection to start before handling this command.
-      is(inputEvent.inputType, "deleteWordBackward",
-         aDescription + 'inputType should be "deleteWordBackward" when deleting to previous word boundary with non-collapsed selection');
-    } else {
-      is(inputEvent.inputType, "deleteContentBackward",
-         aDescription + 'inputType should be "deleteContentBackward" when deleting to previous word boundary with non-collapsed selection');
+    is(editTarget.innerHTML, "abc ", `${aDescription}"def" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteWordBackward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteWordBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
     }
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to previous word boundary with non-collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to previous word boundary with non-collapsed selection");
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteWordBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteWordBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
-    editTarget.innerHTML = "abc";
+    editTarget.innerHTML = "abc def";
     editTarget.focus();
-    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    selection.collapse(editTarget.firstChild, 0);
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = `removing first word, "${kWordSelectEatSpaceToNextWord ? "abc" : "abc "}", with forward deletion from its start`;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
+    todo_is(editTarget.innerHTML, "abc def", `${aDescription}"abc" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    editTarget.innerHTML = "abc def";
+    editTarget.focus();
+    selection.collapse(editTarget.firstChild, 0);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
     SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to next word boundary with non-collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to next word boundary with non-collapsed selection");
+    is(editTarget.innerHTML, kWordSelectEatSpaceToNextWord ? "def" : " def",
+       `${aDescription}"${kWordSelectEatSpaceToNextWord ? "abc " : "abc"}" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteWordForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteWordForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteWordForward", `${aDescription}inputType of "input" event for ${action} should be "deleteWordForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // Delete to previous/next word boundary with non-collapsed selection.
+    editTarget.innerHTML = "abc def";
+    editTarget.focus();
+    selection.setBaseAndExtent(editTarget.firstChild, "abc d".length, editTarget.firstChild, "abc de".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters backward from middle of second word";
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
+    // Only on Windows, we collapse selection to start before handling this command.
+    let expectedInputType = kIsWin ? "deleteWordBackward" : "deleteContentBackward";
+    is(editTarget.innerHTML, kIsWin ? "abc ef" : "abc df",
+      `${aDescription}${kIsWin ? "characters between current word start and selection start" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    editTarget.innerHTML = "abc def";
+    editTarget.focus();
+    selection.setBaseAndExtent(editTarget.firstChild, "a".length, editTarget.firstChild, "ab".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters forward from middle of first word";
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
+    // Only on Windows, we collapse selection to start before handling this command.
+    expectedInputType = kIsWin ? "deleteWordForward" : "deleteContentForward";
+    let expectedValue = "ac def";
     if (kIsWin) {
-      // Only on Windows, we collapse selection to start before handling this command.
-      is(inputEvent.inputType, "deleteWordForward",
-         aDescription + 'inputType should be "deleteWordForward" when deleting to next word boundary with non-collapsed selection');
-    } else {
-      is(inputEvent.inputType, "deleteContentForward",
-         aDescription + 'inputType should be "deleteContentForward" when deleting to next word boundary with non-collapsed selection');
+      expectedValue = kWordSelectEatSpaceToNextWord ? "adef" : "a def";
     }
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to next word boundary with non-collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to next word boundary with non-collapsed selection");
+    is(editTarget.innerHTML, expectedValue,
+      `${aDescription}${kIsWin ? "characters between selection start and next word start" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     // Delete to previous/next visual line boundary with collapsed selection.
-    editTarget.innerHTML = "a";
+    editTarget.innerHTML = "abc def";
     editTarget.focus();
-    selection.selectAllChildren(editTarget);
-    selection.collapseToEnd();
+    selection.collapse(editTarget.firstChild, "abc d".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = "removing characters backward to start of line";
     SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to previous visual line boundary with collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to previous visual line boundary with collapsed selection");
-    is(inputEvent.inputType, "deleteSoftLineBackward",
-       aDescription + 'inputType should be "deleteSoftLineBackward" when deleting to previous visual line boundary with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to previous visual line boundary with collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to previous visual line boundary with collapsed selection");
+    is(editTarget.innerHTML, "ef", `${aDescription}characters between start of line and caret should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteSoftLineBackward",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteSoftLineBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteSoftLineBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteSoftLineBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
-    editTarget.innerHTML = "a";
+    editTarget.innerHTML = "abc def";
     editTarget.focus();
-    selection.selectAllChildren(editTarget);
-    selection.collapseToStart();
+    selection.collapse(editTarget.firstChild, "ab".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = "removing characters forward to end of line";
     SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to next visual line boundary with collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to next visual line boundary with collapsed selection");
-    is(inputEvent.inputType, "deleteSoftLineForward",
-       aDescription + 'inputType should be "deleteSoftLineForward" when deleting to visual line boundary with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to visual line boundary with collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to visual line boundary with collapsed selection");
+    is(editTarget.innerHTML, "ab", `${aDescription}characters between caret and end of line should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteSoftLineForward",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteSoftLineForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteSoftLineForward", `${aDescription}inputType of "input" event for ${action} should be "deleteSoftLineForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     // Delete to previous/next visual line boundary with non-collapsed selection.
-    editTarget.innerHTML = "abc";
+    editTarget.innerHTML = "abc def";
     editTarget.focus();
-    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    selection.setBaseAndExtent(editTarget.firstChild, "abc d".length, editTarget.firstChild, "abc_de".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = "removing characters backward to start of line (with selection in second word)";
     SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to previous visual line boundary with non-collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to previous visual line boundary with non-collapsed selection");
-    if (kIsWin) {
-      // Only on Windows, we collapse selection to start before handling this command.
-      is(inputEvent.inputType, "deleteSoftLineBackward",
-         aDescription + 'inputType should be "deleteSoftLineBackward" when deleting to next visual line boundary with non-collapsed selection');
-    } else {
-      is(inputEvent.inputType, "deleteContentBackward",
-         aDescription + 'inputType should be "deleteContentBackward" when deleting to previous visual line boundary with non-collapsed selection');
+    // Only on Windows, we collapse selection to start before handling this command.
+    expectedInputType = kIsWin ? "deleteSoftLineBackward" : "deleteContentBackward";
+    is(editTarget.innerHTML, kIsWin ? "ef" : "abc df",
+      `${aDescription}${kIsWin ? "characters between start of line and caret" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
     }
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to previous visual line boundary with non-collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to previous visual line boundary with non-collapsed selection");
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
-    editTarget.innerHTML = "abc";
+    editTarget.innerHTML = "abc def";
     editTarget.focus();
-    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    selection.setBaseAndExtent(editTarget.firstChild, "a".length, editTarget.firstChild, "ab".length);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = "removing characters forward to end of line (with selection in second word)";
     SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine");
-    ok(inputEvent,
-       aDescription + "input event should be fired by deleting to next visual line boundary with non-collapsed selection");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when deleting to next visual line boundary with non-collapsed selection");
-    if (kIsWin) {
-      // Only on Windows, we collapse selection to start before handling this command.
-      is(inputEvent.inputType, "deleteSoftLineForward",
-         aDescription + 'inputType should be "deleteSoftLineForward" when deleting to next visual line boundary with non-collapsed selection');
-    } else {
-      is(inputEvent.inputType, "deleteContentForward",
-         aDescription + 'inputType should be "deleteContentForward" when deleting to next visual line boundary with non-collapsed selection');
+    // Only on Windows, we collapse selection to start before handling this command.
+    expectedInputType = kIsWin ? "deleteSoftLineForward" : "deleteContentForward";
+    is(editTarget.innerHTML, kIsWin ? "a" : "ac def",
+      `${aDescription}${kIsWin ? "characters between caret anc end of line" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
     }
-    is(inputEvent.data, null,
-       aDescription + "data should be null when deleting to next visual line boundary with non-collapsed selection");
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when deleting to next visual line boundary with non-collapsed selection");
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     // Toggling text direction
     editTarget.focus();
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    SpecialPowers.doCommand(window, "cmd_switchTextDirection");
-    ok(inputEvent,
-       aDescription + "input event should be fired by dispatching cmd_switchTextDirection command #1");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when dispatching cmd_switchTextDirection command #1");
-    is(inputEvent.inputType, "formatSetBlockTextDirection",
-       aDescription + 'inputType should be "formatSetBlockTextDirection" when dispatching cmd_switchTextDirection command #1');
-    is(inputEvent.data, "rtl",
-       aDescription + 'data should be "rtl" when dispatching cmd_switchTextDirection command #1');
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when dispatching cmd_switchTextDirection command #1");
+    action = 'switching text direction from "ltr" to "rtl"';
+    SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection");
+    // XXX If editing host is a descendant of `<body>`, this must be a bug.
+    todo_is(body.getAttribute("dir"), null, `${aDescription}dir attribute of <body> should not be set" by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "rtl", `${aDescription}data of "beforeinput" event for ${action} should be "rtl"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event should not have been fired at ${action} since "beforeinput" was canceled`);
+
+    body.setAttribute("dir", "rtl");
+    htmlEditor.flags &= ~SpecialPowers.Ci.nsIPlaintextEditor.eEditorLeftToRight;
+    htmlEditor.flags |= SpecialPowers.Ci.nsIPlaintextEditor.eEditorRightToLeft; // XXX flags update is required, must be a bug.
+    aDocument.documentElement.scrollTop; // XXX Update the body frame
+    editTarget.focus();
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'switching text direction from "rtl" to "ltr"';
+    SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection");
+    // XXX If editing host is a descendant of `<body>`, this must be a bug.
+    todo_is(body.getAttribute("dir"), "rtl",
+       `${aDescription}dir attribute of <body> should not have been modified by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "ltr", `${aDescription}data of "beforeinput" event for ${action} should be "ltr"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event should not have been fired at ${action} since "beforeinput" was canceled`);
 
+    body.removeAttribute("dir");
+    htmlEditor.flags &= ~SpecialPowers.Ci.nsIPlaintextEditor.eEditorRightToLeft;
+    htmlEditor.flags |= SpecialPowers.Ci.nsIPlaintextEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug.
+    aDocument.documentElement.scrollTop; // XXX Update the body frame
+    editTarget.focus();
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    SpecialPowers.doCommand(window, "cmd_switchTextDirection");
-    ok(inputEvent,
-       aDescription + "input event should be fired by dispatching cmd_switchTextDirection command #2");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when dispatching cmd_switchTextDirection command #2");
-    is(inputEvent.inputType, "formatSetBlockTextDirection",
-       aDescription + 'inputType should be "formatSetBlockTextDirection" when dispatching cmd_switchTextDirection command #2');
-    is(inputEvent.data, "ltr",
-       aDescription + 'data should be "ltr" when dispatching cmd_switchTextDirection command #2');
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when dispatching cmd_switchTextDirection command #2");
+    action = 'switching text direction from "ltr" to "rtl"';
+    SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection");
+    // XXX If editing host is a descendant of `<body>`, this must be a bug.
+    is(body.getAttribute("dir"), "rtl", `${aDescription}dir attribute of <body> should've been set to "rtl" by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "rtl", `${aDescription}data of "beforeinput" event for ${action} should be "rtl"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "formatSetBlockTextDirection", `${aDescription}inputType of "input" event for ${action} should be "formatSetBlockTextDirection"`);
+    is(inputEvent.data, "rtl", `${aDescription}data of "input" event for ${action} should be "rtl"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    editTarget.focus();
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'switching text direction from "rtl" to "ltr"';
+    SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection");
+    // XXX If editing host is a descendant of `<body>`, this must be a bug.
+    is(body.getAttribute("dir"), "ltr", `${aDescription}dir attribute of <body> should've been set to "ltr" by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "ltr", `${aDescription}data of "beforeinput" event for ${action} should be "ltr"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "formatSetBlockTextDirection", `${aDescription}inputType of "input" event for ${action} should be "formatSetBlockTextDirection"`);
+    is(inputEvent.data, "ltr", `${aDescription}data of "input" event for ${action} should be "ltr"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     // Inserting link
     editTarget.innerHTML = "link";
     editTarget.focus();
     selection.selectAllChildren(editTarget);
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
     inputEvent = null;
-    aDocument.execCommand("createLink", false, "https://example.com/foo/bar.html");
-    ok(inputEvent,
-       aDescription + 'input event should be fired by execCommand("createLink", false, "https://example.com/foo/bar.html")');
-    ok(inputEvent.isTrusted,
-       aDescription + 'input event should be trusted when execCommand("createLink", false, "https://example.com/foo/bar.html")');
-    is(inputEvent.inputType, "insertLink",
-       aDescription + 'inputType should be "insertLink" when execCommand("createLink", false, "https://example.com/foo/bar.html")');
-    is(inputEvent.data, "https://example.com/foo/bar.html",
-       aDescription + 'data should be "https://example.com/foo/bar.html" when execCommand("createLink", false, "https://example.com/foo/bar.html")');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when execCommand("createLink", false, "https://example.com/foo/bar.html")');
+    action = "setting link with absolute URL";
+    SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "https://example.com/foo/bar.html");
+    todo_is(editTarget.innerHTML, "link", `${aDescription}the text should not habe been modified by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertLink", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertLink"`);
+      is(beforeInputEvent.data, "https://example.com/foo/bar.html",
+         `${aDescription}data of "beforeinput" event for ${action} should be "https://example.com/foo/bar.html"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since "beforeinput" was canceled`);
+
+    editTarget.innerHTML = "link";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "setting link with absolute URL";
+    SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "https://example.com/foo/bar.html");
+    is(editTarget.innerHTML, '<a href="https://example.com/foo/bar.html">link</a>',
+       `${aDescription}the text should've been wrapped by <a href> element by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertLink", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertLink"`);
+      is(beforeInputEvent.data, "https://example.com/foo/bar.html",
+         `${aDescription}data of "beforeinput" event for ${action} should be "https://example.com/foo/bar.html"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertLink", `${aDescription}inputType of "input" event for ${action} should be "insertLink"`);
+    is(inputEvent.data, "https://example.com/foo/bar.html", `${aDescription}data of "input" event for ${action} should be "https://example.com/foo/bar.html"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    editTarget.innerHTML = "link";
+    selection.selectAllChildren(editTarget);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "setting link with relative URL";
+    SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "foo/bar.html");
+    is(editTarget.innerHTML, '<a href="foo/bar.html">link</a>', `${aDescription}the text should've been wrapped by <a href> element by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertLink", `${aDescription}inputType of "beforeinput" event for ${action} should be "insertLink"`);
+      is(beforeInputEvent.data, "foo/bar.html", `${aDescription}data of "beforeinput" event for ${action} should be "foo/bar.html"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertLink", `${aDescription}inputType of "input" event for ${action} should be "insertLink"`);
+    is(inputEvent.data, "foo/bar.html", `${aDescription}data of "input" event for ${action} should be "foo/bar.html"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // Format commands, which we might support with shortcut keys.
+    for (let test of [{command: "cmd_bold",
+                       tag: "b",
+                       otherRemoveTags: ["strong"],
+                       inputType: "formatBold"},
+                      {command: "cmd_italic",
+                       tag: "i",
+                       otherRemoveTags: ["em"],
+                       inputType: "formatItalic"},
+                      {command: "cmd_underline",
+                       tag: "u",
+                       inputType: "formatUnderline"},
+                      {command: "cmd_strikethrough",
+                       tag: "strike",
+                       otherRemoveTags: ["s"],
+                       inputType: "formatStrikeThrough"},
+                      {command: "cmd_subscript",
+                       tag: "sub",
+                       exclusiveTags: ["sup"],
+                       inputType: "formatSubscript"},
+                      {command: "cmd_superscript",
+                       tag: "sup",
+                       exclusiveTags: ["sub"],
+                       inputType: "formatSuperscript"}]) {
+      editTarget.innerHTML = "format";
+      editTarget.focus();
+      selection.selectAllChildren(editTarget);
+      cancelBeforeInput = false;
+      todoMultipleInputEvents = false;
+      beforeInputEvent = null;
+      inputEvent = null;
+      action = `formatting with "${test.command}"`;
+      SpecialPowers.doCommand(aWindow, test.command);
+      is(editTarget.innerHTML, `<${test.tag}>format</${test.tag}>`,
+         `${aDescription}all text should be wrapped with <${test.tag}> element at ${action}`);
+      todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+      if (beforeInputEvent) {
+        is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+        is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+        is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+        is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+      }
+      ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+      is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+      is(inputEvent.inputType, test.inputType, `${aDescription}inputType of "input" event should be "${test.inputType}"`);
+      is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+      is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+      selection.selectAllChildren(editTarget);
+      cancelBeforeInput = false;
+      todoMultipleInputEvents = false;
+      beforeInputEvent = null;
+      inputEvent = null;
+      action = `removing format with "${test.command}"`;
+      SpecialPowers.doCommand(aWindow, test.command);
+      is(editTarget.innerHTML, "format",
+         `${aDescription}<${test.tag}> element should be unwrapped by ${action}`);
+      todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+      if (beforeInputEvent) {
+        is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+        is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+        is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+        is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+      }
+      ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+      is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+      is(inputEvent.inputType, test.inputType, `${aDescription}inputType of "input" event should be "${test.inputType}"`);
+      is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+      is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+      selection.selectAllChildren(editTarget);
+      cancelBeforeInput = true;
+      todoMultipleInputEvents = false;
+      beforeInputEvent = null;
+      inputEvent = null;
+      action = `formatting with "${test.command}" but "beforeinput" is canceled`;
+      SpecialPowers.doCommand(aWindow, test.command);
+      todo_is(editTarget.innerHTML, "format",
+         `${aDescription}text shouldn't have been modified at ${action}`);
+      todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+      if (beforeInputEvent) {
+        is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+        is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+        is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+        is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+      }
+      todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+
+      editTarget.innerHTML = `<${test.tag}>format</${test.tag}>`;
+      editTarget.focus();
+      selection.selectAllChildren(editTarget);
+      cancelBeforeInput = true;
+      todoMultipleInputEvents = false;
+      beforeInputEvent = null;
+      inputEvent = null;
+      action = `removing format with "${test.command}" but "beforeinput" is canceled`;
+      SpecialPowers.doCommand(aWindow, test.command);
+      todo_is(editTarget.innerHTML, `<${test.tag}>format</${test.tag}>`,
+         `${aDescription}text shouldn't have been modified at ${action}`);
+      todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+      if (beforeInputEvent) {
+        is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+        is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+        is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+        is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+      }
+      todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+
+      if (test.otherRemoveTags) {
+        for (let anotherTag of test.otherRemoveTags) {
+          editTarget.innerHTML = `<${anotherTag}>format</${anotherTag}>`;
+          editTarget.focus();
+          selection.selectAllChildren(editTarget);
+          cancelBeforeInput = false;
+          todoMultipleInputEvents = false;
+          beforeInputEvent = null;
+          inputEvent = null;
+          action = `removing <${anotherTag}> element with "${test.command}"`;
+          SpecialPowers.doCommand(aWindow, test.command);
+          is(editTarget.innerHTML, `format`,
+             `${aDescription}<${anotherTag}> element should be unwrapped by ${action}`);
+          todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+          if (beforeInputEvent) {
+            is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+            is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+            is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+            is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+          }
+          ok(inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+          is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+          todo_is(inputEvent.inputType, test.inputType, `${aDescription}inputType of "input" event should be "${test.inputType}"`);
+          is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+          is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+          editTarget.innerHTML = `<${anotherTag}>format</${anotherTag}>`;
+          editTarget.focus();
+          selection.selectAllChildren(editTarget);
+          cancelBeforeInput = true;
+          todoMultipleInputEvents = false;
+          beforeInputEvent = null;
+          inputEvent = null;
+          action = `removing <${anotherTag}> element with "${test.command}" but "beforeinput" is canceled`;
+          SpecialPowers.doCommand(aWindow, test.command);
+          todo_is(editTarget.innerHTML, `<${anotherTag}>format</${anotherTag}>`,
+             `${aDescription}text shouldn't have been modified at ${action}`);
+          todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+          if (beforeInputEvent) {
+            is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+            is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+            is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+            is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+          }
+          todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+
+          editTarget.innerHTML = `<${test.tag}><${anotherTag}>format</${anotherTag}></${test.tag}>`;
+          editTarget.focus();
+          selection.selectAllChildren(editTarget);
+          cancelBeforeInput = false;
+          todoMultipleInputEvents = true;
+          beforeInputEvent = null;
+          inputEvent = null;
+          action = `removing both <${test.tag}> and <${anotherTag}> elements with "${test.command}"`;
+          SpecialPowers.doCommand(aWindow, test.command);
+          is(editTarget.innerHTML, `format`,
+             `${aDescription}Both <${test.tag}> and <${anotherTag}> elements should be unwrapped by ${action}`);
+          todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+          if (beforeInputEvent) {
+            is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+            is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+            is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+            is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+          }
+          ok(inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+          is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+          todo_is(inputEvent.inputType, test.inputType, `${aDescription}inputType of "input" event should be "${test.inputType}"`);
+          is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+          is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+          editTarget.innerHTML = `<${test.tag}><${anotherTag}>format</${anotherTag}></${test.tag}>`;
+          editTarget.focus();
+          selection.selectAllChildren(editTarget);
+          cancelBeforeInput = true;
+          todoMultipleInputEvents = true;
+          beforeInputEvent = null;
+          inputEvent = null;
+          action = `removing both <${test.tag}> and <${anotherTag}> elements with "${test.command}" but "beforeinput" is canceled`;
+          SpecialPowers.doCommand(aWindow, test.command);
+          todo_is(editTarget.innerHTML, `<${test.tag}><${anotherTag}>format</${anotherTag}></${test.tag}>`,
+             `${aDescription}text shouldn't have been modified at ${action}`);
+          todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+          if (beforeInputEvent) {
+            is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+            is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+            is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+            is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+          }
+          todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+        }
+      }
+      if (test.exclusiveTags) {
+        for (let exclusiveTag of test.exclusiveTags) {
+          editTarget.innerHTML = `<${exclusiveTag}>format</${exclusiveTag}>`;
+          editTarget.focus();
+          selection.selectAllChildren(editTarget);
+          cancelBeforeInput = false;
+          todoMultipleInputEvents = true;
+          beforeInputEvent = null;
+          inputEvent = null;
+          action = `removing <${exclusiveTag}> element with formatting with "${test.command}"`;
+          SpecialPowers.doCommand(aWindow, test.command);
+          is(editTarget.innerHTML, `<${test.tag}>format</${test.tag}>`,
+             `${aDescription}<${exclusiveTag}> element should be replaced with <${test.tag}> element by ${action}`);
+          todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+          if (beforeInputEvent) {
+            is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+            is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+            is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+            is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+          }
+          ok(inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+          is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+          is(inputEvent.inputType, test.inputType, `${aDescription}inputType of "input" event should be "${test.inputType}"`);
+          is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+          is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+          editTarget.innerHTML = `<${exclusiveTag}>format</${exclusiveTag}>`;
+          editTarget.focus();
+          selection.selectAllChildren(editTarget);
+          cancelBeforeInput = true;
+          todoMultipleInputEvents = true;
+          beforeInputEvent = null;
+          inputEvent = null;
+          action = `removing <${exclusiveTag}> element with formatting with "${test.command}" but "beforeinput" is canceled`;
+          SpecialPowers.doCommand(aWindow, test.command);
+          todo_is(editTarget.innerHTML, `<${exclusiveTag}>format</${exclusiveTag}>`,
+             `${aDescription}text shouldn't have been modified at ${action}`);
+          todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+          if (beforeInputEvent) {
+            is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+            is(beforeInputEvent.inputType, test.inputType, `${aDescription}inputType of "beforeinput" event should be "${test.inputType}"`);
+            is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+            is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+          }
+          todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+        }
+      }
+    }
+
+    // Indent and Outdent
+    editTarget.innerHTML = "format";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = `indenting with "cmd_indent"`;
+    SpecialPowers.doCommand(aWindow, "cmd_indent");
+    is(editTarget.innerHTML, `<blockquote>format</blockquote>`,
+       `${aDescription}all text should be wrapped with <blockquote> element at ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatIndent", `${aDescription}inputType of "beforeinput" event should be "formatIndent"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+    is(inputEvent.inputType, "formatIndent", `${aDescription}inputType of "input" event should be "formatIndent"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     selection.selectAllChildren(editTarget);
-    aDocument.execCommand("createLink", false, "foo/bar.html");
-    ok(inputEvent,
-       aDescription + 'input event should be fired by execCommand("createLink", false, "foo/bar.html")');
-    ok(inputEvent.isTrusted,
-       aDescription + 'input event should be trusted when execCommand("createLink", false, "foo/bar.html")');
-    is(inputEvent.inputType, "insertLink",
-       aDescription + 'inputType should be "insertLink" when execCommand("createLink", false, "foo/bar.html")');
-    is(inputEvent.data, "foo/bar.html",
-       aDescription + 'data should be "foo/bar.html" when execCommand("createLink", false, "foo/bar.html")');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when execCommand("createLink", false, "foo/bar.html")');
+    cancelBeforeInput = false;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = `outdenting with "cmd_outdent"`;
+    SpecialPowers.doCommand(aWindow, "cmd_outdent");
+    is(editTarget.innerHTML, "format",
+       `${aDescription}<blockquote> element should be unwrapped by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatOutdent", `${aDescription}inputType of "beforeinput" event should be "formatOutdent"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.cancelable, false, `${aDescription}"input" event for ${action} should never be cancelable`);
+    is(inputEvent.inputType, "formatOutdent", `${aDescription}inputType of "input" event should be "formatOutdent"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
-    aWindow.removeEventListener("input", handler, true);
+    selection.selectAllChildren(editTarget);
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = `indenting with "cmd_indent" but "beforeinput" is canceled`;
+    SpecialPowers.doCommand(aWindow, "cmd_indent");
+    todo_is(editTarget.innerHTML, "format",
+       `${aDescription}text shouldn't have been modified at ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatIndent", `${aDescription}inputType of "beforeinput" event should be "formatIndent"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+
+    editTarget.innerHTML = `<blockquote>format</blockquote>`;
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    cancelBeforeInput = true;
+    todoMultipleInputEvents = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = `outdenting with "cmd_outdent" but "beforeinput" is canceled`;
+    SpecialPowers.doCommand(aWindow, "cmd_outdent");
+    todo_is(editTarget.innerHTML, `<blockquote>format</blockquote>`,
+       `${aDescription}text shouldn't have been modified at ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatOutdent", `${aDescription}inputType of "beforeinput" event should be "formatOutdent"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action}`);
+
+    aWindow.removeEventListener("beforeinput", beforeInputHandler, true);
+    aWindow.removeEventListener("input", inputHandler, true);
   }
 
   doTests(document.getElementById("editor1").contentDocument,
           document.getElementById("editor1").contentWindow,
           "Editor1, body has contenteditable attribute");
   doTests(document.getElementById("editor2").contentDocument,
           document.getElementById("editor2").contentWindow,
           "Editor2, html has contenteditable attribute");
--- a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
+++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
@@ -13,213 +13,723 @@
 </div>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="application/javascript">
+"use strict";
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 1);  // In a11y module
 SimpleTest.waitForFocus(runTests, window);
 
+const kIsWin = navigator.platform.indexOf("Win") == 0;
 const kIsMac = navigator.platform.indexOf("Mac") == 0;
 
-function runTests() {
+// TODO: When we remove `beforeInputEvent[0]` instance check, we should enable complexity check again.
+/* eslint-disable complexity */
+async function runTests() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
+  const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word");
+
   function doTests(aElement, aDescription, aIsTextarea) {
     aDescription += ": ";
     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");
+    let cancelBeforeInput = false;
+    let beforeInputEvent = null;
+    let inputEvent = null;
+    let action = "";
+    let beforeInputHandler = (aEvent) => {
+      ok(!beforeInputEvent, `${aDescription}Multiple "beforeinput" events are fired at ${action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+      if (cancelBeforeInput) {
+        aEvent.preventDefault();
+      }
+      ok(aEvent.isTrusted, `${aDescription}"beforeinput" event at ${action} must be trusted`);
+      is(aEvent.target, aElement, `${aDescription}"beforeinput" event at ${action} is fired on unexpected element: ${aEvent.target.tagName}`);
+      ok(aEvent instanceof InputEvent, `${aDescription}"beforeinput" event at ${action} should be dispatched with InputEvent interface`);
+      ok(aEvent.bubbles, `${aDescription}"beforeinput" event at ${action} must be bubbles`);
+      beforeInputEvent = aEvent;
+    };
+    let inputHandler = (aEvent) => {
+      ok(!inputEvent, `${aDescription}Multiple "input" events are fired at ${action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+      ok(aEvent.isTrusted, `${aDescription}"input" event at ${action} must be trusted`);
+      is(aEvent.target, aElement, `"input" event at ${action} is fired on unexpected element: ${aEvent.target.tagName}`);
+      ok(aEvent instanceof InputEvent, `${aDescription}"input" event at ${action} should be dispatched with InputEvent interface`);
+      ok(!aEvent.cancelable, `${aDescription}"input" event at ${action} must not be cancelable`);
+      ok(aEvent.bubbles, `${aDescription}"input" event at ${action} must be bubbles`);
       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)");
+         `${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` +
+         `the current time but it differed by ${duration}ms)`);
       inputEvent = aEvent;
     };
 
-    aElement.addEventListener("input", handler, true);
+    aElement.addEventListener("beforeinput", beforeInputHandler, true);
+    aElement.addEventListener("input", inputHandler, true);
 
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'typing "a"';
     sendString("a");
-    is(aElement.value, "a", aDescription + "'a' key didn't change the value");
-    ok(inputEvent, aDescription + "input event wasn't fired by 'a' key");
-    ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event");
-    is(inputEvent.inputType, "insertText",
-       aDescription + 'inputType should be "insertText" when typing "a"');
-    is(inputEvent.data, "a",
-       aDescription + 'data should be "a" when typing "a"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when typing "a"');
+    todo_is(aElement.value, "", `${aDescription}${action} shouldn't insert "a" since "beforeinput" was canceled"`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event by ${action} should be "insertText"`);
+      is(beforeInputEvent.data, "a", `${aDescription}data of "beforeinput" event by ${action} should be "a"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since "beforeinput" was canceled`);
 
+    aElement.value = "";
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'typing "a"';
+    sendString("a");
+    is(aElement.value, "a", `${aDescription}${action} should've inserted "a"`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event by ${action} should be "insertText"`);
+      is(beforeInputEvent.data, "a", `${aDescription}data of "beforeinput" event by ${action} should be "a"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertText", `${aDescription}inputType of "input" event by ${action} should be "insertText"`);
+    is(inputEvent.data, "a", `${aDescription}data of "input" event by ${action} should be "a"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event by ${action} should be null`);
+
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'removing "a" with "Backspace"';
     synthesizeKey("KEY_Backspace");
-    is(aElement.value, "", aDescription + "BackSpace key didn't remove the value");
-    ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key");
-    ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event");
-    is(inputEvent.inputType, "deleteContentBackward",
-       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Backspace" with collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Backspace" with collapsed selection');
+    todo_is(aElement.value, "a", `${aDescription}${action} shouldn't remove "a" since "beforeinput" was canceled"`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentBackward", `${aDescription}inputType of "beforeinput" event by ${action} should be "deleteContentBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since "beforeinput" was canceled`);
 
+    aElement.value = "a";
+    aElement.setSelectionStart = "a".length;
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'removing "a" with "Backspace"';
+    synthesizeKey("KEY_Backspace");
+    is(aElement.value, "", `${aDescription}${action} should've removed "a"`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentBackward", `${aDescription}inputType of "beforeinput" event by ${action} should be "deleteContentBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentBackward", `${aDescription}inputType of "input" event by ${action} should be "deleteContentBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event by ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event by ${action} should be null`);
+
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'typing "Enter"';
+    synthesizeKey("KEY_Enter");
     if (aIsTextarea) {
-      inputEvent = null;
-      synthesizeKey("KEY_Enter");
-      is(aElement.value, "\n", aDescription + "Enter key didn't change the value");
-      ok(inputEvent, aDescription + "input event wasn't fired by Enter key");
-      ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event");
-      is(inputEvent.inputType, "insertLineBreak",
-         aDescription + 'inputType should be "insertLineBreak" when pressing "Enter"');
-      is(inputEvent.data, null,
-         aDescription + 'data should be null when pressing "Enter"');
-      is(inputEvent.dataTransfer, null,
-         aDescription + 'dataTransfer should be null when pressing "Enter"');
+      todo_is(aElement.value, "", `${aDescription}${action} shouldn't insert a line break since "beforeinput" was canceled"`);
+    } else {
+      is(aElement.value, "", `${aDescription}${action} shouldn't insert a line break since it's a single line editor"`);
+    }
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertLineBreak", `${aDescription}inputType of "beforeinput" event by ${action} should be "insertLineBreak"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    if (aIsTextarea) {
+      todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since "beforeinput" was canceled`);
+    } else {
+      ok(!inputEvent, `${aDescription}$"input" event shouldn't have been fired at ${action} since it's a single line editor"`);
     }
 
+    aElement.value = "";
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'typing "Enter"';
+    synthesizeKey("KEY_Enter");
+    if (aIsTextarea) {
+      is(aElement.value, "\n", `${aDescription}${action} should've inserted a line break"`);
+    } else {
+      is(aElement.value, "", `${aDescription}${action} shouldn't insert a line break since it's a single line editor"`);
+    }
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertLineBreak", `${aDescription}inputType of "beforeinput" event by ${action} should be "insertLineBreak"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    if (aIsTextarea) {
+      ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+      is(inputEvent.inputType, "insertLineBreak", `${aDescription}inputType of "input" event by ${action} should be "insertLineBreak"`);
+      is(inputEvent.data, null, `${aDescription}data of "input" event by ${action} should be null`);
+      is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event by ${action} should be null`);
+    } else {
+      ok(!inputEvent, `${aDescription}$"input" event shouldn't have been fired at ${action} since it's a single line editor"`);
+    }
+
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
     aElement.value = "foo-bar";
-    is(aElement.value, "foo-bar", aDescription + "value wasn't set");
-    ok(!inputEvent, aDescription + "input event was fired by setting value");
+    action = "setting value";
+    is(aElement.value, "foo-bar", `${aDescription}value should've been set by ${action}`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event shouldn't have been fired by ${action}`);
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired by ${action}`);
 
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
     aElement.value = "";
+    action = "setting empty value";
     is(aElement.value, "", aDescription + "value wasn't set (empty)");
-    ok(!inputEvent, aDescription + "input event was fired by setting empty value");
+    is(aElement.value, "", `${aDescription}value should've been set to empy by ${action}`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event shouldn't have been fired by ${action}`);
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired by ${action}`);
 
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = "typing a space";
     sendString(" ");
-    is(aElement.value, " ", aDescription + "Space key didn't change the value");
-    ok(inputEvent, aDescription + "input event wasn't fired by Space key");
-    ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event");
-    is(inputEvent.inputType, "insertText",
-       aDescription + 'inputType should be "insertText" when typing " "');
-    is(inputEvent.data, " ",
-       aDescription + 'data should be " " when typing " "');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when typing " "');
+    is(aElement.value, " ", `${aDescription}" " should've been inserted by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "insertText", `${aDescription}inputType of "beforeinput" event by ${action} should be "insertText"`);
+      is(beforeInputEvent.data, " ", `${aDescription}data of "beforeinput" event by ${action} should be " "`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "insertText", `${aDescription}inputType of "input" event by ${action} should be "insertText"`);
+    is(inputEvent.data, " ", `${aDescription}data of "input" event by ${action} should be " "`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event by ${action} should be null`);
 
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'typing "Delete" at end';
     synthesizeKey("KEY_Delete");
-    is(aElement.value, " ", aDescription + "Delete key removed the value");
-    ok(!inputEvent, aDescription + "input event was fired by Delete key at the end");
+    is(aElement.value, " ", `${aDescription}${action} shouldn't remove anything since no removable content`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event by ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired by ${action} since no removable content`);
 
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'typing "ArrowLeft"';
     synthesizeKey("KEY_ArrowLeft");
-    is(aElement.value, " ", aDescription + "Left key removed the value");
-    ok(!inputEvent, aDescription + "input event was fired by Left key");
+    is(aElement.value, " ", `${aDescription}${action} shouldn't remove anything`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event shouldn't have been fired by ${action}`);
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired by ${action}`);
 
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'typing "Delete"';
+    synthesizeKey("KEY_Delete");
+    todo_is(aElement.value, " ", `${aDescription}${action} shouldn't remove the content since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event by ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since "beforeinput" was canceled`);
+
+    aElement.value = " ";
+    aElement.selectionStart = 0;
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'typing "Delete"';
     synthesizeKey("KEY_Delete");
-    is(aElement.value, "", aDescription + "Delete key didn't remove the value");
-    ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start");
-    ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event");
-    is(inputEvent.inputType, "deleteContentForward",
-       aDescription + 'inputType should be "deleteContentForward" when pressing "Delete" with collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Delete" with collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Delete" with collapsed selection');
+    is(aElement.value, "", `${aDescription}${action} should've removed " "`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event by ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event by ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event by ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event by ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "input" event by ${action} should be "deleteContentForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event by ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event by ${action} should be null`);
 
+    // TODO: Check canceling "beforeinput" case of "historyUndo" here.
+
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'doing "Undo"';
     synthesizeKey("z", {accelKey: true});
-    is(aElement.value, " ", aDescription + "Accel+Z key didn't undo the value");
-    ok(inputEvent, aDescription + "input event wasn't fired by Undo");
-    ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event");
-    is(inputEvent.inputType, "historyUndo",
-       aDescription + 'inputType should be "historyUndo" when doing "Undo"');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when doing "Undo"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when doing "Undo"');
+    is(aElement.value, " ", `${aDescription}${action} should restore " "`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "historyUndo", `${aDescription}inputType of "beforeinput" event for ${action} should be "historyUndo"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "historyUndo", `${aDescription}inputType of "input" event for ${action} should be "historyUndo"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'doing "Undo" again';
+    synthesizeKey("z", {accelKey: true});
+    is(aElement.value, " ", `${aDescription}${action} shouldn't modify the value since no undo transaction`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event shouldn't have been fired at ${action} since no undo transaction`);
+    ok(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since no undo transaction`);
+
+    // TODO: Check canceling "beforeinput" case of "historyRedo" here.
+
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'doing "Redo"';
     synthesizeKey("Z", {accelKey: true, shiftKey: true});
-    is(aElement.value, "", aDescription + "Accel+Y key didn't redo the value");
-    ok(inputEvent, aDescription + "input event wasn't fired by Redo");
-    ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event");
-    is(inputEvent.inputType, "historyRedo",
-       aDescription + 'inputType should be "historyRedo" when doing "Redo"');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when doing "Redo"');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when doing "Redo"');
+    is(aElement.value, "", `${aDescription}${action} should remove " "`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "historyRedo", `${aDescription}inputType of "beforeinput" event for ${action} should be "historyRedo"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "historyRedo", `${aDescription}inputType of "input" event for ${action} should be "historyRedo"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'doing "Redo" again';
+    synthesizeKey("z", {accelKey: true});
+    is(aElement.value, " ", `${aDescription}${action} shouldn't modify the value since no redo transaction`);
+    ok(!beforeInputEvent, `${aDescription}"beforeinput" event shouldn't have been fired at ${action} since no redo transaction`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't have been fired at ${action} since no redo transaction`);
 
     // Backspace/Delete with non-collapsed selection.
     aElement.value = "a";
+    aElement.focus();
     aElement.select();
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'removing "a" with "Backspace" (with selection)';
+    synthesizeKey("KEY_Backspace");
+    todo_is(aElement.value, "a", `${aDescription}"a" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    aElement.value = "a";
+    aElement.focus();
+    aElement.select();
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
     synthesizeKey("KEY_Backspace");
-    ok(inputEvent,
-       aDescription + 'input event should be fired by pressing "Backspace" with non-collapsed selection');
-    ok(inputEvent.isTrusted,
-       aDescription + 'input event should be trusted when pressing "Backspace" with non-collapsed selection');
-    is(inputEvent.inputType, "deleteContentBackward",
-       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with non-collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when pressing "Backspace" with non-collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when pressing "Backspace" with non-collapsed selection');
+    is(aElement.value, "", `${aDescription}"a" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentBackward",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteContentBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.value = "a";
+    aElement.focus();
+    aElement.select();
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'removing "a" with "Delete" (with selection)';
+    synthesizeKey("KEY_Delete");
+    todo_is(aElement.value, "a", `${aDescription}"a" should've been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should be fired at ${action} even if it won't remove any content`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}${action} should not fire "input" event since "beforeinput" was canceled`);
 
     aElement.value = "a";
+    aElement.focus();
     aElement.select();
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'removing "a" with "Delete" (with selection)';
     synthesizeKey("KEY_Delete");
-    ok(inputEvent,
-       aDescription + 'input event should be fired by pressing "Delete" with non-collapsed selection');
-    ok(inputEvent.isTrusted,
-       aDescription + 'input event should be trusted when pressing "Delete" with non-collapsed selection');
-    is(inputEvent.inputType, "deleteContentForward",
-       aDescription + 'inputType should be "deleteContentBackward" when Delete "Backspace" with non-collapsed selection');
-    is(inputEvent.data, null,
-       aDescription + 'data should be null when Delete "Backspace" with non-collapsed selection');
-    is(inputEvent.dataTransfer, null,
-       aDescription + 'dataTransfer should be null when Delete "Backspace" with non-collapsed selection');
+    is(aElement.value, "", `${aDescription}" " should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteContentForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteContentForward", `${aDescription}inputType of "input" event for ${action} should be "deleteContentForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // Delete to previous/next word boundary with collapsed selection.
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("abc def".length, "abc def".length);
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'removing last word, "def", with backward deletion from its end';
+    SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+    todo_is(aElement.value, "abc def", `${aDescription}"def" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("abc def".length, "abc def".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+    is(aElement.value, "abc ", `${aDescription}"def" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteWordBackward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteWordBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteWordBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteWordBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange(0, 0);
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = `removing first word, "${kWordSelectEatSpaceToNextWord ? "abc" : "abc "}", with forward deletion from its start`;
+    SpecialPowers.doCommand(window, "cmd_deleteWordForward");
+    todo_is(aElement.value, "abc def",
+       `${aDescription}"${kWordSelectEatSpaceToNextWord ? "abc" : "abc "}" shouldn't have been removed by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    todo(!inputEvent, `${aDescription}"input" event shouldn't been fired at ${action} since "beforeinput" was canceled`);
+
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange(0, 0);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    SpecialPowers.doCommand(window, "cmd_deleteWordForward");
+    is(aElement.value, kWordSelectEatSpaceToNextWord ? "def" : " def",
+       `${aDescription}"${kWordSelectEatSpaceToNextWord ? "abc" : "abc "}" should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteWordForward", `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteWordForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteWordForward", `${aDescription}inputType of "input" event for ${action} should be "deleteWordForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // Delete to previous/next word boundary with non-collapsed selection.
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("abc d".length, "abc de".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters backward from middle of second word";
+    SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+    // Only on Windows, we collapse selection to start before handling this command.
+    let expectedInputType = kIsWin ? "deleteWordBackward" : "deleteContentBackward";
+    is(aElement.value, kIsWin ? "abc ef" : "abc df",
+      `${aDescription}${kIsWin ? "characters between current word start and selection start" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("a".length, "ab".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters forward from middle of first word";
+    SpecialPowers.doCommand(window, "cmd_deleteWordForward");
+    // Only on Windows, we collapse selection to start before handling this command.
+    expectedInputType = kIsWin ? "deleteWordForward" : "deleteContentForward";
+    let expectedValue = "ac def";
+    if (kIsWin) {
+      expectedValue = kWordSelectEatSpaceToNextWord ? "adef" : "a def";
+    }
+    is(aElement.value, expectedValue,
+      `${aDescription}${kIsWin ? "characters between selection start and next word start" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // Delete to previous/next visual line boundary with collapsed selection.
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("abc d".length, "abc d".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters backward to start of line";
+    SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+    is(aElement.value, "ef", `${aDescription}characters between start of line and caret should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteSoftLineBackward",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteSoftLineBackward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteSoftLineBackward", `${aDescription}inputType of "input" event for ${action} should be "deleteSoftLineBackward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("ab".length, "ab".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters forward to end of line";
+    SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine");
+    is(aElement.value, "ab", `${aDescription}characters between caret and end of line should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "deleteSoftLineForward",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "deleteSoftLineForward"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "deleteSoftLineForward", `${aDescription}inputType of "input" event for ${action} should be "deleteSoftLineForward"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    // Delete to previous/next visual line boundary with non-collapsed selection.
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("abc d".length, "abc_de".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters backward to start of line (with selection in second word)";
+    SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+    // Only on Windows, we collapse selection to start before handling this command.
+    expectedInputType = kIsWin ? "deleteSoftLineBackward" : "deleteContentBackward";
+    is(aElement.value, kIsWin ? "ef" : "abc df",
+      `${aDescription}${kIsWin ? "characters between start of line and caret" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.value = "abc def";
+    aElement.focus();
+    document.documentElement.scrollTop;  // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+    aElement.setSelectionRange("a".length, "ab".length);
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = "removing characters forward to end of line (with selection in second word)";
+    SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine");
+    // Only on Windows, we collapse selection to start before handling this command.
+    expectedInputType = kIsWin ? "deleteSoftLineForward" : "deleteContentForward";
+    is(aElement.value, kIsWin ? "a" : "ac def",
+      `${aDescription}${kIsWin ? "characters between caret anc end of line" : "selected characters"} should've been removed by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, expectedInputType, `${aDescription}inputType of "beforeinput" event for ${action} should be "${expectedInputType}"`);
+      is(beforeInputEvent.data, null, `${aDescription}data of "beforeinput" event for ${action} should be null`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, expectedInputType, `${aDescription}inputType of "input" event for ${action} should be "${expectedInputType}"`);
+    is(inputEvent.data, null, `${aDescription}data of "input" event for ${action} should be null`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
 
     // Toggling text direction
     aElement.focus();
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'switching text direction from "ltr" to "rtl"';
     SpecialPowers.doCommand(window, "cmd_switchTextDirection");
-    ok(inputEvent,
-       aDescription + "input event should be fired by dispatching cmd_switchTextDirection command #1");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when dispatching cmd_switchTextDirection command #1");
-    is(inputEvent.inputType, "formatSetBlockTextDirection",
-       aDescription + 'inputType should be "formatSetBlockTextDirection" when dispatching cmd_switchTextDirection command #1');
-    is(inputEvent.data, "rtl",
-       aDescription + 'data should be "rtl" when dispatching cmd_switchTextDirection command #1');
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when dispatching cmd_switchTextDirection command #1");
+    todo_is(aElement.getAttribute("dir"), null, `${aDescription}dir attribute of the element should not be set" by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "rtl", `${aDescription}data of "beforeinput" event for ${action} should be "rtl"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event should not have been fired at ${action} since "beforeinput" was canceled`);
 
+    aElement.setAttribute("dir", "rtl");
+    aElement.scrollTop; // XXX Update the root frame
+    aElement.focus();
+    cancelBeforeInput = true;
+    beforeInputEvent = null;
     inputEvent = null;
+    action = 'switching text direction from "rtl" to "ltr"';
     SpecialPowers.doCommand(window, "cmd_switchTextDirection");
-    ok(inputEvent,
-       aDescription + "input event should be fired by dispatching cmd_switchTextDirection command #2");
-    ok(inputEvent.isTrusted,
-       aDescription + "input event should be trusted when dispatching cmd_switchTextDirection command #2");
-    is(inputEvent.inputType, "formatSetBlockTextDirection",
-       aDescription + 'inputType should be "formatSetBlockTextDirection" when dispatching cmd_switchTextDirection command #2');
-    is(inputEvent.data, "ltr",
-       aDescription + 'data should be "ltr" when dispatching cmd_switchTextDirection command #2');
-    is(inputEvent.dataTransfer, null,
-       aDescription + "dataTransfer should be null when dispatching cmd_switchTextDirection command #2");
+    todo_is(aElement.getAttribute("dir"), "rtl",
+       `${aDescription}dir attribute of the element should not have been modified by ${action} since "beforeinput" was canceled`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "ltr", `${aDescription}data of "beforeinput" event for ${action} should be "ltr"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    todo(!inputEvent, `${aDescription}"input" event should not have been fired at ${action} since "beforeinput" was canceled`);
 
-    aElement.removeEventListener("input", handler, true);
+    aElement.removeAttribute("dir");
+    aElement.scrollTop; // XXX Update the root frame
+    aElement.focus();
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'switching text direction from "ltr" to "rtl"';
+    SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+    is(aElement.getAttribute("dir"), "rtl", `${aDescription}dir attribute of the element should've been set to "rtl" by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "rtl", `${aDescription}data of "beforeinput" event for ${action} should be "rtl"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "formatSetBlockTextDirection", `${aDescription}inputType of "input" event for ${action} should be "formatSetBlockTextDirection"`);
+    is(inputEvent.data, "rtl", `${aDescription}data of "input" event for ${action} should be "rtl"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.focus();
+    cancelBeforeInput = false;
+    beforeInputEvent = null;
+    inputEvent = null;
+    action = 'switching text direction from "rtl" to "ltr"';
+    SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+    is(aElement.getAttribute("dir"), "ltr", `${aDescription}dir attribute of the element should've been set to "ltr" by ${action}`);
+    todo(beforeInputEvent, `${aDescription}"beforeinput" event should've been fired at ${action}`);
+    if (beforeInputEvent) {
+      is(beforeInputEvent.cancelable, true, `${aDescription}"beforeinput" event for ${action} should be cancelable`);
+      is(beforeInputEvent.inputType, "formatSetBlockTextDirection",
+         `${aDescription}inputType of "beforeinput" event for ${action} should be "formatSetBlockTextDirection"`);
+      is(beforeInputEvent.data, "ltr", `${aDescription}data of "beforeinput" event for ${action} should be "ltr"`);
+      is(beforeInputEvent.dataTransfer, null, `${aDescription}dataTransfer of "beforeinput" event for ${action} should be null`);
+    }
+    ok(inputEvent, `${aDescription}"input" event should've been fired at ${action}`);
+    is(inputEvent.inputType, "formatSetBlockTextDirection", `${aDescription}inputType of "input" event for ${action} should be "formatSetBlockTextDirection"`);
+    is(inputEvent.data, "ltr", `${aDescription}data of "input" event for ${action} should be "ltr"`);
+    is(inputEvent.dataTransfer, null, `${aDescription}dataTransfer of "input" event for ${action} should be null`);
+
+    aElement.removeEventListener("beforeinput", beforeInputHandler, true);
+    aElement.removeEventListener("input", inputHandler, true);
   }
 
   doTests(document.getElementById("input"), "<input type=\"text\">", false);
   doTests(document.getElementById("textarea"), "<textarea>", true);
 
   SimpleTest.finish();
 }
 
--- a/editor/libeditor/tests/test_dragdrop.html
+++ b/editor/libeditor/tests/test_dragdrop.html
@@ -16,48 +16,57 @@
        style="height: 4px; background-color: lemonchiffon;"></div>
   <div id="container"></div>
 
 <script type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 
 function checkInputEvent(aEvent, aExpectedTarget, aInputType, aData, aDataTransfer, aDescription) {
-  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.target, aExpectedTarget, `${aDescription}: "input" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
-  is(aEvent.inputType, aInputType, `${aDescription}: inputType should be "${aInputType}" on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
-  is(aEvent.data, aData, `${aDescription}: data should be ${aData} on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
+  ok(aEvent instanceof InputEvent, `${aDescription}: "${aEvent.type}" event should be dispatched with InputEvent interface`);
+  is(aEvent.cancelable, aEvent.type === "beforeinput", `${aDescription}: "${aEvent.type}" event should be ${aEvent.type === "beforeinput" ? "" : "never "}cancelable`);
+  is(aEvent.bubbles, true, `${aDescription}: "${aEvent.type}" event should always bubble`);
+  is(aEvent.target, aExpectedTarget, `${aDescription}: "${aEvent.type}" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
+  is(aEvent.inputType, aInputType, `${aDescription}: inputType of "${aEvent.type}" event should be "${aInputType}" on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
+  is(aEvent.data, aData, `${aDescription}: data of "${aEvent.type}" event should be ${aData} on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
   if (aDataTransfer === null) {
     is(aEvent.dataTransfer, null, `${aDescription}: dataTransfer should be null on the <${aExpectedTarget.tagName.toLowerCase()}> element`);
   } else {
     for (let dataTransfer of aDataTransfer) {
       let description = `${aDescription}: on the <${aExpectedTarget.tagName.toLowerCase()}> element`;
       if (dataTransfer.todo) {
         // XXX It seems that synthesizeDrop() don't emulate perfectly if caller specifies the data directly.
         todo_is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data,
-                `${description}: dataTransfer should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`);
+                `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`);
       } else {
         is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data,
-           `${description}: dataTransfer should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`);
+           `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`);
       }
     }
   }
 }
 
 async function doTest() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.input_events.beforeinput.enabled", true]],
+  });
+
   let container = document.getElementById("container");
   let dropZone = document.getElementById("dropZone");
 
+  let beforeinputEvents = [];
   let inputEvents = [];
   let dragEvents = [];
+  function onBeforeinput(event) {
+    beforeinputEvents.push(event);
+  }
   function onInput(event) {
     inputEvents.push(event);
   }
+  document.addEventListener("beforeinput", onBeforeinput);
   document.addEventListener("input", onInput);
 
   let selection = window.getSelection();
 
   const kIsMac = navigator.platform.includes("Mac");
   const kIsWin = navigator.platform.includes("Win");
 
   const kNativeLF = kIsWin ? "\r\n" : "\n";
@@ -79,127 +88,140 @@ async function doTest() {
   // For same reason, don't use multiline comment.
   let description, span, b, input, otherInput, textarea, otherTextarea, contenteditable, otherContenteditable, onDrop, onDragStart;
 
   // -------- Test dragging regular text
   description = "dragging part of non-editable <span> element";
   container.innerHTML = '<span style="font-size: 24px;">Some Text</span>';
   span = document.querySelector("div#container > span");
   selection.setBaseAndExtent(span.firstChild, 4, span.firstChild, 6);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"),
                      span.textContent.substring(4, 6),
                      `${description}: dataTransfer should have selected text as "text/plain"`);
     compareHTML(aEvent.dataTransfer.getData("text/html"),
                 span.outerHTML.replace(/>.+</, `>${span.textContent.substring(4, 6)}<`),
                 `${description}: dataTransfer should have the parent inline element and only selected text as "text/html"`);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: dropZone,
   });
+  is(beforeinputEvents.length, 0,
+     `${description}: No "beforeinput" event should be fired when dragging non-editable selection to non-editable drop zone`);
   is(inputEvents.length, 0,
      `${description}: No "input" event should be fired when dragging non-editable selection to non-editable drop zone`);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text from an <input>
   description = "dragging part of text in <input> element";
   container.innerHTML = '<input value="Drag Me">';
   input = document.querySelector("div#container > input");
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input.setSelectionRange(1, 4);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"),
                      input.value.substring(1, 4),
                      `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should not have data as "text/html"`);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: dropZone,
   });
+  is(beforeinputEvents.length, 0,
+     `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`);
   is(inputEvents.length, 0,
      `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text from an <textarea>
   description = "dragging part of text in <textarea> element";
   container.innerHTML = "<textarea>Some Text To Drag</textarea>";
   textarea = document.querySelector("div#container > textarea");
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea.setSelectionRange(1, 7);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"),
                      textarea.value.substring(1, 7),
                      `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should not have data as "text/html"`);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(textarea).editor.selection,
     destElement: dropZone,
   });
+  is(beforeinputEvents.length, 0,
+     `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`);
   is(inputEvents.length, 0,
      `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text from a contenteditable
   description = "dragging part of text in contenteditable element";
   container.innerHTML = "<p contenteditable>This is some <b>editable</b> text.</p>";
   contenteditable = document.querySelector("div#container > p");
   b = document.querySelector("div#container > p > b");
   selection.setBaseAndExtent(b.firstChild, 2, b.firstChild, 6);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"),
                      b.textContent.substring(2, 6),
                      `${description}: dataTransfer should have selected text as "text/plain"`);
     compareHTML(aEvent.dataTransfer.getData("text/html"),
                 b.outerHTML.replace(/>.+</, `>${b.textContent.substring(2, 6)}<`),
                 `${description}: dataTransfer should have selected nodes as "text/html"`);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: dropZone,
   });
+  is(beforeinputEvents.length, 0,
+     `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`);
   is(inputEvents.length, 0,
      `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired`);
   document.removeEventListener("drop", onDrop);
 
 
   // -------- Test dragging regular text of text/html to <input>
   description = "dragging text in non-editable <span> to <input>";
   container.innerHTML = "<span>Static</span><input>";
   span = document.querySelector("div#container > span");
   input = document.querySelector("div#container > input");
   selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"),
                      span.textContent.substring(2, 5),
                      `${description}: dataTransfer should have selected text as "text/plain"`);
     compareHTML(aEvent.dataTransfer.getData("text/html"),
@@ -208,54 +230,63 @@ async function doTest() {
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: input,
   });
   is(input.value, span.textContent.substring(2, 5),
      `${description}: <input>.value should be modified`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: one "beforeinput" event should be fired on <input>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: one "input" event should be fired on <input>`);
   checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <input>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging regular text of text/html to disabled <input>
   description = "dragging text in non-editable <span> to <input disabled>";
   container.innerHTML = "<span>Static</span><input disabled>";
   span = document.querySelector("div#container > span");
   input = document.querySelector("div#container > input");
   selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: input,
   });
   is(input.value, "",
      `${description}: <input disable>.value should not be modified`);
+  is(beforeinputEvents.length, 0,
+     `${description}: no "beforeinput" event should be fired on <input disabled>`);
   is(inputEvents.length, 0,
      `${description}: no "input" event should be fired on <input disabled>`);
   is(dragEvents.length, 0,
      `${description}: no "drop" event should be fired on <input disabled>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging regular text of text/html to readonly <input>
   // XXX Oddly, dropping in <input readonly> causes trying to load another page.
   // description = "dragging text in non-editable <span> to <input readonly>";
   // container.innerHTML = "<span>Static</span><input readonly>";
   // span = document.querySelector("div#container > span");
   // input = document.querySelector("div#container > input");
   // selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5);
+  // beforeinputEvents = [];
   // inputEvents = [];
   // dragEvents = [];
   // onDrop = aEvent => {
   //   dragEvents.push(aEvent);
   //   comparePlainText(aEvent.dataTransfer.getData("text/plain"),
   //                    span.textContent.substring(2, 5),
   //                    `${description}: dataTransfer should have selected text as "text/plain"`);
   //   compareHTML(aEvent.dataTransfer.getData("text/html"),
@@ -264,28 +295,31 @@ async function doTest() {
   // };
   // document.addEventListener("drop", onDrop);
   // await synthesizePlainDragAndDrop({
   //   srcSelection: selection,
   //   destElement: input,
   // });
   // is(input.value, "",
   //    `${description}: <input readonly>.value should not be modified`);
+  // is(beforeinputEvents.length, 0,
+  //    `${description}: no "beforeinput" event should be fired on <input readonly>`);
   // is(inputEvents.length, 0,
   //    `${description}: no "input" event should be fired on <input readonly>`);
   // is(dragEvents.length, 0,
   //    `${description}: no "drop" event should be fired on <input readonly>`);
   // document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging regular text of text/plain to <textarea>
   description = "dragging text in non-editable <span> to <textarea>";
   container.innerHTML = "<span>Static</span><textarea></textarea>";
   span = document.querySelector("div#container > span");
   textarea = document.querySelector("div#container > textarea");
   selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"),
                      span.textContent.substring(2, 5),
                      `${description}: dataTransfer should have selected text as "text/plain"`);
     compareHTML(aEvent.dataTransfer.getData("text/html"),
@@ -294,137 +328,179 @@ async function doTest() {
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: textarea,
   });
   is(textarea.value, span.textContent.substring(2, 5),
      `${description}: <textarea>.value should be modified`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: one "beforeinput" event should be fired on <textarea>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: one "input" event should be fired on <textarea>`);
   checkInputEvent(inputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging only text/html data (like from another app) to <input>.
   description = "dragging only text/html data to <input>";
   container.innerHTML = "<span>Static</span><input>";
   span = document.querySelector("div#container > span");
   input = document.querySelector("div#container > input");
   selection.selectAllChildren(span);
+  beforeinputEvents = [];
   inputEvents = [];
   onDragStart = aEvent => {
     // Clear all dataTransfer data first.  Then, it'll be filled only with
     // the text/html data passed to synthesizeDrop().
     aEvent.dataTransfer.clearData();
   };
   window.addEventListener("dragstart", onDragStart, {capture: true});
   synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy");
+  is(beforeinputEvents.length, 0,
+     `${description}: no "beforeinput" event should be fired on <input>`);
   is(inputEvents.length, 0,
      `${description}: no "input" event should be fired on <input>`);
   window.removeEventListener("dragstart", onDragStart, {capture: true});
 
   // -------- Test dragging both text/plain and text/html data (like from another app) to <input>.
   description = "dragging both text/plain and text/html data to <input>";
   container.innerHTML = "<span>Static</span><input>";
   span = document.querySelector("div#container > span");
   input = document.querySelector("div#container > input");
   selection.selectAllChildren(span);
+  beforeinputEvents = [];
   inputEvents = [];
   onDragStart = aEvent => {
     // Clear all dataTransfer data first.  Then, it'll be filled only with
     // the text/plain data and text/html data passed to synthesizeDrop().
     aEvent.dataTransfer.clearData();
   };
   window.addEventListener("dragstart", onDragStart, {capture: true});
   synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"},
                                 {type: "text/plain", data: "Some Plain Text"}]], "copy");
   is(input.value, "Some Plain Text",
      `${description}: The text/plain data should be inserted`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: Only one "beforeinput" events should be fired on <input> element`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "Some Plain Text", null,
+                    description);
+  }
   is(inputEvents.length, 1,
      `${description}: Only one "input" events should be fired on <input> element`);
   checkInputEvent(inputEvents[0], input, "insertFromDrop", "Some Plain Text", null,
                   description);
   window.removeEventListener("dragstart", onDragStart, {capture: true});
 
   // -------- Test dragging special text type from another app to <input>
   description = "dragging both text/plain and text/html data to <input>";
   container.innerHTML = "<span>Static</span><input>";
   span = document.querySelector("div#container > span");
   input = document.querySelector("div#container > input");
   selection.selectAllChildren(span);
+  beforeinputEvents = [];
   inputEvents = [];
   onDragStart = aEvent => {
     // Clear all dataTransfer data first.  Then, it'll be filled only with
     // the text/x-moz-text-internal data passed to synthesizeDrop().
     aEvent.dataTransfer.clearData();
   };
   window.addEventListener("dragstart", onDragStart, {capture: true});
   synthesizeDrop(span, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy");
   is(input.value, "",
      `${description}: <input>.value should not be modified with "text/x-moz-text-internal" data`);
+  // Note that even if editor does not handle given dataTransfer, web apps
+  // may handle it by itself.  Therefore, editor should dispatch "beforeinput"
+  // event.
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: one "beforeinput" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`);
+  if (beforeinputEvents.length > 0) {
+    // But unfortunately, on <input> and <textarea>, dataTransfer won't be set...
+    checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "", null, description);
+  }
   is(inputEvents.length, 0,
      `${description}: no "input" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`);
   window.removeEventListener("dragstart", onDragStart, {capture: true});
 
   // -------- Test dragging only text/plain data (like from another app) to contenteditable.
   description = "dragging both text/plain and text/html data to contenteditable";
   container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>';
   span = document.querySelector("div#container > span");
   contenteditable = document.querySelector("div#container > div");
   selection.selectAllChildren(span);
+  beforeinputEvents = [];
   inputEvents = [];
   onDragStart = aEvent => {
     // Clear all dataTransfer data first.  Then, it'll be filled only with
     // the text/plain data and text/html data passed to synthesizeDrop().
     aEvent.dataTransfer.clearData();
   };
   window.addEventListener("dragstart", onDragStart, {capture: true});
   synthesizeDrop(span, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy");
   is(contenteditable.innerHTML, "Sample Text",
      `${description}: The text/plain data should be inserted`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: Only one "beforeinput" events should be fired on contenteditable element`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
+                    [{todo: true, type: "text/plain", data: "Sample Text"}],
+                    description);
+  }
   is(inputEvents.length, 1,
      `${description}: Only one "input" events should be fired on contenteditable element`);
   checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null,
                   [{todo: true, type: "text/plain", data: "Sample Text"}],
                   description);
   window.removeEventListener("dragstart", onDragStart, {capture: true});
 
   // -------- Test dragging only text/html data (like from another app) to contenteditable.
   description = "dragging only text/html data to contenteditable";
   container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>';
   span = document.querySelector("div#container > span");
   contenteditable = document.querySelector("div#container > div");
   selection.selectAllChildren(span);
+  beforeinputEvents = [];
   inputEvents = [];
   onDragStart = aEvent => {
     // Clear all dataTransfer data first.  Then, it'll be filled only with
     // the text/plain data and text/html data passed to synthesizeDrop().
     aEvent.dataTransfer.clearData();
   };
   window.addEventListener("dragstart", onDragStart, {capture: true});
   synthesizeDrop(span, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy");
   is(contenteditable.innerHTML, "Sample <i>Italic</i> Text",
      `${description}: The text/plain data should be inserted`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: Only one "beforeinput" events should be fired on contenteditable element`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
+                    [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}],
+                    description);
+  }
   is(inputEvents.length, 1,
      `${description}: Only one "input" events should be fired on contenteditable element`);
   checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null,
                   [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}],
                   description);
   window.removeEventListener("dragstart", onDragStart, {capture: true});
 
   // -------- Test dragging contenteditable to <input>
   description = "dragging text in contenteditable to <input>";
   container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><input>";
   contenteditable = document.querySelector("div#container > div");
   input = document.querySelector("div#container > input");
   selection.setBaseAndExtent(contenteditable.firstChild, 2,
                              contenteditable.firstChild.nextSibling.nextSibling, 2);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "me bold t",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -433,31 +509,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: input,
   });
   is(contenteditable.innerHTML, "Soext",
      `${description}: Dragged range should be removed from contenteditable`);
   is(input.value, "me bold t",
      `${description}: <input>.value should be modified`);
+  todo_is(beforeinputEvents.length, 2,
+          `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, description);
+  }
   is(inputEvents.length, 2,
           `${description}: 2 "input" events should be fired on contenteditable and <input>`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], input, "insertFromDrop", "me bold t", null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging contenteditable to <textarea>
   description = "dragging text in contenteditable to <textarea>";
   container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>";
   contenteditable = document.querySelector("div#container > div");
   textarea = document.querySelector("div#container > textarea");
   selection.setBaseAndExtent(contenteditable.firstChild, 2,
                              contenteditable.firstChild.nextSibling.nextSibling, 2);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "me bold t",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -466,31 +549,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: textarea,
   });
   is(contenteditable.innerHTML, "Soext",
      `${description}: Dragged range should be removed from contenteditable`);
   is(textarea.value, "me bold t",
      `${description}: <textarea>.value should be modified`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on contenteditable and <textarea>`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "me bold t", null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging contenteditable to same contenteditable
   description = "dragging text in contenteditable to same contenteditable";
   container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>";
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > b");
   span = document.querySelector("div#container > div > span");
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -499,16 +589,24 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: span,
   });
   todo_is(contenteditable.innerHTML, "<b>bd</b> <span>MM<b>ol</b>MM</span>",
      `${description}: dragged range should be removed from contenteditable`);
   todo_isnot(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span><b>ol</b>",
      `${description}: dragged range should be removed from contenteditable`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on contenteditable`);
@@ -517,16 +615,17 @@ async function doTest() {
   // -------- Test copy-dragging contenteditable to same contenteditable
   description = "copy-dragging text in contenteditable to same contenteditable";
   container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>";
   document.documentElement.scrollTop;
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > b");
   span = document.querySelector("div#container > div > span");
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -536,32 +635,40 @@ async function doTest() {
     srcSelection: selection,
     destElement: span,
     dragEvent: kModifiersToCopy,
   });
   todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>",
      `${description}: dragged range shouldn't be removed from contenteditable`);
   todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>",
      `${description}: dragged range shouldn't be removed from contenteditable`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only 1 "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 1,
      `${description}: only 1 "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging contenteditable to other contenteditable
   description = "dragging text in contenteditable to other contenteditable";
   container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>';
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > b");
   otherContenteditable = document.querySelector("div#container > div ~ div");
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -570,16 +677,24 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: otherContenteditable,
   });
   is(contenteditable.innerHTML, "<b>bd</b>",
      `${description}: dragged range should be removed from contenteditable`);
   is(otherContenteditable.innerHTML, "<b>ol</b>",
      `${description}: dragged content should be inserted into other contenteditable`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
@@ -587,16 +702,17 @@ async function doTest() {
 
   // -------- Test dragging contenteditable to other contenteditable
   description = "copy-dragging text in contenteditable to other contenteditable";
   container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>';
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > b");
   otherContenteditable = document.querySelector("div#container > div ~ div");
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -606,16 +722,23 @@ async function doTest() {
     srcSelection: selection,
     destElement: otherContenteditable,
     dragEvent: kModifiersToCopy,
   });
   is(contenteditable.innerHTML, "<b>bold</b>",
      `${description}: dragged range shouldn't be removed from contenteditable`);
   is(otherContenteditable.innerHTML, "<b>ol</b>",
      `${description}: dragged content should be inserted into other contenteditable`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on other contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on other contenteditable`);
   checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
@@ -623,32 +746,41 @@ async function doTest() {
   // -------- Test dragging nested contenteditable to contenteditable
   description = "dragging text in nested contenteditable to contenteditable";
   container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>';
   contenteditable = document.querySelector("div#container > div");
   otherContenteditable = document.querySelector("div#container > div > div > p");
   b = document.querySelector("div#container > div > div > p > b");
   contenteditable.focus();
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: contenteditable.firstChild,
   });
   is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>',
      `${description}: dragged range should be moved from nested contenteditable to the contenteditable`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on contenteditable`);
@@ -656,16 +788,17 @@ async function doTest() {
 
   // -------- Test copy-dragging nested contenteditable to contenteditable
   description = "copy-dragging text in nested contenteditable to contenteditable";
   container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>';
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > div > p > b");
   contenteditable.focus();
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -673,16 +806,23 @@ async function doTest() {
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: contenteditable.firstChild,
     dragEvent: kModifiersToCopy,
   });
   is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>',
      `${description}: dragged range should be moved from nested contenteditable to the contenteditable`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on contenteditable`);
   document.removeEventListener("drop", onDrop);
@@ -690,32 +830,41 @@ async function doTest() {
   // -------- Test dragging contenteditable to nested contenteditable
   description = "dragging text in contenteditable to nested contenteditable";
   container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>';
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > p > b");
   otherContenteditable = document.querySelector("div#container > div > div > p");
   contenteditable.focus();
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
   };
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: otherContenteditable,
   });
   is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>',
      `${description}: dragged range should be moved from contenteditable to nested contenteditable`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on contenteditable and nested contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on contenteditable`);
@@ -724,16 +873,17 @@ async function doTest() {
   // -------- Test copy-dragging contenteditable to nested contenteditable
   description = "copy-dragging text in contenteditable to nested contenteditable";
   container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>';
   contenteditable = document.querySelector("div#container > div");
   b = document.querySelector("div#container > div > p > b");
   otherContenteditable = document.querySelector("div#container > div > div > p");
   contenteditable.focus();
   selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     is(aEvent.dataTransfer.getData("text/plain"), "ol",
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>",
        `${description}: dataTransfer should have selected nodes as "text/html"`);
@@ -741,32 +891,40 @@ async function doTest() {
   document.addEventListener("drop", onDrop);
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: otherContenteditable,
     dragEvent: kModifiersToCopy,
   });
   is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>',
      `${description}: dragged range should be moved from nested contenteditable to the contenteditable`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null,
+                    [{type: "text/html", data: "<b>ol</b>"},
+                     {type: "text/plain", data: "ol"}], description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null,
                   [{type: "text/html", data: "<b>ol</b>"},
                    {type: "text/plain", data: "ol"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text in <input> to contenteditable
   description = "dragging text in <input> to contenteditable";
   container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input = document.querySelector("div#container > input");
   contenteditable = document.querySelector("div#container > div");
   input.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -775,32 +933,40 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: contenteditable,
   });
   is(input.value, "Somt",
      `${description}: dragged range should be removed from <input>`);
   is(contenteditable.innerHTML, "e Tex<br>",
      `${description}: dragged content should be inserted into contenteditable`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
+                    [{type: "text/plain", data: "e Tex"}], description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <input> and contenteditable`);
   checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null,
                   [{type: "text/plain", data: "e Tex"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging text in <input> to contenteditable
   description = "copy-dragging text in <input> to contenteditable";
   container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input = document.querySelector("div#container > input");
   contenteditable = document.querySelector("div#container > div");
   input.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -810,31 +976,38 @@ async function doTest() {
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: contenteditable,
     dragEvent: kModifiersToCopy,
   });
   is(input.value, "Some Text",
      `${description}: dragged range shouldn't be removed from <input>`);
   is(contenteditable.innerHTML, "e Tex<br>",
      `${description}: dragged content should be inserted into contenteditable`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
+                    [{type: "text/plain", data: "e Tex"}], description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null,
                   [{type: "text/plain", data: "e Tex"}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text in <textarea> to contenteditable
   description = "dragging text in <textarea> to contenteditable";
   container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea = document.querySelector("div#container > textarea");
   contenteditable = document.querySelector("div#container > div");
   textarea.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -845,32 +1018,40 @@ async function doTest() {
     destElement: contenteditable,
   });
   is(textarea.value, "Linne2",
      `${description}: dragged range should be removed from <textarea>`);
   todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>",
      `${description}: dragged content should be inserted into contenteditable`);
   todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>",
      `${description}: dragged content should be inserted into contenteditable`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null,
+                    [{type: "text/plain", data: `e1${kNativeLF}Li`}], description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <input> and contenteditable`);
   checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null,
                   [{type: "text/plain", data: `e1${kNativeLF}Li`}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging text in <textarea> to contenteditable
   description = "copy-dragging text in <textarea> to contenteditable";
   container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea = document.querySelector("div#container > textarea");
   contenteditable = document.querySelector("div#container > div");
   textarea.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -882,31 +1063,38 @@ async function doTest() {
     dragEvent: kModifiersToCopy,
   });
   is(textarea.value, "Line1\nLine2",
      `${description}: dragged range should be removed from <textarea>`);
   todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>",
      `${description}: dragged content should be inserted into contenteditable`);
   todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>",
      `${description}: dragged content should be inserted into contenteditable`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null,
+                    [{type: "text/plain", data: `e1${kNativeLF}Li`}], description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null,
                   [{type: "text/plain", data: `e1${kNativeLF}Li`}], description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text in <input> to other <input>
   description = "dragging text in <input> to other <input>";
   container.innerHTML = '<input value="Some Text"><input>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input = document.querySelector("div#container > input");
   otherInput = document.querySelector("div#container > input + input");
   input.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -915,31 +1103,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: otherInput,
   });
   is(input.value, "Somt",
      `${description}: dragged range should be removed from <input>`);
   is(otherInput.value, "e Tex",
      `${description}: dragged content should be inserted into other <input>`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <input> and other <input>`);
   checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], otherInput, "insertFromDrop", "e Tex", null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other <input>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging text in <input> to other <input>
   description = "copy-dragging text in <input> to other <input>";
   container.innerHTML = '<input value="Some Text"><input>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input = document.querySelector("div#container > input");
   otherInput = document.querySelector("div#container > input + input");
   input.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -949,30 +1144,36 @@ async function doTest() {
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: otherInput,
     dragEvent: kModifiersToCopy,
   });
   is(input.value, "Some Text",
      `${description}: dragged range shouldn't be removed from <input>`);
   is(otherInput.value, "e Tex",
      `${description}: dragged content should be inserted into other <input>`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on  other <input>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], otherInput, "insertFromDrop", "e Tex", null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on  other <input>`);
   checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other <input>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text in <input> to <textarea>
   description = "dragging text in <input> to other <textarea>";
   container.innerHTML = '<input value="Some Text"><textarea></textarea>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input = document.querySelector("div#container > input");
   textarea = document.querySelector("div#container > textarea");
   input.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -981,31 +1182,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: textarea,
   });
   is(input.value, "Somt",
      `${description}: dragged range should be removed from <input>`);
   is(textarea.value, "e Tex",
      `${description}: dragged content should be inserted into <textarea>`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <input> and <textarea>`);
   checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "e Tex", null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging text in <input> to <textarea>
   description = "copy-dragging text in <input> to <textarea>";
   container.innerHTML = '<input value="Some Text"><textarea></textarea>';
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   input = document.querySelector("div#container > input");
   textarea = document.querySelector("div#container > textarea");
   input.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -1015,30 +1223,36 @@ async function doTest() {
     srcSelection: SpecialPowers.wrap(input).editor.selection,
     destElement: textarea,
     dragEvent: kModifiersToCopy,
   });
   is(input.value, "Some Text",
      `${description}: dragged range shouldn't be removed from <input>`);
   is(textarea.value, "e Tex",
      `${description}: dragged content should be inserted into <textarea>`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on  <textarea>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", "e Tex", null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on  <textarea>`);
   checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text in <textarea> to <input>
   description = "dragging text in <textarea> to <input>";
   container.innerHTML = "<textarea>Line1\nLine2</textarea><input>";
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea = document.querySelector("div#container > textarea");
   input = document.querySelector("div#container > input");
   textarea.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -1047,31 +1261,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(textarea).editor.selection,
     destElement: input,
   });
   is(textarea.value, "Linne2",
      `${description}: dragged range should be removed from <textarea>`);
   is(input.value, "e1 Li",
      `${description}: dragged content should be inserted into <input>`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <textarea> and <input>`);
   checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging text in <textarea> to <input>
   description = "copy-dragging text in <textarea> to <input>";
   container.innerHTML = "<textarea>Line1\nLine2</textarea><input>";
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea = document.querySelector("div#container > textarea");
   input = document.querySelector("div#container > input");
   textarea.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -1081,30 +1302,36 @@ async function doTest() {
     srcSelection: SpecialPowers.wrap(textarea).editor.selection,
     destElement: input,
     dragEvent: kModifiersToCopy,
   });
   is(textarea.value, "Line1\nLine2",
      `${description}: dragged range shouldn't be removed from <textarea>`);
   is(input.value, "e1 Li",
      `${description}: dragged content should be inserted into <input>`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on <input>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on <input>`);
   checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging text in <textarea> to other <textarea>
   description = "dragging text in <textarea> to other <textarea>";
   container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>";
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea = document.querySelector("div#container > textarea");
   otherTextarea = document.querySelector("div#container > textarea + textarea");
   textarea.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -1113,31 +1340,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: SpecialPowers.wrap(textarea).editor.selection,
     destElement: otherTextarea,
   });
   is(textarea.value, "Linne2",
      `${description}: dragged range should be removed from <textarea>`);
   is(otherTextarea.value, "e1\nLi",
      `${description}: dragged content should be inserted into other <textarea>`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <textarea> and other <textarea>`);
   checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging text in <textarea> to other <textarea>
   description = "copy-dragging text in <textarea> to other <textarea>";
   container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>";
   document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
   textarea = document.querySelector("div#container > textarea");
   otherTextarea = document.querySelector("div#container > textarea + textarea");
   textarea.setSelectionRange(3, 8);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8),
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "",
        `${description}: dataTransfer should have not have selected nodes as "text/html"`);
@@ -1147,30 +1381,36 @@ async function doTest() {
     srcSelection: SpecialPowers.wrap(textarea).editor.selection,
     destElement: otherTextarea,
     dragEvent: kModifiersToCopy,
   });
   is(textarea.value, "Line1\nLine2",
      `${description}: dragged range shouldn't be removed from <textarea>`);
   is(otherTextarea.value, "e1\nLi",
      `${description}: dragged content should be inserted into other <textarea>`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on other <textarea>`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on other <textarea>`);
   checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on <textarea>`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging multiple-line text in contenteditable to <input>
   description = "dragging multiple-line text in contenteditable to <input>";
   container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>';
   contenteditable = document.querySelector("div#container > div");
   input = document.querySelector("div#container > input");
   selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3,
                              contenteditable.firstChild.nextSibling.firstChild, 2);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`,
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>",
        `${description}: dataTransfer should have have selected nodes as "text/html"`);
@@ -1179,31 +1419,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: input,
   });
   is(contenteditable.innerHTML, "<div>Linne2</div>",
      `${description}: dragged content should be removed from contenteditable`);
   is(input.value, "e1 Li",
      `${description}: dragged range should be inserted into <input>`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <input> and contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging multiple-line text in contenteditable to <input>
   description = "copy-dragging multiple-line text in contenteditable to <input>";
   container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>';
   contenteditable = document.querySelector("div#container > div");
   input = document.querySelector("div#container > input");
   selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3,
                              contenteditable.firstChild.nextSibling.firstChild, 2);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`,
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>",
        `${description}: dataTransfer should have have selected nodes as "text/html"`);
@@ -1213,30 +1460,36 @@ async function doTest() {
     srcSelection: selection,
     destElement: input,
     dragEvent: kModifiersToCopy,
   });
   is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>",
      `${description}: dragged content should be removed from contenteditable`);
   is(input.value, "e1 Li",
      `${description}: dragged range should be inserted into <input>`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test dragging multiple-line text in contenteditable to <textarea>
   description = "dragging multiple-line text in contenteditable to <textarea>";
   container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>';
   contenteditable = document.querySelector("div#container > div");
   textarea = document.querySelector("div#container > textarea");
   selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3,
                              contenteditable.firstChild.nextSibling.firstChild, 2);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`,
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>",
        `${description}: dataTransfer should have have selected nodes as "text/html"`);
@@ -1245,31 +1498,38 @@ async function doTest() {
   await synthesizePlainDragAndDrop({
     srcSelection: selection,
     destElement: textarea,
   });
   is(contenteditable.innerHTML, "<div>Linne2</div>",
      `${description}: dragged content should be removed from contenteditable`);
   is(textarea.value, "e1\nLi",
      `${description}: dragged range should be inserted into <textarea>`);
+  todo_is(beforeinputEvents.length, 2,
+     `${description}: 2 "beforeinput" events should be fired on <textarea> and contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, description);
+    checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 2,
      `${description}: 2 "input" events should be fired on <textarea> and contenteditable`);
   checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, description);
   checkInputEvent(inputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
   // -------- Test copy-dragging multiple-line text in contenteditable to <textarea>
   description = "copy-dragging multiple-line text in contenteditable to <textarea>";
   container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>';
   contenteditable = document.querySelector("div#container > div");
   textarea = document.querySelector("div#container > textarea");
   selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3,
                              contenteditable.firstChild.nextSibling.firstChild, 2);
+  beforeinputEvents = [];
   inputEvents = [];
   dragEvents = [];
   onDrop = aEvent => {
     dragEvents.push(aEvent);
     comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`,
        `${description}: dataTransfer should have selected text as "text/plain"`);
     is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>",
        `${description}: dataTransfer should have have selected nodes as "text/html"`);
@@ -1279,24 +1539,30 @@ async function doTest() {
     srcSelection: selection,
     destElement: textarea,
     dragEvent: kModifiersToCopy,
   });
   is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>",
      `${description}: dragged content should be removed from contenteditable`);
   is(textarea.value, "e1\nLi",
      `${description}: dragged range should be inserted into <textarea>`);
+  todo_is(beforeinputEvents.length, 1,
+     `${description}: only one "beforeinput" events should be fired on contenteditable`);
+  if (beforeinputEvents.length > 0) {
+    checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
+  }
   is(inputEvents.length, 1,
      `${description}: only one "input" events should be fired on contenteditable`);
   checkInputEvent(inputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, description);
   is(dragEvents.length, 1,
      `${description}: Only one "drop" event should be fired on other contenteditable`);
   document.removeEventListener("drop", onDrop);
 
 
+  document.removeEventListener("beforeinput", onBeforeinput);
   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
@@ -73,254 +73,457 @@ async function copyHTMLContent(aInnerHTM
         SimpleTest.finish();
       },
       "text/html");
   });
 }
 
 function checkInputEvent(aEvent, aInputType, aData, aDataTransfer, 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}`);
+     `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+  is(aEvent.cancelable, aEvent.type === "beforeinput",
+     `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`);
   is(aEvent.bubbles, true,
-     `"input" event should always bubble ${aDescription}`);
+     `"${aEvent.type}" event should always bubble ${aDescription}`);
   is(aEvent.inputType, aInputType,
-     `inputType should be "${aInputType}" ${aDescription}`);
+     `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`);
   is(aEvent.data, aData,
-     `data should be ${aData} ${aDescription}`);
+     `data of "${aEvent.type}" event should be ${aData} ${aDescription}`);
   if (aDataTransfer === null) {
     is(aEvent.dataTransfer, null,
-       `dataTransfer should be null ${aDescription}`);
+       `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
   } else {
     for (let dataTransfer of aDataTransfer) {
       is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data,
-         `dataTransfer should have "${dataTransfer.data}" whose type is "${dataTransfer.type}" ${aDescription}`);
+         `dataTransfer of "${aEvent.type}" should have "${dataTransfer.data}" whose type is "${dataTransfer.type}" ${aDescription}`);
     }
   }
 }
 
 async function doTextareaTests(aTextarea) {
+  let beforeInputEvents = [];
   let inputEvents = [];
+  function onBeforeInput(aEvent) {
+    beforeInputEvents.push(aEvent);
+  }
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
+  aTextarea.addEventListener("beforeinput", onBeforeInput);
   aTextarea.addEventListener("input", onInput);
 
   await copyPlaintext("abc\ndef\nghi");
   aTextarea.focus();
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
   is(aTextarea.value,
      "> abc\n> def\n> ghi\n\n",
      "Pasted each line should start with \"> \"");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired #1');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, "#1");
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired #1');
   checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, "#1");
   aTextarea.value = "";
 
   await copyPlaintext("> abc\n> def\n> ghi");
   aTextarea.focus();
+  beforeInputEvents = [];
   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");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired #2');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, "#2");
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired #2');
   checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, "#2");
   aTextarea.value = "";
 
   await copyPlaintext("> abc\n> def\n\nghi");
   aTextarea.focus();
+  beforeInputEvents = [];
   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");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired #3');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, "#3");
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired #3');
   checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, "#3");
   aTextarea.value = "";
 
   await copyPlaintext("abc\ndef\n\n");
   aTextarea.focus();
+  beforeInputEvents = [];
   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 \">\"");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired #4');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, "#4");
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired #4');
   checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, "#4");
   aTextarea.value = "";
 
+  await copyPlaintext("abc\ndef\n\n");
+  aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true});
+  aTextarea.focus();
+  beforeInputEvents = [];
+  inputEvents = [];
+  synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+  is(aTextarea.value, "",
+     'Pasting as quote should have been canceled if "paste" event was canceled');
+  is(beforeInputEvents.length, 0,
+     'No "beforeinput" event should be fired since "paste" event was canceled #5');
+  is(inputEvents.length, 0,
+     'No "input" event should be fired since "paste" was canceled #5');
+  aTextarea.value = "";
+
+  await copyPlaintext("abc\ndef\n\n");
+  aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
+  aTextarea.focus();
+  beforeInputEvents = [];
+  inputEvents = [];
+  synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+  todo_is(aTextarea.value, "",
+     'Pasting as quote should have been canceled if "beforeinput" event was canceled');
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired #5');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, "#6");
+  }
+  todo_is(inputEvents.length, 0,
+     'No "input" event should be fired since "beforeinput" was canceled #6');
+  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});
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   is(aTextarea.value, "abc",
      "If 'click' event is consumed at capturing phase of the <body>, paste should not be canceled");
   is(pasteEventCount, 1,
      "If 'click' event is consumed at capturing phase of the <body>, 'paste' event should still be fired");
+  todo_is(beforeInputEvents.length, 1,
+     '"beforeinput" event should be fired when the "click" event is canceled');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, 'when the "click" event is canceled');
+  }
   is(inputEvents.length, 1,
      '"input" event should be fired when the "click" event is canceled');
+  checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, 'when the "click" event is canceled');
   aTextarea.value = "";
 
   await copyPlaintext("abc");
   aTextarea.focus();
   aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  beforeInputEvents = [];
   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");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired even if "mouseup" event is canceled');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, 'even if "mouseup" event is canceled');
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired even if "mouseup" event is canceled');
   checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, 'even if "mouseup" event is canceled');
   aTextarea.value = "";
 
   await copyPlaintext("abc");
   aTextarea.focus();
   aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   is(aTextarea.value, "abc",
      "If 'click' event handler is added to the <textarea>, paste should not be canceled");
   is(pasteEventCount, 1,
      "If 'click' event handler is added to the <textarea>, 'paste' event should be fired once");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, 'even if "click" event is canceled in bubbling phase');
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired even if "click" event is canceled in bubbling phase');
   checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, '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;
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aTextarea, {button: 1});
   is(aTextarea.value, "",
      "If 'auxclick' event is consumed, paste should be canceled");
   is(pasteEventCount, 0,
      "If 'auxclick' event is consumed, 'paste' event should not be fired once");
+  is(beforeInputEvents.length, 0,
+     'No "beforeinput" event should be fired if "auxclick" event is canceled');
   is(inputEvents.length, 0,
      'No "input" event should be fired if "auxclick" event is canceled');
   aTextarea.value = "";
 
+  await copyPlaintext("abc");
+  aTextarea.focus();
+  aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true});
+  pasteEventCount = 0;
+  beforeInputEvents = [];
+  inputEvents = [];
+  synthesizeMouseAtCenter(aTextarea, {button: 1});
+  is(aTextarea.value, "",
+     "If 'paste' event is consumed, paste should be canceled");
+  is(pasteEventCount, 1,
+     'One "paste" event should be fired for making it possible to consume');
+  is(beforeInputEvents.length, 0,
+     'No "beforeinput" event should be fired if "paste" event is canceled');
+  is(inputEvents.length, 0,
+     'No "input" event should be fired if "paste" event is canceled');
+  aTextarea.value = "";
+
+  await copyPlaintext("abc");
+  aTextarea.focus();
+  aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
+  pasteEventCount = 0;
+  beforeInputEvents = [];
+  inputEvents = [];
+  synthesizeMouseAtCenter(aTextarea, {button: 1});
+  todo_is(aTextarea.value, "",
+     "If 'beforeinput' event is consumed, paste should be canceled");
+  is(pasteEventCount, 1,
+     'One "paste" event should be fired before "beforeinput" event is consumed');
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired for making it possible to consume');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, 'when "beforeinput" is canceled in bubbling phase');
+  }
+  todo_is(inputEvents.length, 0,
+     'No "input" event should be fired if "paste" event is canceled');
+  aTextarea.value = "";
+
   aTextarea.removeEventListener("paste", pasteEventLogger);
+  aTextarea.removeEventListener("beforeinput", onBeforeInput);
   aTextarea.removeEventListener("input", onInput);
 }
 
 async function doContenteditableTests(aEditableDiv) {
+  let beforeInputEvents = [];
   let inputEvents = [];
+  function onBeforeInput(aEvent) {
+    beforeInputEvents.push(aEvent);
+  }
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
+  aEditableDiv.addEventListener("beforeinput", onBeforeInput);
   aEditableDiv.addEventListener("input", onInput);
 
   await copyPlaintext("abc\ndef\nghi");
   aEditableDiv.focus();
+  beforeInputEvents = [];
   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");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired on the editing host');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null,
+                    [{type: "text/plain", data: "abc\ndef\nghi"}], "(contenteditable)");
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired on the editing host');
   checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null,
                   [{type: "text/plain", data: "abc\ndef\nghi"}], "(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});
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   is(aEditableDiv.innerHTML, "abc",
      "If 'click' event is consumed at capturing phase of the window, paste should not be canceled");
   is(pasteEventCount, 1,
      "If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once");
+  todo_is(beforeInputEvents.length, 1,
+     '"beforeinput" event should still be fired when the "click" event is canceled (contenteditable)');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", null,
+                    [{type: "text/plain", data: "abc"}], 'when the "click" event is canceled (contenteditable)');
+  }
   is(inputEvents.length, 1,
      '"input" event should still be fired when the "click" event is canceled (contenteditable)');
+  checkInputEvent(inputEvents[0], "insertFromPaste", null,
+                  [{type: "text/plain", data: "abc"}], 'when the "click" event is canceled (contenteditable)');
   aEditableDiv.innerHTML = "";
 
   await copyPlaintext("abc");
   aEditableDiv.focus();
   aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  beforeInputEvents = [];
   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");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired even if "mouseup" event is canceled (contenteditable)');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}],
+                    'even if "mouseup" event is canceled (contenteditable)');
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired even if "mouseup" event is canceled (contenteditable)');
   checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}],
                   'even if "mouseup" event is canceled (contenteditable)');
   aEditableDiv.innerHTML = "";
 
   await copyPlaintext("abc");
   aEditableDiv.focus();
   aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
   pasteEventCount = 0;
+  beforeInputEvents = [];
   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");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}],
+                    'even if "click" event is canceled in bubbling phase (contenteditable)');
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)');
   checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}],
                   '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;
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   is(aEditableDiv.innerHTML, "",
      "If 'auxclick' event is consumed, paste should be canceled");
   is(pasteEventCount, 0,
      "If 'auxclick' event is consumed, 'paste' event should not be fired");
+  is(beforeInputEvents.length, 0,
+     'No "beforeinput" event should be fired if "auxclick" event is canceled (contenteditable)');
   is(inputEvents.length, 0,
      'No "input" event should be fired if "auxclick" event is canceled (contenteditable)');
   aEditableDiv.innerHTML = "";
 
+  await copyPlaintext("abc");
+  aEditableDiv.focus();
+  aEditableDiv.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true});
+  pasteEventCount = 0;
+  beforeInputEvents = [];
+  inputEvents = [];
+  synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+  is(aEditableDiv.innerHTML, "",
+     "If 'paste' event is consumed, paste should be canceled");
+  is(pasteEventCount, 1,
+     'One "paste" event should be fired for making it possible to consume');
+  is(beforeInputEvents.length, 0,
+     'No "beforeinput" event should be fired if "paste" event is canceled (contenteditable)');
+  is(inputEvents.length, 0,
+     'No "input" event should be fired if "paste" event is canceled (contenteditable)');
+  aEditableDiv.innerHTML = "";
+
+  await copyPlaintext("abc");
+  aEditableDiv.focus();
+  aEditableDiv.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
+  pasteEventCount = 0;
+  beforeInputEvents = [];
+  inputEvents = [];
+  synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+  todo_is(aEditableDiv.innerHTML, "",
+     "If 'paste' event is consumed, paste should be canceled");
+  is(pasteEventCount, 1,
+     'One "paste" event should be fired before "beforeinput" event');
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired for making it possible to consume (contenteditable)');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}],
+                    'when "beforeinput" will be canceled (contenteditable)');
+  }
+  todo_is(inputEvents.length, 0,
+     'No "input" event should be fired if "beforeinput" event is canceled (contenteditable)');
+  aEditableDiv.innerHTML = "";
+
   // If clipboard event is disabled, InputEvent.dataTransfer should have only empty string.
   await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", false]]});
   await copyPlaintext("abc");
   aEditableDiv.focus();
   pasteEventCount = 0;
+  beforeInputEvents = [];
   inputEvents = [];
   synthesizeMouseAtCenter(aEditableDiv, {button: 1});
   is(aEditableDiv.innerHTML, "abc",
      "Even if clipboard event is disabled, paste should be done");
   is(pasteEventCount, 0,
      "If clipboard event is disabled, 'paste' event shouldn't be fired once");
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired even if clipboard event is disabled (contenteditable)');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}],
+                    "when clipboard event is disabled (contenteditable)");
+  }
   is(inputEvents.length, 1,
      'One "input" event should be fired even if clipboard event is disabled (contenteditable)');
   checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}],
                   "when clipboard event is disabled (contenteditable)");
   await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", true]]});
   aEditableDiv.innerHTML = "";
 
   aEditableDiv.removeEventListener("paste", pasteEventLogger);
@@ -328,40 +531,50 @@ async function doContenteditableTests(aE
   // 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();
+  beforeInputEvents = [];
   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');
   // On windows, HTML clipboard includes extra data.
   // The values are from widget/windows/nsDataObj.cpp.
   const kHTMLPrefix = (navigator.platform.includes("Win")) ? "<html><body>\n<!--StartFragment-->" : "";
   const kHTMLPostfix = (navigator.platform.includes("Win")) ? "<!--EndFragment-->\n</body>\n</html>" : "";
+  todo_is(beforeInputEvents.length, 1,
+     'One "beforeinput" event should be fired when pasting HTML');
+  if (beforeInputEvents.length > 0) {
+    checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null,
+                    [{type: "text/html",
+                      data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}],
+