Bug 1217700 part.4 Add automated tests for IMEContentObserver r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Wed, 19 Apr 2017 21:57:58 +0900
changeset 566206 b16dc026e2da2d68fed7db366e49ee26f4d2d64f
parent 566205 2a41d150ac17e28906d6396f099e92f4be5c7985
child 625241 030be5c50bab79c105bfb6360785bfa35d3dff13
push id55137
push usermasayuki@d-toybox.com
push dateFri, 21 Apr 2017 05:00:08 +0000
reviewersm_kato
bugs1217700
milestone55.0a1
Bug 1217700 part.4 Add automated tests for IMEContentObserver r?m_kato IMEContentObserver notifies IME of 3 notifications at most when editor is changed. The order is: 1. text change (with merged range if 2 or more change occurred during an edit transaction) 2. selection change (only the latest selection change. other changes occurred before that during an editor transaction are ignored) 3. position change (scrolled, resized, window moved, etc) This does not check the behavior in designMode because some operation in testWithHTMLEditor() causes unexpected behavior, e.g., moving focus. It *might* be bug of design mode. However, it doesn't matter for this bug. The important thing of this bug is, there should be automated tests for IMEContentObserver. And fortunately, IMEContentObserver does not check the type of editor. So, it's enough to test only contenteditable element for HTMLEditor at least for now. Therefore, I gave up to test it in designMode for now. MozReview-Commit-ID: 7L6ZlbVMU2P
testing/mochitest/tests/SimpleTest/EventUtils.js
widget/tests/window_composition_text_querycontent.xul
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -763,20 +763,21 @@ function _computeKeyCodeFromChar(aChar)
  *        However, if some of these attributes are true, this function activates
  *        the modifiers only during dispatching the key events.
  *        Note that if some of these values are false, they are ignored (i.e.,
  *        not inactivated with this function).
  *  - keyCode: Must be 0 - 255 (0xFF). If this is specified explicitly,
  *             .keyCode value is initialized with this value.
  *
  * aWindow is optional, and defaults to the current window object.
+ * aCallback is optional, use the callback for receiving notifications of TIP.
  */
-function synthesizeKey(aKey, aEvent, aWindow = window)
+function synthesizeKey(aKey, aEvent, aWindow = window, aCallback)
 {
-  var TIP = _getTIP(aWindow);
+  var TIP = _getTIP(aWindow, aCallback);
   if (!TIP) {
     return;
   }
   var KeyboardEvent = _getKeyboardEvent(aWindow);
   var modifiers = _emulateToActivateModifiers(TIP, aEvent, aWindow);
   var keyEventDict = _createKeyboardEventDictionary(aKey, aEvent, aWindow);
   var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
   var dispatchKeydown =
--- a/widget/tests/window_composition_text_querycontent.xul
+++ b/widget/tests/window_composition_text_querycontent.xul
@@ -58,16 +58,31 @@ function is(aLeft, aRight, aMessage)
   window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage);
 }
 
 function isnot(aLeft, aRight, aMessage)
 {
   window.opener.wrappedJSObject.SimpleTest.isnot(aLeft, aRight, aMessage);
 }
 
+function todo(aCondition, aMessage)
+{
+  window.opener.wrappedJSObject.SimpleTest.todo(aCondition, aMessage);
+}
+
+function todo_is(aLeft, aRight, aMessage)
+{
+  window.opener.wrappedJSObject.SimpleTest.todo_is(aLeft, aRight, aMessage);
+}
+
+function todo_isnot(aLeft, aRight, aMessage)
+{
+  window.opener.wrappedJSObject.SimpleTest.todo_isnot(aLeft, aRight, aMessage);
+}
+
 function isSimilarTo(aLeft, aRight, aAllowedDifference, aMessage)
 {
   if (Math.abs(aLeft - aRight) <= aAllowedDifference) {
     ok(true, aMessage);
   } else {
     ok(false, aMessage + ", got=" + aLeft + ", expected=" + (aRight - aAllowedDifference) + "~" + (aRight + aAllowedDifference));
   }
 }
