Bug 1639161 - part 1: Create `ReplaceTextTransaction` class r=m_kato
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 21 May 2020 02:30:09 +0000
changeset 531401 2ac7be4d82390baf45f3e50dd6693604a93df651
parent 531400 9749264fe7adfb475501643fc19b69fcfceeccc4
child 531402 5f86836c31e5c21db6ef51ada6893f5e7c65be5c
push id37439
push userbtara@mozilla.com
push dateThu, 21 May 2020 21:49:34 +0000
treeherdermozilla-central@92c11f0bf14b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersm_kato
bugs1639161
milestone78.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 1639161 - part 1: Create `ReplaceTextTransaction` class r=m_kato Currently, when `HTMLEditor` replaces text in a text node, `HTMLEditor` creates a set of `DeleteTextTransaction` and `InsertTextTransaction`. However, this has bad impact for footprint and causes the callers messy. This patch creates `ReplaceTextTransaction` instead and `HTMLEditor::ReplaceTextWithTransaction()` as its wrapper. Unfortunately, this becomes not calling `nsIEditActionListener::DidDeleteText()`, however, this is not used by mozilla-central, comm-central nor BlueGriffon. IIRC, it was not removed for some legacy addons of Thunderbird. Therefore, it must be okay to remove it. Differential Revision: https://phabricator.services.mozilla.com/D76078
editor/libeditor/CompositionTransaction.cpp
editor/libeditor/DeleteTextTransaction.cpp
editor/libeditor/EditTransactionBase.cpp
editor/libeditor/EditTransactionBase.h
editor/libeditor/EditorBase.cpp
editor/libeditor/EditorBase.h
editor/libeditor/HTMLEditor.cpp
editor/libeditor/HTMLEditor.h
editor/libeditor/InsertTextTransaction.cpp
editor/libeditor/ReplaceTextTransaction.cpp
editor/libeditor/ReplaceTextTransaction.h
editor/libeditor/SelectionState.cpp
editor/libeditor/SelectionState.h
editor/libeditor/moz.build
editor/libeditor/tests/test_bug1310912.html
--- a/editor/libeditor/CompositionTransaction.cpp
+++ b/editor/libeditor/CompositionTransaction.cpp
@@ -94,53 +94,61 @@ NS_IMETHODIMP CompositionTransaction::Do
   if (mReplaceLength == 0) {
     ErrorResult error;
     editorBase->DoInsertText(textNode, mOffset, mStringToInsert, error);
     if (error.Failed()) {
       NS_WARNING("EditorBase::DoInsertText() failed");
       return error.StealNSResult();
     }
     editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
-                                                   mStringToInsert);
+                                                   mStringToInsert.Length());
   } else {
+    // If composition string is split to multiple text nodes, we should put
+    // whole new composition string to the first text node and remove the
+    // compostion string in other nodes.
     uint32_t replaceableLength = textNode->TextLength() - mOffset;
     ErrorResult error;
     editorBase->DoReplaceText(textNode, mOffset, mReplaceLength,
                               mStringToInsert, error);
     if (error.Failed()) {
       NS_WARNING("EditorBase::DoReplaceText() failed");
       return error.StealNSResult();
     }
-    DebugOnly<nsresult> rvIgnored =
-        editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
-                                                       mReplaceLength);
-    NS_WARNING_ASSERTION(
-        NS_SUCCEEDED(rvIgnored),
-        "RangeUpdater::SelAdjDeleteText() failed, but ignored");
+
+    // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
+    // this transaction will remove whole composition string.  Therefore,
+    // selection should be restored at start of composition string.
+    // XXX Perhaps, this is a bug of our selection managemnt at undoing.
+    editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
+                                                   replaceableLength);
+    // But some ranges which after the composition string should be restored
+    // as-is.
     editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
-                                                   mStringToInsert);
+                                                   mStringToInsert.Length());
 
-    // If IME text node is multiple node, ReplaceData doesn't remove all IME
-    // text.  So we need remove remained text into other text node.
-    // XXX I think that this shouldn't occur.  Composition string should be
-    //     in a text node.
     if (replaceableLength < mReplaceLength) {
+      // XXX Perhaps, scanning following sibling text nodes with composition
+      //     string length which we know is wrong because there may be
+      //     non-empty text nodes which are inserted by JS.  Instead, we
+      //     should remove all text in the ranges of IME selections.
       int32_t remainLength = mReplaceLength - replaceableLength;
       IgnoredErrorResult ignoredError;
       for (nsIContent* nextSibling = textNode->GetNextSibling();
            nextSibling && nextSibling->IsText() && remainLength;
            nextSibling = nextSibling->GetNextSibling()) {
-        OwningNonNull<Text> textNode = *static_cast<Text*>(nextSibling);
-        uint32_t textLength = textNode->TextLength();
-        editorBase->DoDeleteText(textNode, 0, remainLength, ignoredError);
+        OwningNonNull<Text> followingTextNode =
+            *static_cast<Text*>(nextSibling);
+        uint32_t textLength = followingTextNode->TextLength();
+        editorBase->DoDeleteText(followingTextNode, 0, remainLength,
+                                 ignoredError);
         NS_WARNING_ASSERTION(!ignoredError.Failed(),
                              "EditorBase::DoDeleteText() failed, but ignored");
         ignoredError.SuppressException();
         // XXX Needs to check whether the text is deleted as expected.
-        editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, 0,
+        editorBase->RangeUpdaterRef().SelAdjDeleteText(followingTextNode, 0,
                                                        remainLength);
         remainLength -= textLength;
       }
     }
   }
 
   nsresult rv = SetSelectionForRanges();
   NS_WARNING_ASSERTION(
--- a/editor/libeditor/DeleteTextTransaction.cpp
+++ b/editor/libeditor/DeleteTextTransaction.cpp
@@ -115,21 +115,18 @@ NS_IMETHODIMP DeleteTextTransaction::DoT
   OwningNonNull<EditorBase> editorBase = *mEditorBase;
   OwningNonNull<Text> textNode = *mTextNode;
   editorBase->DoDeleteText(textNode, mOffset, mLengthToDelete, error);
   if (error.Failed()) {
     NS_WARNING("EditorBase::DoDeleteText() failed");
     return error.StealNSResult();
   }
 
-  DebugOnly<nsresult> rvIgnored =
-      editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
-                                                     mLengthToDelete);
-  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
-                       "RangeUpdater::SelAdjDeleteText() failed, but ignored");
+  editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
+                                                 mLengthToDelete);
 
   if (!editorBase->AllowsTransactionsToChangeSelection()) {
     return NS_OK;
   }
 
   RefPtr<Selection> selection = editorBase->GetSelection();
   if (NS_WARN_IF(!selection)) {
     return NS_ERROR_FAILURE;
--- a/editor/libeditor/EditTransactionBase.cpp
+++ b/editor/libeditor/EditTransactionBase.cpp
@@ -12,16 +12,17 @@
 #include "DeleteNodeTransaction.h"
 #include "DeleteRangeTransaction.h"
 #include "DeleteTextTransaction.h"
 #include "EditAggregateTransaction.h"
 #include "InsertNodeTransaction.h"
 #include "InsertTextTransaction.h"
 #include "JoinNodeTransaction.h"
 #include "PlaceholderTransaction.h"
+#include "ReplaceTextTransaction.h"
 #include "SplitNodeTransaction.h"
 
 #include "nsError.h"
 #include "nsISupportsBase.h"
 
 namespace mozilla {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(EditTransactionBase)
@@ -64,13 +65,14 @@ NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteNodeTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteRangeTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteTextTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(EditAggregateTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(InsertNodeTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(InsertTextTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(JoinNodeTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(PlaceholderTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(ReplaceTextTransaction)
 NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(SplitNodeTransaction)
 
 #undef NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS
 
 }  // namespace mozilla
--- a/editor/libeditor/EditTransactionBase.h
+++ b/editor/libeditor/EditTransactionBase.h
@@ -29,16 +29,17 @@ class CreateElementTransaction;
 class DeleteNodeTransaction;
 class DeleteRangeTransaction;
 class DeleteTextTransaction;
 class EditAggregateTransaction;
 class InsertNodeTransaction;
 class InsertTextTransaction;
 class JoinNodeTransaction;
 class PlaceholderTransaction;
+class ReplaceTextTransaction;
 class SplitNodeTransaction;
 
 #define NS_DECL_GETASTRANSACTION_BASE(aClass) \
   virtual aClass* GetAs##aClass();            \
   virtual const aClass* GetAs##aClass() const;
 
 /**
  * Base class for all document editing transactions.
@@ -66,16 +67,17 @@ class EditTransactionBase : public nsITr
   NS_DECL_GETASTRANSACTION_BASE(DeleteNodeTransaction)
   NS_DECL_GETASTRANSACTION_BASE(DeleteRangeTransaction)
   NS_DECL_GETASTRANSACTION_BASE(DeleteTextTransaction)
   NS_DECL_GETASTRANSACTION_BASE(EditAggregateTransaction)
   NS_DECL_GETASTRANSACTION_BASE(InsertNodeTransaction)
   NS_DECL_GETASTRANSACTION_BASE(InsertTextTransaction)
   NS_DECL_GETASTRANSACTION_BASE(JoinNodeTransaction)
   NS_DECL_GETASTRANSACTION_BASE(PlaceholderTransaction)
+  NS_DECL_GETASTRANSACTION_BASE(ReplaceTextTransaction)
   NS_DECL_GETASTRANSACTION_BASE(SplitNodeTransaction)
 
  protected:
   virtual ~EditTransactionBase() = default;
 };
 
 #undef NS_DECL_GETASTRANSACTION_BASE
 
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -2479,16 +2479,38 @@ AdjustTextInsertionRange(const EditorDOM
                          aInsertedPoint.Offset() + aInsertedString.Length()));
   }
 
   return MakeTuple(
       EditorDOMPointInText(aInsertedPoint.ContainerAsText(), 0),
       EditorDOMPointInText::AtEndOf(*aInsertedPoint.ContainerAsText()));
 }
 
+Tuple<EditorDOMPointInText, EditorDOMPointInText>
+EditorBase::ComputeInsertedRange(const EditorDOMPointInText& aInsertedPoint,
+                                 const nsAString& aInsertedString) const {
+  MOZ_ASSERT(aInsertedPoint.IsSet());
+
+  // The DOM was potentially modified during the transaction. This is possible
+  // through mutation event listeners. That is, the node could've been removed
+  // from the doc or otherwise modified.
+  if (!MaybeHasMutationEventListeners(
+          NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED)) {
+    EditorDOMPointInText endOfInsertion(
+        aInsertedPoint.ContainerAsText(),
+        aInsertedPoint.Offset() + aInsertedString.Length());
+    return MakeTuple(aInsertedPoint, endOfInsertion);
+  }
+  if (aInsertedPoint.ContainerAsText()->IsInComposedDoc()) {
+    EditorDOMPointInText begin, end;
+    return AdjustTextInsertionRange(aInsertedPoint, aInsertedString);
+  }
+  return MakeTuple(EditorDOMPointInText(), EditorDOMPointInText());
+}
+
 nsresult EditorBase::InsertTextIntoTextNodeWithTransaction(
     const nsAString& aStringToInsert,
     const EditorDOMPointInText& aPointToInsert, bool aSuppressIME) {
   MOZ_ASSERT(IsEditActionDataAvailable());
   MOZ_ASSERT(aPointToInsert.IsSetAndValid());
 
   EditorDOMPointInText pointToInsert(aPointToInsert);
   RefPtr<EditTransactionBase> transaction;
@@ -2516,30 +2538,19 @@ nsresult EditorBase::InsertTextIntoTextN
   // higher level now I believe.
   BeginUpdateViewBatch();
   nsresult rv = DoTransactionInternal(transaction);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                        "EditorBase::DoTransactionInternal() failed");
   EndUpdateViewBatch();
 
   if (AsHTMLEditor() && pointToInsert.IsSet()) {
-    // The DOM was potentially modified during the transaction. This is possible
-    // through mutation event listeners. That is, the node could've been removed
-    // from the doc or otherwise modified.
-    if (!MaybeHasMutationEventListeners(
-            NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED)) {
-      EditorDOMPointInText endOfInsertion(
-          pointToInsert.ContainerAsText(),
-          pointToInsert.Offset() + aStringToInsert.Length());
-      TopLevelEditSubActionDataRef().DidInsertText(*this, pointToInsert,
-                                                   endOfInsertion);
-    } else if (pointToInsert.ContainerAsText()->IsInComposedDoc()) {
-      EditorDOMPointInText begin, end;
-      Tie(begin, end) =
-          AdjustTextInsertionRange(pointToInsert, aStringToInsert);
+    EditorDOMPointInText begin, end;
+    Tie(begin, end) = ComputeInsertedRange(pointToInsert, aStringToInsert);
+    if (begin.IsSet() && end.IsSet()) {
       TopLevelEditSubActionDataRef().DidInsertText(*this, begin, end);
     }
   }
 
   // let listeners know what happened
   if (!mActionListeners.IsEmpty()) {
     for (auto& listener : mActionListeners.Clone()) {
       // TODO: might need adaptation because of mutation event listeners called
@@ -2713,20 +2724,17 @@ nsresult EditorBase::SetTextNodeWithoutT
   DebugOnly<nsresult> rvIgnored =
       SelectionRefPtr()->Collapse(&aTextNode, aString.Length());
   if (NS_WARN_IF(Destroyed())) {
     return NS_ERROR_EDITOR_DESTROYED;
   }
   NS_ASSERTION(NS_SUCCEEDED(rvIgnored),
                "Selection::Collapse() failed, but ignored");
 
-  rvIgnored = RangeUpdaterRef().SelAdjDeleteText(aTextNode, 0, length);
-  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
-                       "RangeUpdater::SelAdjDeleteText() failed, but ignored");
-  RangeUpdaterRef().SelAdjInsertText(aTextNode, 0, aString);
+  RangeUpdaterRef().SelAdjReplaceText(aTextNode, 0, length, aString.Length());
 
   // Let listeners know what happened
   if (!mActionListeners.IsEmpty()) {
     for (auto& listener : mActionListeners.Clone()) {
       if (length) {
         DebugOnly<nsresult> rvIgnored =
             listener->DidDeleteText(&aTextNode, 0, length, NS_OK);
         if (NS_WARN_IF(Destroyed())) {
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -82,16 +82,17 @@ class IMEContentObserver;
 class InsertNodeTransaction;
 class InsertTextTransaction;
 class JoinNodeTransaction;
 class ListElementSelectionState;
 class ListItemElementSelectionState;
 class ParagraphStateAtSelection;
 class PlaceholderTransaction;
 class PresShell;
+class ReplaceTextTransaction;
 class SplitNodeResult;
 class SplitNodeTransaction;
 class TextComposition;
 class TextEditor;
 class TextInputListener;
 class TextServicesDocument;
 class TypeInState;
 class WSRunObject;
@@ -2232,16 +2233,25 @@ class EditorBase : public nsIEditor,
    * @return                    The transaction to remove content around the
    *                            range.  Its type is DeleteNodeTransaction or
    *                            DeleteTextTransaction.
    */
   already_AddRefed<EditTransactionBase> CreateTransactionForCollapsedRange(
       const nsRange& aCollapsedRange,
       HowToHandleCollapsedRange aHowToHandleCollapsedRange);
 
+  /**
+   * ComputeInsertedRange() returns actual range modified by inserting string
+   * in a text node.  If mutation event listener changed the text data, this
+   * returns a range which covers all over the text data.
+   */
+  Tuple<EditorDOMPointInText, EditorDOMPointInText> ComputeInsertedRange(
+      const EditorDOMPointInText& aInsertedPoint,
+      const nsAString& aInsertedString) const;
+
  private:
   nsCOMPtr<nsISelectionController> mSelectionController;
   RefPtr<Document> mDocument;
 
   AutoEditActionDataSetter* mEditActionData;
 
   /**
    * SetTextDirectionTo() sets text-direction of the root element.
@@ -2521,16 +2531,17 @@ class EditorBase : public nsIEditor,
   friend class DeleteTextTransaction;
   friend class HTMLEditUtils;
   friend class InsertNodeTransaction;
   friend class InsertTextTransaction;
   friend class JoinNodeTransaction;
   friend class ListElementSelectionState;
   friend class ListItemElementSelectionState;
   friend class ParagraphStateAtSelection;
+  friend class ReplaceTextTransaction;
   friend class SplitNodeTransaction;
   friend class TypeInState;
   friend class WSRunObject;
   friend class WSRunScanner;
   friend class nsIEditor;
 };
 
 }  // namespace mozilla
--- a/editor/libeditor/HTMLEditor.cpp
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "HTMLEditor.h"
 
 #include "HTMLEditorEventListener.h"
 #include "HTMLEditUtils.h"
 #include "JoinNodeTransaction.h"
+#include "ReplaceTextTransaction.h"
 #include "SplitNodeTransaction.h"
 #include "TypeInState.h"
 #include "WSRunObject.h"
 
 #include "mozilla/ComposerCommandsUpdater.h"
 #include "mozilla/ContentIterator.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/EditAction.h"
@@ -3253,16 +3254,140 @@ nsresult HTMLEditor::DeleteTextWithTrans
 
   nsresult rv =
       EditorBase::DeleteTextWithTransaction(aTextNode, aOffset, aLength);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                        "EditorBase::DeleteTextWithTransaction() failed");
   return rv;
 }
 
+nsresult HTMLEditor::ReplaceTextWithTransaction(
+    Text& aTextNode, uint32_t aOffset, uint32_t aLength,
+    const nsAString& aStringToInsert) {
+  MOZ_ASSERT(IsEditActionDataAvailable());
+  MOZ_ASSERT(aLength > 0 || !aStringToInsert.IsEmpty());
+
+  if (aStringToInsert.IsEmpty()) {
+    nsresult rv = DeleteTextWithTransaction(aTextNode, aOffset, aLength);
+    if (NS_WARN_IF(Destroyed())) {
+      return NS_ERROR_EDITOR_DESTROYED;
+    }
+    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+                         "HTMLEditor::DeleteTextWithTransaction() failed");
+    return rv;
+  }
+
+  if (!aLength) {
+    RefPtr<Document> document = GetDocument();
+    if (NS_WARN_IF(!document)) {
+      return NS_ERROR_NOT_INITIALIZED;
+    }
+    nsresult rv = InsertTextWithTransaction(
+        *document, aStringToInsert, EditorRawDOMPoint(&aTextNode, aOffset));
+    if (NS_WARN_IF(Destroyed())) {
+      return NS_ERROR_EDITOR_DESTROYED;
+    }
+    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+                         "HTMLEditor::InsertTextWithTransaction() failed");
+    return rv;
+  }
+
+  if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // This should emulates inserting text for better undo/redo behavior.
+  IgnoredErrorResult ignoredError;
+  AutoEditSubActionNotifier startToHandleEditSubAction(
+      *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+  if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+    return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
+  }
+  NS_WARNING_ASSERTION(
+      !ignoredError.Failed(),
+      "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+  // FYI: Create the insertion point before changing the DOM tree because
+  //      the point may become invalid offset after that.
+  EditorDOMPointInText pointToInsert(&aTextNode, aOffset);
+
+  // `ReplaceTextTransaction()` removes the replaced text first, then,
+  // insert new text.  Therefore, if selection is in the text node, the
+  // range is moved to start of the range and deletion and never adjusted
+  // for the inserting text since the change occurs after the range.
+  // Therefore, we might need to save/restore selection here.
+  Maybe<AutoSelectionRestorer> restoreSelection;
+  if (!AllowsTransactionsToChangeSelection() && !ArePreservingSelection()) {
+    for (uint32_t i = 0; i < SelectionRefPtr()->RangeCount(); i++) {
+      const nsRange* range = SelectionRefPtr()->GetRangeAt(i);
+      if (!range) {
+        continue;
+      }
+      if ((range->GetStartContainer() == &aTextNode &&
+           range->StartOffset() >= aOffset) ||
+          (range->GetEndContainer() == &aTextNode &&
+           range->EndOffset() >= aOffset)) {
+        restoreSelection.emplace(*this);
+        break;
+      }
+    }
+  }
+
+  RefPtr<ReplaceTextTransaction> transaction = ReplaceTextTransaction::Create(
+      *this, aStringToInsert, aTextNode, aOffset, aLength);
+  MOZ_ASSERT(transaction);
+
+  if (aLength && !mActionListeners.IsEmpty()) {
+    for (auto& listener : mActionListeners.Clone()) {
+      DebugOnly<nsresult> rvIgnored =
+          listener->WillDeleteText(&aTextNode, aOffset, aLength);
+      NS_WARNING_ASSERTION(
+          NS_SUCCEEDED(rvIgnored),
+          "nsIEditActionListener::WillDeleteText() failed, but ignored");
+    }
+  }
+
+  nsresult rv = DoTransactionInternal(transaction);
+  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+                       "EditorBase::DoTransactionInternal() failed");
+
+  if (pointToInsert.IsSet()) {
+    EditorDOMPointInText begin, end;
+    Tie(begin, end) = ComputeInsertedRange(pointToInsert, aStringToInsert);
+    if (begin.IsSet() && end.IsSet()) {
+      TopLevelEditSubActionDataRef().DidDeleteText(*this, begin);
+      TopLevelEditSubActionDataRef().DidInsertText(*this, begin, end);
+    }
+  }
+
+  // Now, restores selection for allowing the following listeners to modify
+  // selection.
+  restoreSelection.reset();
+
+  if (!mActionListeners.IsEmpty()) {
+    for (auto& listener : mActionListeners.Clone()) {
+      // XXX The listeners are notified of DidDeleteText after inserted.
+      //     But it's used only by addons for Thunderbird, so that we should
+      //     remove this later for obviously dropping the support.
+      DebugOnly<nsresult> rvIgnored =
+          listener->DidDeleteText(&aTextNode, aOffset, aLength, rv);
+      NS_WARNING_ASSERTION(
+          NS_SUCCEEDED(rvIgnored),
+          "nsIEditActionListener::WillDeleteText() failed, but ignored");
+      rvIgnored =
+          listener->DidInsertText(&aTextNode, aOffset, aStringToInsert, rv);
+      NS_WARNING_ASSERTION(
+          NS_SUCCEEDED(rvIgnored),
+          "nsIEditActionListener::DidInsertText() failed, but ignored");
+    }
+  }
+
+  return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : rv;
+}
+
 nsresult HTMLEditor::InsertTextWithTransaction(
     Document& aDocument, const nsAString& aStringToInsert,
     const EditorRawDOMPoint& aPointToInsert,
     EditorRawDOMPoint* aPointAfterInsertedString) {
   if (NS_WARN_IF(!aPointToInsert.IsSet())) {
     return NS_ERROR_INVALID_ARG;
   }
 
--- a/editor/libeditor/HTMLEditor.h
+++ b/editor/libeditor/HTMLEditor.h
@@ -689,16 +689,24 @@ class HTMLEditor final : public TextEdit
    * @param aOffset             Start offset of removing text in aTextNode.
    * @param aLength             Length of removing text.
    */
   MOZ_CAN_RUN_SCRIPT nsresult DeleteTextWithTransaction(dom::Text& aTextNode,
                                                         uint32_t aOffset,
                                                         uint32_t aLength);
 
   /**
+   * ReplaceTextWithTransaction() replaces text in the range with
+   * aStringToInsert.
+   */
+  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ReplaceTextWithTransaction(
+      dom::Text& aTextNode, uint32_t aOffset, uint32_t aLength,
+      const nsAString& aStringToInsert);
+
+  /**
    * DeleteParentBlocksIfEmpty() removes parent block elements if they
    * don't have visible contents.  Note that due performance issue of
    * WSRunObject, this call may be expensive.  And also note that this
    * removes a empty block with a transaction.  So, please make sure that
    * you've already created `AutoPlaceholderBatch`.
    *
    * @param aPoint      The point whether this method climbing up the DOM
    *                    tree to remove empty parent blocks.
--- a/editor/libeditor/InsertTextTransaction.cpp
+++ b/editor/libeditor/InsertTextTransaction.cpp
@@ -70,17 +70,17 @@ NS_IMETHODIMP InsertTextTransaction::DoT
     NS_ASSERTION(NS_SUCCEEDED(rvIgnored),
                  "Selection::Collapse() failed, but ignored");
   } else {
     // Do nothing - DOM Range gravity will adjust selection
   }
   // XXX Other transactions do not do this but its callers do.
   //     Why do this transaction do this by itself?
   editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
-                                                 mStringToInsert);
+                                                 mStringToInsert.Length());
 
   return NS_OK;
 }
 
 NS_IMETHODIMP InsertTextTransaction::UndoTransaction() {
   if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
     return NS_ERROR_NOT_INITIALIZED;
   }
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/ReplaceTextTransaction.cpp
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ReplaceTextTransaction.h"
+
+#include "HTMLEditUtils.h"
+
+#include "mozilla/OwningNonNull.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ReplaceTextTransaction, EditTransactionBase,
+                                   mEditorBase, mTextNode)
+
+NS_IMPL_ADDREF_INHERITED(ReplaceTextTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(ReplaceTextTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReplaceTextTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP ReplaceTextTransaction::DoTransaction() {
+  if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode) ||
+      NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*mTextNode))) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  OwningNonNull<EditorBase> editorBase = *mEditorBase;
+  OwningNonNull<Text> textNode = *mTextNode;
+
+  ErrorResult error;
+  editorBase->DoReplaceText(textNode, mOffset, mStringToBeReplaced.Length(),
+                            mStringToInsert, error);
+  if (error.Failed()) {
+    NS_WARNING("EditorBase::DoReplaceText() failed");
+    return error.StealNSResult();
+  }
+  // XXX What should we do if mutation event listener changed the node?
+  editorBase->RangeUpdaterRef().SelAdjReplaceText(textNode, mOffset,
+                                                  mStringToBeReplaced.Length(),
+                                                  mStringToInsert.Length());
+
+  if (!editorBase->AllowsTransactionsToChangeSelection()) {
+    return NS_OK;
+  }
+
+  // XXX Should we stop setting selection when mutation event listener
+  //     modifies the text node?
+  RefPtr<Selection> selection = editorBase->GetSelection();
+  if (NS_WARN_IF(!selection)) {
+    return NS_ERROR_FAILURE;
+  }
+  DebugOnly<nsresult> rvIgnored =
+      selection->Collapse(textNode, mOffset + mStringToInsert.Length());
+  if (NS_WARN_IF(editorBase->Destroyed())) {
+    return NS_ERROR_EDITOR_DESTROYED;
+  }
+  NS_ASSERTION(NS_SUCCEEDED(rvIgnored),
+               "Selection::Collapse() failed, but ignored");
+  return NS_OK;
+}
+
+NS_IMETHODIMP ReplaceTextTransaction::UndoTransaction() {
+  if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode) ||
+      NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*mTextNode))) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  ErrorResult error;
+  nsAutoString insertedString;
+  mTextNode->SubstringData(mOffset, mStringToInsert.Length(), insertedString,
+                           error);
+  if (error.Failed()) {
+    NS_WARNING("CharacterData::SubstringData() failed");
+    return error.StealNSResult();
+  }
+  if (insertedString != mStringToInsert) {
+    NS_WARNING(
+        "ReplaceTextTransaction::UndoTransaction() did nothing due to "
+        "unexpected text");
+    return NS_OK;
+  }
+
+  OwningNonNull<EditorBase> editorBase = *mEditorBase;
+  OwningNonNull<Text> textNode = *mTextNode;
+
+  editorBase->DoReplaceText(textNode, mOffset, mStringToInsert.Length(),
+                            mStringToBeReplaced, error);
+  if (error.Failed()) {
+    NS_WARNING("EditorBase::DoReplaceText() failed");
+    return error.StealNSResult();
+  }
+  // XXX What should we do if mutation event listener changed the node?
+  editorBase->RangeUpdaterRef().SelAdjReplaceText(textNode, mOffset,
+                                                  mStringToInsert.Length(),
+                                                  mStringToBeReplaced.Length());
+
+  if (!editorBase->AllowsTransactionsToChangeSelection()) {
+    return NS_OK;
+  }
+
+  // XXX Should we stop setting selection when mutation event listener
+  //     modifies the text node?
+  RefPtr<Selection> selection = editorBase->GetSelection();
+  if (NS_WARN_IF(!selection)) {
+    return NS_ERROR_FAILURE;
+  }
+  DebugOnly<nsresult> rvIgnored =
+      selection->Collapse(textNode, mOffset + mStringToBeReplaced.Length());
+  if (NS_WARN_IF(editorBase->Destroyed())) {
+    return NS_ERROR_EDITOR_DESTROYED;
+  }
+  NS_ASSERTION(NS_SUCCEEDED(rvIgnored),
+               "Selection::Collapse() failed, but ignored");
+  return NS_OK;
+}
+
+NS_IMETHODIMP ReplaceTextTransaction::RedoTransaction() {
+  if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode) ||
+      NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*mTextNode))) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  ErrorResult error;
+  nsAutoString undoneString;
+  mTextNode->SubstringData(mOffset, mStringToBeReplaced.Length(), undoneString,
+                           error);
+  if (error.Failed()) {
+    NS_WARNING("CharacterData::SubstringData() failed");
+    return error.StealNSResult();
+  }
+  if (undoneString != mStringToBeReplaced) {
+    NS_WARNING(
+        "ReplaceTextTransaction::RedoTransaction() did nothing due to "
+        "unexpected text");
+    return NS_OK;
+  }
+
+  OwningNonNull<EditorBase> editorBase = *mEditorBase;
+  OwningNonNull<Text> textNode = *mTextNode;
+
+  editorBase->DoReplaceText(textNode, mOffset, mStringToBeReplaced.Length(),
+                            mStringToInsert, error);
+  if (error.Failed()) {
+    NS_WARNING("EditorBase::DoReplaceText() failed");
+    return error.StealNSResult();
+  }
+  // XXX What should we do if mutation event listener changed the node?
+  editorBase->RangeUpdaterRef().SelAdjReplaceText(textNode, mOffset,
+                                                  mStringToBeReplaced.Length(),
+                                                  mStringToInsert.Length());
+
+  if (!editorBase->AllowsTransactionsToChangeSelection()) {
+    return NS_OK;
+  }
+
+  // XXX Should we stop setting selection when mutation event listener
+  //     modifies the text node?
+  RefPtr<Selection> selection = editorBase->GetSelection();
+  if (NS_WARN_IF(!selection)) {
+    return NS_ERROR_FAILURE;
+  }
+  DebugOnly<nsresult> rvIgnored =
+      selection->Collapse(textNode, mOffset + mStringToInsert.Length());
+  if (NS_WARN_IF(editorBase->Destroyed())) {
+    return NS_ERROR_EDITOR_DESTROYED;
+  }
+  NS_ASSERTION(NS_SUCCEEDED(rvIgnored),
+               "Selection::Collapse() failed, but ignored");
+  return NS_OK;
+}
+
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/ReplaceTextTransaction.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ReplaceTextTransaction_h
+#define ReplaceTextTransaction_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/EditorDOMPoint.h"
+#include "mozilla/EditTransactionBase.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/Text.h"
+#include "nsDebug.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+class EditorBase;
+
+class ReplaceTextTransaction final : public EditTransactionBase {
+ private:
+  ReplaceTextTransaction(EditorBase& aEditorBase,
+                         const nsAString& aStringToInsert, dom::Text& aTextNode,
+                         uint32_t aStartOffset, uint32_t aLength)
+      : mEditorBase(&aEditorBase),
+        mTextNode(&aTextNode),
+        mStringToInsert(aStringToInsert),
+        mOffset(aStartOffset) {
+    if (aLength) {
+      IgnoredErrorResult ignoredError;
+      mTextNode->SubstringData(mOffset, aLength, mStringToBeReplaced,
+                               ignoredError);
+      NS_WARNING_ASSERTION(
+          !ignoredError.Failed(),
+          "Failed to initialize ReplaceTextTransaction::mStringToBeReplaced, "
+          "but ignored");
+    }
+  }
+
+  virtual ~ReplaceTextTransaction() = default;
+
+ public:
+  static already_AddRefed<ReplaceTextTransaction> Create(
+      EditorBase& aEditorBase, const nsAString& aStringToInsert,
+      dom::Text& aTextNode, uint32_t aStartOffset, uint32_t aLength) {
+    MOZ_ASSERT(aLength > 0, "Use InsertTextTransaction instead");
+    MOZ_ASSERT(!aStringToInsert.IsEmpty(), "Use DeleteTextTransaction instead");
+    MOZ_ASSERT(aTextNode.Length() >= aStartOffset);
+    MOZ_ASSERT(aTextNode.Length() >= aStartOffset + aLength);
+
+    RefPtr<ReplaceTextTransaction> transaction = new ReplaceTextTransaction(
+        aEditorBase, aStringToInsert, aTextNode, aStartOffset, aLength);
+    return transaction.forget();
+  }
+
+  ReplaceTextTransaction() = delete;
+  ReplaceTextTransaction(const ReplaceTextTransaction&) = delete;
+  ReplaceTextTransaction& operator=(const ReplaceTextTransaction&) = delete;
+
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ReplaceTextTransaction,
+                                           EditTransactionBase)
+
+  NS_DECL_EDITTRANSACTIONBASE
+  NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(ReplaceTextTransaction)
+
+  MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() final;
+
+ private:
+  RefPtr<EditorBase> mEditorBase;
+  RefPtr<dom::Text> mTextNode;
+
+  nsString mStringToInsert;
+  nsString mStringToBeReplaced;
+
+  uint32_t mOffset;
+};
+
+}  // namespace mozilla
+
+#endif  // #ifndef ReplaceTextTransaction_
--- a/editor/libeditor/SelectionState.cpp
+++ b/editor/libeditor/SelectionState.cpp
@@ -403,66 +403,79 @@ nsresult RangeUpdater::SelAdjJoinNodes(n
       // adjust end point in aLeftNode
       rangeItem->mEndContainer = &aRightNode;
     }
   }
 
   return NS_OK;
 }
 
-void RangeUpdater::SelAdjInsertText(const Text& aTextNode, int32_t aOffset,
-                                    const nsAString& aString) {
+void RangeUpdater::SelAdjReplaceText(const Text& aTextNode, int32_t aOffset,
+                                     int32_t aReplacedLength,
+                                     int32_t aInsertedLength) {
   if (mLocked) {
     // lock set by Will/DidReplaceParent, etc...
     return;
   }
 
-  uint32_t length = aString.Length();
+  // First, adjust selection for insertion because when offset is in the
+  // replaced range, it's adjusted to aOffset and never modified by the
+  // insertion if we adjust selection for deletion first.
+  SelAdjInsertText(aTextNode, aOffset, aInsertedLength);
+
+  // Then, adjust selection for deletion.
+  SelAdjDeleteText(aTextNode, aOffset, aReplacedLength);
+}
+
+void RangeUpdater::SelAdjInsertText(const Text& aTextNode, int32_t aOffset,
+                                    int32_t aInsertedLength) {
+  if (mLocked) {
+    // lock set by Will/DidReplaceParent, etc...
+    return;
+  }
+
   for (RefPtr<RangeItem>& rangeItem : mArray) {
     MOZ_ASSERT(rangeItem);
 
     if (rangeItem->mStartContainer == &aTextNode &&
         rangeItem->mStartOffset > aOffset) {
-      rangeItem->mStartOffset += length;
+      rangeItem->mStartOffset += aInsertedLength;
     }
     if (rangeItem->mEndContainer == &aTextNode &&
         rangeItem->mEndOffset > aOffset) {
-      rangeItem->mEndOffset += length;
+      rangeItem->mEndOffset += aInsertedLength;
     }
   }
 }
 
-nsresult RangeUpdater::SelAdjDeleteText(const Text& aTextNode, int32_t aOffset,
-                                        int32_t aLength) {
+void RangeUpdater::SelAdjDeleteText(const Text& aTextNode, int32_t aOffset,
+                                    int32_t aDeletedLength) {
   if (mLocked) {
     // lock set by Will/DidReplaceParent, etc...
-    return NS_OK;
+    return;
   }
 
   for (RefPtr<RangeItem>& rangeItem : mArray) {
-    if (NS_WARN_IF(!rangeItem)) {
-      return NS_ERROR_FAILURE;
-    }
+    MOZ_ASSERT(rangeItem);
 
     if (rangeItem->mStartContainer == &aTextNode &&
         rangeItem->mStartOffset > aOffset) {
-      rangeItem->mStartOffset -= aLength;
+      rangeItem->mStartOffset -= aDeletedLength;
       if (rangeItem->mStartOffset < 0) {
         rangeItem->mStartOffset = 0;
       }
     }
     if (rangeItem->mEndContainer == &aTextNode &&
         rangeItem->mEndOffset > aOffset) {
-      rangeItem->mEndOffset -= aLength;
+      rangeItem->mEndOffset -= aDeletedLength;
       if (rangeItem->mEndOffset < 0) {
         rangeItem->mEndOffset = 0;
       }
     }
   }
-  return NS_OK;
 }
 
 void RangeUpdater::DidReplaceContainer(const Element& aRemovedElement,
                                        Element& aInsertedElement) {
   if (NS_WARN_IF(!mLocked)) {
     return;
   }
   mLocked = false;
--- a/editor/libeditor/SelectionState.h
+++ b/editor/libeditor/SelectionState.h
@@ -138,19 +138,21 @@ class MOZ_STACK_CLASS RangeUpdater final
   template <typename PT, typename CT>
   nsresult SelAdjInsertNode(const EditorDOMPointBase<PT, CT>& aPoint);
   void SelAdjDeleteNode(nsINode& aNode);
   nsresult SelAdjSplitNode(nsIContent& aRightNode, nsIContent& aNewLeftNode);
   nsresult SelAdjJoinNodes(nsINode& aLeftNode, nsINode& aRightNode,
                            nsINode& aParent, int32_t aOffset,
                            int32_t aOldLeftNodeLength);
   void SelAdjInsertText(const dom::Text& aTextNode, int32_t aOffset,
-                        const nsAString& aString);
-  nsresult SelAdjDeleteText(const dom::Text& aTextNode, int32_t aOffset,
-                            int32_t aLength);
+                        int32_t aInsertedLength);
+  void SelAdjDeleteText(const dom::Text& aTextNode, int32_t aOffset,
+                        int32_t aDeletedLength);
+  void SelAdjReplaceText(const dom::Text& aTextNode, int32_t aOffset,
+                         int32_t aReplacedLength, int32_t aInsertedLength);
   // the following gravity routines need will/did sandwiches, because the other
   // gravity routines will be called inside of these sandwiches, but should be
   // ignored.
   void WillReplaceContainer() {
     // XXX Isn't this possible with mutation event listener?
     NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
     mLocked = true;
   }
--- a/editor/libeditor/moz.build
+++ b/editor/libeditor/moz.build
@@ -61,16 +61,17 @@ UNIFIED_SOURCES += [
     'HTMLInlineTableEditor.cpp',
     'HTMLStyleEditor.cpp',
     'HTMLTableEditor.cpp',
     'InsertNodeTransaction.cpp',
     'InsertTextTransaction.cpp',
     'InternetCiter.cpp',
     'JoinNodeTransaction.cpp',
     'PlaceholderTransaction.cpp',
+    'ReplaceTextTransaction.cpp',
     'SelectionState.cpp',
     'SplitNodeTransaction.cpp',
     'TextEditor.cpp',
     'TextEditorDataTransfer.cpp',
     'TextEditSubActionHandler.cpp',
     'TypeInState.cpp',
     'WSRunObject.cpp',
 ]
--- a/editor/libeditor/tests/test_bug1310912.html
+++ b/editor/libeditor/tests/test_bug1310912.html
@@ -11,83 +11,111 @@ https://bugzilla.mozilla.org/show_bug.cg
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1310912">Mozilla Bug 1310912</a>
 <p id="display"></p>
 <div id="content" style="display: none;">
 
 </div>
 
-<div id="editable1" contenteditable="true">ABC</div>
+<div contenteditable>ABC</div>
 <pre id="test">
 
 <script class="testbody" type="application/javascript">
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
-  let elm = document.getElementById("editable1");
+  let editor = document.querySelector("div[contenteditable]");
 
-  elm.focus();
+  editor.focus();
   let sel = window.getSelection();
-  sel.collapse(elm.childNodes[0], elm.textContent.length);
+  sel.collapse(editor.childNodes[0], editor.textContent.length);
 
   synthesizeCompositionChange({
     composition: {
       string: "DEF",
       clauses: [
         { length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE },
       ],
     },
     caret: { start: 3, length: 0 },
   });
-  ok(elm.textContent == "ABCDEF", "composing text should be set");
+  is(editor.textContent, "ABCDEF", "composing text should be set");
 
   window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
   synthesizeCompositionChange({
     composition: {
       string: "GHI",
       clauses: [
         { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
       ],
     },
     caret: { start: 0, length: 0 },
   });
-  ok(elm.textContent == "ABCGHI", "composing text should be replaced");
+  is(editor.textContent, "ABCGHI", "composing text should be replaced");
 
   window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
   synthesizeCompositionChange({
     composition: {
       string: "JKL",
       clauses: [
         { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
       ],
     },
     caret: { start: 0, length: 0 },
   });
-  ok(elm.textContent == "ABCJKL", "composing text should be replaced");
+  is(editor.textContent, "ABCJKL", "composing text should be replaced");
 
   window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
   synthesizeCompositionChange({
     composition: {
       string: "MNO",
       clauses: [
         { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
       ],
     },
     caret: { start: 1, length: 0 },
   });
-  ok(elm.textContent == "ABCMNO", "composing text should be replaced");
+  is(editor.textContent, "ABCMNO", "composing text should be replaced");
 
+  // Normal selection is the caret, therefore, inserting empty text node
+  // creates the following DOM tree:
+  // <div contenteditable>
+  //  |- #text ("ABCM")
+  //  |- #text ("")
+  //  +- #text ("NO")
   window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+  is(editor.childNodes[0].data, "ABCM",
+     "First text node should only have \"M\" of the composition string");
+  is(editor.childNodes[1].data, "",
+     "Second text node should be the inserted empty text node");
+  is(editor.childNodes[2].data, "NO",
+     "Third text node should have the remaining composition string");
+  todo_is(editor.childNodes[3].nodeName, "BR",
+     "Forth node is empty text node, but I don't where this comes from");
+
+  // Then, committing composition makes the commit string into the first
+  // text node and makes the following text nodes empty.
+  // XXX I don't know whether the empty text nodes should be removed or not
+  //     at this moment.
   synthesizeComposition({ type: "compositioncommitasis" });
-  ok(elm.textContent == "ABCMNO", "composing text should be committed");
+  is(editor.textContent, "ABCMNO",
+     "composing text should be committed");
+  is(editor.childNodes[0].data, "ABCMNO",
+     "First text node should have the committed string");
 
   synthesizeKey("Z", { accelKey: true });
-  ok(elm.textContent == "ABC", "text should be undoed");
+  is(editor.textContent, "ABC",
+     "text should be undone (commit string should've gone");
+  is(editor.childNodes[0].data, "ABC",
+     "First text node should have the committed string after undone");
 
   synthesizeKey("Z", { accelKey: true, shiftKey: true });
-  ok(elm.textContent == "ABCMNO", "text should be redoed");
+  is(editor.textContent, "ABCMNO",
+     "text should be redone (commit string should've be back");
+  is(editor.childNodes[0].data, "ABCMNO",
+     "First text node should have the committed string after redone");
 
   SimpleTest.finish();
 });
 </script>
 </pre>
 </body>
 </html>