@@ -5547,17 +5562,17 @@ function runNestedSettingValue()
   window.removeEventListener("compositionstart", eventHandler, true);
   window.removeEventListener("compositionupdate", eventHandler, true);
   window.removeEventListener("compositionend", eventHandler, true);
   window.removeEventListener("input", eventHandler, true);
   window.removeEventListener("text", eventHandler, true);
 
 }
 
-function runAsyncForceCommitTest(aNextTest)
+function runAsyncForceCommitTest()
 {
   var events;
   function eventHandler(aEvent)
   {
     events.push(aEvent);
   };
 
   // If IME commits composition for a request, TextComposition commits
@@ -5581,17 +5596,17 @@ function runAsyncForceCommitTest(aNextTe
          "runAsyncForceCommitTest: composition events shouldn't been fired by asynchronous call of nsITextInputProcessor.commitComposition()");
 
       window.removeEventListener("compositionstart", eventHandler, true);
       window.removeEventListener("compositionupdate", eventHandler, true);
       window.removeEventListener("compositionend", eventHandler, true);
       window.removeEventListener("input", eventHandler, true);
       window.removeEventListener("text", eventHandler, true);
 
-      SimpleTest.executeSoon(aNextTest);
+      SimpleTest.executeSoon(continueTest);
     }, 1);
     return true;
   };
 
   window.addEventListener("compositionstart", eventHandler, true);
   window.addEventListener("compositionupdate", eventHandler, true);
   window.addEventListener("compositionend", eventHandler, true);
   window.addEventListener("input", eventHandler, true);
@@ -6207,17 +6222,17 @@ function runControlCharTest()
   is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #4");
 
   SpecialPowers.clearUserPref("dom.compositionevent.allow_control_characters");
 
   textarea.removeEventListener("compositionupdate", handler, true);
   textarea.removeEventListener("compositionend", handler, true);
 }
 
-function runRemoveContentTest(aCallback)
+function runRemoveContentTest()
 {
   var events = [];
   function eventHandler(aEvent)
   {
     events.push(aEvent);
   }
   textarea.addEventListener("compositionstart", eventHandler, true);
   textarea.addEventListener("compositionupdate", eventHandler, true);
@@ -6298,17 +6313,17 @@ function runRemoveContentTest(aCallback)
       parent.insertBefore(textarea, nextSibling);
 
       textarea.removeEventListener("compositionstart", eventHandler, true);
       textarea.removeEventListener("compositionupdate", eventHandler, true);
       textarea.removeEventListener("compositionend", eventHandler, true);
       textarea.removeEventListener("input", eventHandler, true);
       textarea.removeEventListener("text", eventHandler, true);
 
-      SimpleTest.executeSoon(aCallback);
+      SimpleTest.executeSoon(continueTest);
     }, 50);
   }, 50);
 }
 
 function runTestOnAnotherContext(aPanelOrFrame, aFocusedEditor, aTestName)
 {
   aFocusedEditor.value = "";
 
@@ -6439,17 +6454,17 @@ function doPanelTest()
   gIsPanelHiding = true;
   panel.hidePopup();
 }
 
 function onPanelHidden(aEvent)
 {
   panel.hidden = true;
   ok(gIsPanelHiding, "runPanelTest: the panel is hidden unexpectedly");
-  finish();
+  SimpleTest.executeSoon(continueTest);
 }
 
 function runPanelTest()
 {
   panel.hidden = false;
   panel.openPopupAtScreen(window.screenX + window.outerWidth, 0, false);
 }
 
@@ -6764,19 +6779,19 @@ function runMaxLengthTest()
   synthesizeComposition({ type: "compositioncommitasis" });
 
   if (!checkContent("X", kDesc, "#5-2") ||
       !checkSelection(0, "", kDesc, "#5-2")) {
     return;
   }
 }
 
-function runEditorReframeTests(aCallback)
+function* runEditorReframeTests()
 {
-  function runEditorReframeTest(aEditor, aWindow, aEventType, aNextTest)
+  function runEditorReframeTest(aEditor, aWindow, aEventType)
   {
     function getValue()
     {
       return aEditor == contenteditable ?
         aEditor.innerHTML.replace("<br>", "") : aEditor.value;
     }
 
     var description = "runEditorReframeTest(" + aEditor.id + ", \"" + aEventType + "\"): ";
@@ -7040,85 +7055,969 @@ function runEditorReframeTests(aCallback
     aEditor.focus();
     aEditor.addEventListener(aEventType, doReframe);
 
     function doNext()
     {
       if (tests.length <= index) {
         aEditor.style.overflow = "auto";
         aEditor.removeEventListener(aEventType, doReframe);
-        requestAnimationFrame(function() { setTimeout(aNextTest); });
+        requestAnimationFrame(function() { SimpleTest.executeSoon(continueTest); });
         return;
       }
       tests[index].test();
       hitEventLoop(function () {
         tests[index].check();
         index++;
-        setTimeout(doNext, 0);
+        SimpleTest.executeSoon(doNext);
       }, 20);
     }
     doNext();
   }
 
   input.value = "";
-  runEditorReframeTest(input, window, "input", function () {
-    input.value = "";
-    runEditorReframeTest(input, window, "compositionupdate", function () {
-      textarea.value = "";
-      runEditorReframeTest(textarea, window, "input", function () {
-        textarea.value = "";
-        runEditorReframeTest(textarea, window, "compositionupdate", function () {
-          contenteditable.innerHTML = "";
-          runEditorReframeTest(contenteditable, windowOfContenteditable, "input", function () {
-            contenteditable.innerHTML = "";
-            runEditorReframeTest(contenteditable, windowOfContenteditable, "compositionupdate", function () {
-              aCallback();
-            });
-          });
-        });
-      });
+  yield runEditorReframeTest(input, window, "input");
+  input.value = "";
+  yield runEditorReframeTest(input, window, "compositionupdate");
+  textarea.value = "";
+  yield runEditorReframeTest(textarea, window, "input");
+  textarea.value = "";
+  yield runEditorReframeTest(textarea, window, "compositionupdate");
+  contenteditable.innerHTML = "";
+  yield runEditorReframeTest(contenteditable, windowOfContenteditable, "input");
+  contenteditable.innerHTML = "";
+  yield runEditorReframeTest(contenteditable, windowOfContenteditable, "compositionupdate");
+}
+
+function* runIMEContentObserverTest()
+{
+  var notifications = [];
+  var callContinueTest = false;
+  function callback(aTIP, aNotification)
+  {
+    if (aNotification.type != "notify-end-input-transaction") {
+      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);
+    }
+    return true;
+  }
+
+  function dumpUnexpectedNotifications(aDescription, aExpectedCount)
+  {
+    if (notifications.length <= aExpectedCount) {
+      return;
+    }
+    for (var i = aExpectedCount; i < notifications.length; i++) {
+      ok(false,
+         aDescription + " caused unexpected notification: " + notifications[i].type);
+    }
+  }
+
+  function waitUntilNotificationsReceived()
+  {
+    if (notifications.length > 0) {
+      SimpleTest.executeSoon(continueTest);
+    } else {
+      callContinueTest = true;
+    }
+  }
+
+  function flushNotifications()
+  {
+    // FYI: Dispatching non-op keyboard events causes forcibly flushing pending
+    //      notifications.
+    synthesizeKey("KEY_Unidentified", { code: "" });
+    SimpleTest.executeSoon(()=>{
+      notifications = [];
+      continueTest();
     });
-  });
+  }
+
+  function ensureToRemovePrecedingPositionChangeNotification(aDescription)
+  {
+    if (!notifications.length) {
+      return;
+    }
+    if (notifications[0].type != "notify-position-change") {
+      return;
+    }
+    // Sometimes, notify-position-change is notified first separately if
+    // the operation causes scroll or something.  Tests can ignore this.
+    ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it");
+    notifications.shift();
+  }
+
+  function getNativeText(aXPText)
+  {
+    if (kLF == "\n") {
+      return aXPText;
+    }
+    return aXPText.replace(/\n/g, kLF);
+  }
+
+  function checkPositionChangeNotification(aNotification, aDescription)
+  {
+    is(!aNotification || aNotification.type, "notify-position-change",
+       aDescription + " should cause position change notification");
+  }
+
+  function checkSelectionChangeNotification(aNotification, aDescription, aExpected)
+  {
+    is(!aNotification || aNotification.type, "notify-selection-change",
+       aDescription + " should cause selection change notification");
+    if (!aNotification || (aNotification.type != "notify-selection-change")) {
+      return;
+    }
+    is(aNotification.offset, aExpected.offset,
+       aDescription + " should cause selection change notification whose offset is " + aExpected.offset);
+    is(aNotification.text, aExpected.text,
+       aDescription + " should cause selection change notification whose text is '" + aExpected.text + "'");
+    is(aNotification.collapsed, aExpected.text.length == 0,
+       aDescription + " should cause selection change notification whose collapsed is " + (aExpected.text.length == 0));
+    is(aNotification.length, aExpected.text.length,
+       aDescription + " should cause selection change notification whose length is " + aExpected.text.length);
+    is(aNotification.reversed, aExpected.reversed || false,
+       aDescription + " should cause selection change notification whose reversed is " + (aExpected.reversed || false));
+    is(aNotification.writingMode, aExpected.writingMode || "horizontal-tb",
+       aDescription + " should cause selection change notification whose writingMode is '" + (aExpected.writingMode || "horizontal-tb"));
+  }
+
+  function checkTextChangeNotification(aNotification, aDescription, aExpected)
+  {
+    is(!aNotification || aNotification.type, "notify-text-change",
+       aDescription + " should cause text change notification");
+    if (!aNotification || aNotification.type != "notify-text-change") {
+      return;
+    }
+    is(aNotification.offset, aExpected.offset,
+       aDescription + " should cause text change notification whose offset is " + aExpected.offset);
+    is(aNotification.removedLength, aExpected.removedLength,
+       aDescription + " should cause text change notification whose removedLength is " + aExpected.removedLength);
+    is(aNotification.addedLength, aExpected.addedLength,
+       aDescription + " should cause text change notification whose addedLength is " + aExpected.addedLength);
+  }
+
+  function getAsInnerHTML(aElement)
+  {
+    if (aElement.tagName.toLowerCase() == "input" || aElement.tagName.toLowerCase() == "textarea") {
+      return aElement.value;
+    }
+    return aElement.innerHTML;
+  }
+
+  function* testWithPlaintextEditor(aDescription, aElement, aTestLineBreaker)
+  {
+    aElement.value = "";
+    aElement.blur();
+    var doc = aElement.ownerDocument;
+    var win = doc.defaultView;
+    aElement.focus();
+    yield flushNotifications();
+
+    // "a[]"
+    var description = aDescription + "typing 'a'";
+    notifications = [];
+    synthesizeKey("a", { code: "KeyA" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 0, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "ab[]"
+    description = aDescription + "typing 'b'";
+    notifications = [];
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 0, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "abc[]"
+    description = aDescription + "typing 'c'";
+    notifications = [];
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 3, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "ab[c]"
+    description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 2, text: "c", reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "a[bc]"
+    description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: "bc", reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[abc]"
+    description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "abc", reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[]abc"
+    description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "" });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[a]bc"
+    description = aDescription + "selecting 'a' with pressing Shift+ArrowRight";
+    notifications = [];
+    synthesizeKey("KEY_ArrowRight", { code: "ArrowRight", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "a" });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[ab]c"
+    description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight";
+    notifications = [];
+    synthesizeKey("KEY_ArrowRight", { code: "ArrowRight", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "ab" });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[]c"
+    description = aDescription + "deleting 'ab' with pressing Delete";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "[]"
+    description = aDescription + "deleting following 'c' with pressing Delete";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 1, addedLength: 0 });
+    checkPositionChangeNotification(notifications[1], description);
+    dumpUnexpectedNotifications(description, 2);
+
+    // "abc[]"
+    synthesizeKey("a", { code: "KeyA" }, win, callback);
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    yield flushNotifications();
+
+    // "ab[]"
+    description = aDescription + "deleting 'c' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 1, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "[ab]"
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "[]"
+    description = aDescription + "deleting 'ab' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "abcd[]"
+    synthesizeKey("a", { code: "KeyA" }, win, callback);
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    synthesizeKey("d", { code: "KeyD" }, win, callback);
+    yield flushNotifications();
+
+    // "a[bc]d"
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "a[]d"
+    description = aDescription + "deleting 'bc' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "a[bc]d"
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "aB[]d"
+    description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B";
+    notifications = [];
+    synthesizeKey("B", { code: "KeyB", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    if (!aTestLineBreaker) {
+      return;
+    }
+
+    // "aB\n[]d"
+    description = aDescription + "inserting a line break after 'B' with pressing Enter";
+    notifications = [];
+    synthesizeKey("KEY_Enter", { code: "Enter" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: kLFLen });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "aB[]d"
+    description = aDescription + "removing a line break after 'B' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: kLFLen, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "a[B]d"
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "a\n[]d"
+    description = aDescription + "replacing 'B' with a line break with pressing Enter";
+    notifications = [];
+    synthesizeKey("KEY_Enter", { code: "Enter" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 1, addedLength: kLFLen });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "a[\n]d"
+    description = aDescription + "selecting '\n' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: kLF, reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "a[]d"
+    description = aDescription + "removing selected '\n' with pressing Delete";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: kLFLen, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // ab\ncd\nef\ngh\n[]
+    description = aDescription + "setting the value property to 'ab\ncd\nef\ngh\n'";
+    notifications = [];
+    aElement.value = "ab\ncd\nef\ngh\n";
+    yield waitUntilNotificationsReceived();
+    checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: getNativeText("ab\ncd\nef\ngh\n").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("ab\ncd\nef\ngh\n").length, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // []
+    description = aDescription + "setting the value property to ''";
+    notifications = [];
+    aElement.value = "";
+    yield waitUntilNotificationsReceived();
+    // XXX Removing invisible <br> or something? The removed length is a line breaker length longer.
+    checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: getNativeText("ab\ncd\nef\ngh\n").length + kLFLen, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+  }
+
+  function* testWithHTMLEditor(aDescription, aElement, aDefaultParagraphSeparator)
+  {
+    var doc = aElement.ownerDocument;
+    var win = doc.defaultView;
+    var sel = doc.getSelection();
+    var inDesignMode = doc.designMode == "on";
+    var offsetAtStart = 0;
+    var offsetAtContainer = 0;
+    var isDefaultParagraphSeparatorBlock = aDefaultParagraphSeparator != "br";
+    doc.execCommand("defaultParagraphSeparator", false, aDefaultParagraphSeparator);
+
+    // "[]", "<p>[]</p>" or "<div>[]</div>"
+    switch (aDefaultParagraphSeparator) {
+      case "br":
+        aElement.innerHTML = "";
+        break;
+      case "p":
+      case "div":
+        aElement.innerHTML = "<" + aDefaultParagraphSeparator + "></" + aDefaultParagraphSeparator + ">";
+        sel.collapse(aElement.firstChild, 0);
+        offsetAtContainer = offsetAtStart + kLFLen;
+        break;
+      default:
+        ok(false, aDescription + "aDefaultParagraphSeparator is illegal value");
+        yield flushPendingNotifications();
+        return;
+    }
+
+    if (inDesignMode) {
+      win.focus();
+    } else {
+      aElement.focus();
+    }
+    yield flushNotifications();
+
+    // "a[]"
+    var description = aDescription + "typing 'a'";
+    notifications = [];
+    synthesizeKey("a", { code: "KeyA" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    if (isDefaultParagraphSeparatorBlock) {
+      // XXX This must detect a bug.  The offset of inserting "a" into the first block should be
+      //     after the line breaker caused by the first block.
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: 0, addedLength: 1 });
+    } else {
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 0, addedLength: 1 });
+    }
+    checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "ab[]"
+    description = aDescription + "typing 'b'";
+    notifications = [];
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 0, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "abc[]"
+    description = aDescription + "typing 'c'";
+    notifications = [];
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 0, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 3 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "ab[c]"
+    description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, text: "c", reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "a[bc]"
+    description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: "bc", reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[abc]"
+    description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "abc", reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[]abc"
+    description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "" });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[a]bc"
+    description = aDescription + "selecting 'a' with pressing Shift+ArrowRight";
+    notifications = [];
+    synthesizeKey("KEY_ArrowRight", { code: "ArrowRight", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "a" });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[ab]c"
+    description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight";
+    notifications = [];
+    synthesizeKey("KEY_ArrowRight", { code: "ArrowRight", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "ab" });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "[]c"
+    description = aDescription + "deleting 'ab' with pressing Delete";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "[]"
+    description = aDescription + "deleting following 'c' with pressing Delete";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    if (isDefaultParagraphSeparatorBlock) {
+      // XXX Making a block empty causes removing the block once.
+      //     However, after that, new block is inserted with <br>.
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\nc").length, addedLength: kLFLen * 2 });
+    } else {
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 1, addedLength: kLFLen });
+    }
+    checkPositionChangeNotification(notifications[1], description);
+    dumpUnexpectedNotifications(description, 2);
+
+    // "abc[]"
+    synthesizeKey("a", { code: "KeyA" }, win, callback);
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    yield flushNotifications();
+
+    // "ab[]"
+    description = aDescription + "deleting 'c' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 1, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "[ab]"
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "[]"
+    description = aDescription + "deleting 'ab' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "abcd[]"
+    synthesizeKey("a", { code: "KeyA" }, win, callback);
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    synthesizeKey("d", { code: "KeyD" }, win, callback);
+    yield flushNotifications();
+
+    // "a[bc]d"
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "a[]d"
+    description = aDescription + "deleting 'bc' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "a[bc]d"
+    synthesizeKey("b", { code: "KeyB" }, win, callback);
+    synthesizeKey("c", { code: "KeyC" }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "aB[]d"
+    description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B";
+    notifications = [];
+    synthesizeKey("B", { code: "KeyB", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "aB<br>[]d" or "<block>ab</block><block>[]d</block>"
+    description = aDescription + "inserting a line break after 'B' with pressing Enter";
+    notifications = [];
+    synthesizeKey("KEY_Enter", { code: "Enter" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    if (isDefaultParagraphSeparatorBlock) {
+      // Splitting current block causes removing "<block>aB" and inserting "<block>aB</block><block>".
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\naB").length, addedLength: getNativeText("\naB\n").length });
+    } else {
+      // Oddly, inserting <br> causes removing "aB" and inserting "ab<br>".
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: getNativeText("ab\n").length });
+    }
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "aB[]d"
+    description = aDescription + "removing a line break after 'B' with pressing Backspace";
+    notifications = [];
+    synthesizeKey("KEY_Backspace", { code: "Backspace" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    if (isDefaultParagraphSeparatorBlock) {
+      // Joining two blocks causes removing both block elements and inserting new block element.
+      checkTextChangeNotification(notifications[0], description, { offset: offsetAtContainer - kLFLen, removedLength: getNativeText("\naB\nd").length, addedLength: getNativeText("\naBd").length });
+      checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+      checkPositionChangeNotification(notifications[2], description);
+      dumpUnexpectedNotifications(description, 3);
+    } else {
+      checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: kLFLen, addedLength: 0 });
+      todo_is(notifications.length, 3, description + " should cause 3 notifications");
+      todo_is(notifications[1] && notifications[1].type, "notify-selection-change", description + " should cause selection change notification");
+      todo_is(notifications[2] && notifications[2].type, "notify-position-change", description + " should cause position change notification");
+    }
+
+    // "a[B]d"
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield flushNotifications();
+
+    // "a<br>[]d" or "<block>a</block><block>[]d</block>"
+    description = aDescription + "replacing 'B' with a line break with pressing Enter";
+    notifications = [];
+    synthesizeKey("KEY_Enter", { code: "Enter" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    if (isDefaultParagraphSeparatorBlock) {
+      // Splitting current block causes removing "<block>aB" and inserting "<block>aB</block><block>".
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\naB").length, addedLength: getNativeText("\na\n").length });
+    } else {
+      checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 1, addedLength: kLFLen });
+    }
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "a[<br>]d" or "<block>a[</block><block>]d</block>"
+    description = aDescription + "selecting '\\n' with pressing Shift+ArrowLeft";
+    notifications = [];
+    synthesizeKey("KEY_ArrowLeft", { code: "ArrowLeft", shiftKey: true }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: kLF, reversed: true });
+    dumpUnexpectedNotifications(description, 1);
+
+    // "a[]d"
+    description = aDescription + "removing selected '\\n' with pressing Delete";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    if (isDefaultParagraphSeparatorBlock) {
+      // Joining the blocks causes removing "<block>a</block><block>d</block>" and inserting "<block>ad</block>".
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\na\nd").length, addedLength: getNativeText("\nad").length });
+    } else {
+      checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: kLFLen, addedLength: 0 });
+    }
+    checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"
+    description = aDescription + "inserting HTML which has nested block elements";
+    notifications = [];
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    // There is <br> after the end of the line.  Therefore, removed length includes a line breaker length.
+    if (isDefaultParagraphSeparatorBlock) {
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\nad\n").length, addedLength: getNativeText("\n1\n2\n345").length });
+    } else {
+      checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2 + kLFLen, addedLength: getNativeText("\n1\n2\n345").length });
+    }
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1[<div>2<div>3</div>4</div>]5</div>" and removing selection
+    sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(2), 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>15</div>", description + " should remove '<div>2<div>3</div>4</div>'");
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 0 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1[<div>2<div>3</div>]4</div>5</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+    sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(2), 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (partially #1) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>145</div>", description + " should remove '<div>2<div>3</div></div>'");
+    // It causes removing '<div>2<div>3</div>4</div>' and inserting '4'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1[<div>2<div>]3</div>4</div>5</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+    sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (partially #2) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>13<div>4</div>5</div>", description + " should remove '<div>2</div>'");
+    // It causes removing '1<div>2<div>3</div></div>' and inserting '13<div>'.
+    checkTextChangeNotification(notifications[0], description, { offset: kLFLen + offsetAtStart, removedLength: getNativeText("1\n2\n3").length, addedLength: getNativeText("13\n").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1<div>2<div>3[</div>4</div>]5</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+    sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(2), 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (partially #3) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>1<div>2<div>35</div></div></div>", description + " should remove '4'");
+    // It causes removing '45' and inserting '5'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: 2, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"
+    description = aDescription + "inserting HTML which has a pair of nested block elements";
+    notifications = [];
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtStart, removedLength: getNativeText("\n1\n2\n35").length, addedLength: getNativeText("\n1\n2\n345\n6\n789").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection
+    sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (between same level descendants) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>1<div>2<div>37</div></div><div>8</div>9</div>", description + " should remove '456<div>7'");
+    // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>37</div><div>'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n7").length, addedLength: getNativeText("\n37\n").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+    sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (between different level descendants #1) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>1<div>27</div><div>8</div>9</div>", description + " should remove '<div>2<div>3</div>4</div>5<div>6<div>7</div>'");
+    // It causes removing '<div>2<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>27</div>'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n345\n6\n7").length, addedLength: getNativeText("\n27\n").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+    sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (between different level descendants #2) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>1<div>29</div></div>", description + " should remove '<div>3</div>4</div>5<div>6<div>7</div>8</div>'");
+    // It causes removing '<div>3</div>4</div>5</div>6<div>7</div>8</div>9' and inserting '9</div>'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n789").length, addedLength: 1 });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1<div>2<div>3[</div>4</div>5<div>]6<div>7</div>8</div>9</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+    sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).firstChild, 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (between different level descendants #3) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>1<div>2<div>36<div>7</div>8</div></div>9</div>", description + " should remove '<div>36<div>7</div>8</div>'");
+    // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n78").length, addedLength: getNativeText("\n36\n78").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+
+    // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection
+    aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+    sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0);
+    yield flushNotifications();
+    description = aDescription + "deleting child nodes (between different level descendants #4) with pressing Delete key";
+    notifications = [];
+    synthesizeKey("KEY_Delete", { code: "Delete" }, win, callback);
+    yield waitUntilNotificationsReceived();
+    ensureToRemovePrecedingPositionChangeNotification();
+    is(aElement.innerHTML, "<div>1<div>2<div>39</div></div></div>", description + " should remove '</div>4</div>5<div>6<div>7</div>8</div>'");
+    // It causes removing '</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'.
+    checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: getNativeText("45\n6\n789").length, addedLength: getNativeText("9").length });
+    checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+    checkPositionChangeNotification(notifications[2], description);
+    dumpUnexpectedNotifications(description, 3);
+  }
+
+  yield* testWithPlaintextEditor("runIMEContentObserverTest with input element: ", input, false);
+  yield* testWithPlaintextEditor("runIMEContentObserverTest with textarea element: ", textarea, true);
+  yield* testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is br): ", contenteditable, "br");
+  yield* testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is p): ", contenteditable, "p");
+  yield* testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is div): ", contenteditable, "div");
+  // XXX Due to the difference of HTML editor behavior between designMode and contenteditable,
+  //     testWithHTMLEditor() gets some unexpected behavior.  However, IMEContentObserveri is
+  //     not depend on editor's detail.  So, we should investigate this issue later.  It's not
+  //     so important for now.
+  // yield* testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is br): ", iframe2.contentDocument.body, "br");
+  // yield* testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is p): ", iframe2.contentDocument.body, "p");
+  // yield* testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is div): ", iframe2.contentDocument.body, "div");
 }
 
-function runTest()
+var gTestContinuation = null;
+
+function continueTest()
 {
-  contenteditable = document.getElementById("iframe4").contentDocument.getElementById("contenteditable");
-  windowOfContenteditable = document.getElementById("iframe4").contentWindow;
-  textareaInFrame = iframe.contentDocument.getElementById("textarea");
-
+  if (!gTestContinuation) {
+    gTestContinuation = testBody();
+  }
+  var ret = gTestContinuation.next();
+  if (ret.done) {
+    finish();
+  }
+}
+
+function* testBody()
+{
   runUndoRedoTest();
   runCompositionCommitAsIsTest();
   runCompositionCommitTest();
   runCompositionTest();
   runCompositionEventTest();
   runQueryTextRectInContentEditableTest();
   runCharAtPointTest(textarea, "textarea in the document");
   runCharAtPointAtOutsideTest();
   runSetSelectionEventTest();
   runQueryTextContentEventTest();
   runQueryIMESelectionTest();
   runQueryContentEventRelativeToInsertionPoint();
+  yield* runIMEContentObserverTest();
   runCSSTransformTest();
   runBug722639Test();
   runForceCommitTest();
   runNestedSettingValue();
   runBug811755Test();
   runIsComposingTest();
   runRedundantChangeTest();
   runNotRedundantChangeTest();
   runNativeLineBreakerTest();
   runControlCharTest();
-  runEditorReframeTests(function () {
-    runAsyncForceCommitTest(function () {
-      runRemoveContentTest(function () {
-        runFrameTest();
-        runPanelTest();
-        runMaxLengthTest();
-      });
-    });
-  });
+  yield* runEditorReframeTests();
+  yield runAsyncForceCommitTest();
+  yield runRemoveContentTest();
+  runFrameTest();
+  yield runPanelTest();
+  runMaxLengthTest();
+}
+
+function runTest()
+{
+  contenteditable = document.getElementById("iframe4").contentDocument.getElementById("contenteditable");
+  windowOfContenteditable = document.getElementById("iframe4").contentWindow;
+  textareaInFrame = iframe.contentDocument.getElementById("textarea");
+  continueTest();
 }
 
 ]]>
 </script>
 
 </window>