From d3fe79f0e9cd07d3377fedd47fa01dded2b97389 Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Sun, 27 Sep 2020 04:49:41 +0000 Subject: [PATCH] Bug 1658702 - part 21 Initialize target ranges for all edit actions which runs `DeleteSelectionAsSubAction()` r=m_kato A lot of edit actions calls `DeleteSelectionAsSubAction()` if selection is not collapsed. In such case, `getTargetRanges()` should return same result as when the selection range is simply deleted. This patch creates 2 methods to consider whether `EditAction` causes running `DeleteSelectionAsSubAction()` with collapsed selection or non-collapsed selection. And makes `DeleteSelectionAsAction()` stop initializing the target ranges itself. Instead, makes `AutoEditActionDataSetter` do it immediately before dispatching `beforeinput` event unless it's been already initialized manually. * https://searchfox.org/mozilla-central/rev/30e70f2fe80c97bfbfcd975e68538cefd7f58b2a/editor/libeditor/TextEditor.cpp#492 * https://searchfox.org/mozilla-central/rev/30e70f2fe80c97bfbfcd975e68538cefd7f58b2a/editor/libeditor/TextEditor.cpp#731 * https://searchfox.org/mozilla-central/rev/30e70f2fe80c97bfbfcd975e68538cefd7f58b2a/editor/libeditor/TextEditorDataTransfer.cpp#503 The correctness of the new utility methods are tested with new `MOZ_ASSERT` in `DeleteSelectionAsSubAction()`. Additionally, this reorganizes `input-events-get-target-ranges-*.html`. * Moving common code into `input-events-get-target-ranges.js` * Moving non-collapsed selection cases into `input-events-get-target-ranges-non-collapsed-selection.html` * Adding "typing a" case into the new test for testing this patch's behavior Depends on D90542 Differential Revision: https://phabricator.services.mozilla.com/D90639 --- editor/libeditor/EditAction.h | 184 ++ editor/libeditor/EditorBase.cpp | 154 +- editor/libeditor/EditorBase.h | 17 +- ...target-ranges-backspace.tentative.html.ini | 10 +- ...et-ranges-forwarddelete.tentative.html.ini | 10 +- ...non-collapsed-selection.tentative.html.ini | 32 + ...get-target-ranges-backspace.tentative.html | 1478 ++++------------ ...target-ranges-forwarddelete.tentative.html | 1549 ++++------------- ...ges-non-collapsed-selection.tentative.html | 566 ++++++ .../input-events-get-target-ranges.js | 223 +++ 10 files changed, 1844 insertions(+), 2379 deletions(-) create mode 100644 testing/web-platform/meta/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html.ini create mode 100644 testing/web-platform/tests/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html create mode 100644 testing/web-platform/tests/input-events/input-events-get-target-ranges.js diff --git a/editor/libeditor/EditAction.h b/editor/libeditor/EditAction.h index 2d414209dfbe..de49545450a3 100644 --- a/editor/libeditor/EditAction.h +++ b/editor/libeditor/EditAction.h @@ -593,6 +593,190 @@ inline EditorInputType ToInputType(EditAction aEditAction) { } } +inline bool MayEditActionDeleteAroundCollapsedSelection( + const EditAction aEditAction) { + switch (aEditAction) { + case EditAction::eDeleteSelection: + case EditAction::eDeleteBackward: + case EditAction::eDeleteForward: + case EditAction::eDeleteWordBackward: + case EditAction::eDeleteWordForward: + case EditAction::eDeleteToBeginningOfSoftLine: + case EditAction::eDeleteToEndOfSoftLine: + return true; + default: + return false; + } +} + +inline bool IsEditActionTableEditing(const EditAction aEditAction) { + switch (aEditAction) { + case EditAction::eInsertTableRowElement: + case EditAction::eRemoveTableRowElement: + case EditAction::eInsertTableColumn: + case EditAction::eRemoveTableColumn: + case EditAction::eRemoveTableElement: + case EditAction::eRemoveTableCellElement: + case EditAction::eDeleteTableCellContents: + case EditAction::eInsertTableCellElement: + case EditAction::eJoinTableCellElements: + case EditAction::eSplitTableCellElement: + case EditAction::eSetTableCellElementType: + return true; + default: + return false; + } +} + +inline bool MayEditActionDeleteSelection(const EditAction aEditAction) { + switch (aEditAction) { + case EditAction::eNone: + case EditAction::eNotEditing: + return false; + + // EditActions modifying around selection. + case EditAction::eInsertText: + case EditAction::eInsertParagraphSeparator: + case EditAction::eInsertLineBreak: + case EditAction::eDeleteSelection: + case EditAction::eDeleteBackward: + case EditAction::eDeleteForward: + case EditAction::eDeleteWordBackward: + case EditAction::eDeleteWordForward: + case EditAction::eDeleteToBeginningOfSoftLine: + case EditAction::eDeleteToEndOfSoftLine: + case EditAction::eDeleteByDrag: + return true; + + case EditAction::eStartComposition: + return false; + + case EditAction::eUpdateComposition: + case EditAction::eCommitComposition: + case EditAction::eCancelComposition: + case EditAction::eDeleteByComposition: + return true; + + case EditAction::eUndo: + case EditAction::eRedo: + case EditAction::eSetTextDirection: + return false; + + case EditAction::eCut: + return true; + + case EditAction::eCopy: + return false; + + case EditAction::ePaste: + case EditAction::ePasteAsQuotation: + return true; + + case EditAction::eDrop: + return false; // Not deleting selection at drop. + + // EditActions changing format around selection. + case EditAction::eIndent: + case EditAction::eOutdent: + return false; + + // EditActions inserting or deleting something at specified position. + case EditAction::eInsertTableRowElement: + case EditAction::eRemoveTableRowElement: + case EditAction::eInsertTableColumn: + case EditAction::eRemoveTableColumn: + case EditAction::eResizingElement: + case EditAction::eResizeElement: + case EditAction::eMovingElement: + case EditAction::eMoveElement: + case EditAction::eUnknown: + case EditAction::eSetAttribute: + case EditAction::eRemoveAttribute: + case EditAction::eRemoveNode: + case EditAction::eInsertBlockElement: + return false; + + // EditActions inserting someting around selection or replacing selection + // with something. + case EditAction::eReplaceText: + case EditAction::eInsertNode: + case EditAction::eInsertHorizontalRuleElement: + return true; + + // EditActions chaning format around selection or inserting or deleting + // something at specific position. + case EditAction::eInsertLinkElement: + case EditAction::eInsertUnorderedListElement: + case EditAction::eInsertOrderedListElement: + case EditAction::eRemoveUnorderedListElement: + case EditAction::eRemoveOrderedListElement: + case EditAction::eRemoveListElement: + case EditAction::eInsertBlockquoteElement: + case EditAction::eNormalizeTable: + case EditAction::eRemoveTableElement: + case EditAction::eRemoveTableCellElement: + case EditAction::eDeleteTableCellContents: + case EditAction::eInsertTableCellElement: + case EditAction::eJoinTableCellElements: + case EditAction::eSplitTableCellElement: + case EditAction::eSetTableCellElementType: + case EditAction::eSetInlineStyleProperty: + case EditAction::eRemoveInlineStyleProperty: + case EditAction::eSetFontWeightProperty: + case EditAction::eRemoveFontWeightProperty: + case EditAction::eSetTextStyleProperty: + case EditAction::eRemoveTextStyleProperty: + case EditAction::eSetTextDecorationPropertyUnderline: + case EditAction::eRemoveTextDecorationPropertyUnderline: + case EditAction::eSetTextDecorationPropertyLineThrough: + case EditAction::eRemoveTextDecorationPropertyLineThrough: + case EditAction::eSetVerticalAlignPropertySuper: + case EditAction::eRemoveVerticalAlignPropertySuper: + case EditAction::eSetVerticalAlignPropertySub: + case EditAction::eRemoveVerticalAlignPropertySub: + case EditAction::eSetFontFamilyProperty: + case EditAction::eRemoveFontFamilyProperty: + case EditAction::eSetColorProperty: + case EditAction::eRemoveColorProperty: + case EditAction::eSetBackgroundColorPropertyInline: + case EditAction::eRemoveBackgroundColorPropertyInline: + case EditAction::eRemoveAllInlineStyleProperties: + case EditAction::eIncrementFontSize: + case EditAction::eDecrementFontSize: + case EditAction::eSetAlignment: + case EditAction::eAlignLeft: + case EditAction::eAlignRight: + case EditAction::eAlignCenter: + case EditAction::eJustify: + case EditAction::eSetBackgroundColor: + case EditAction::eSetPositionToAbsoluteOrStatic: + case EditAction::eIncreaseOrDecreaseZIndex: + return false; + + // EditActions controlling editor feature or state. + case EditAction::eEnableOrDisableCSS: + case EditAction::eEnableOrDisableAbsolutePositionEditor: + case EditAction::eEnableOrDisableResizer: + case EditAction::eEnableOrDisableInlineTableEditingUI: + case EditAction::eSetCharacterSet: + case EditAction::eSetWrapWidth: + case EditAction::eRewrap: + return false; + + case EditAction::eSetText: + case EditAction::eSetHTML: + return true; + + case EditAction::eInsertHTML: + return true; + + case EditAction::eHidePassword: + case EditAction::eCreatePaddingBRElementForEmptyEditor: + return false; + } + return false; +} + } // namespace mozilla inline bool operator!(const mozilla::EditSubAction& aEditSubAction) { diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp index f6a1b30629e6..8eadc1a3a48f 100644 --- a/editor/libeditor/EditorBase.cpp +++ b/editor/libeditor/EditorBase.cpp @@ -3581,6 +3581,29 @@ EditorBase::CreateTransactionForCollapsedRange( return deleteNodeTransaction.forget(); } +bool EditorBase::FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + nsIEditor::EDirection aDirectionAndAmount) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(Destroyed())) { + return false; + } + if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection( + aDirectionAndAmount, *SelectionRefPtr())) { + return true; + } + // Although AutoRangeArray::ExtendAnchorFocusRangeFor() will use + // nsFrameSelection, if it still has dirty frame, nsFrameSelection doesn't + // extend selection since we block script. + if (RefPtr presShell = GetPresShell()) { + presShell->FlushPendingNotifications(FlushType::Layout); + if (NS_WARN_IF(Destroyed())) { + return false; + } + } + return true; +} + nsresult EditorBase::DeleteSelectionAsAction( nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, nsIPrincipal* aPrincipal) { @@ -3682,47 +3705,15 @@ nsresult EditorBase::DeleteSelectionAsAction( } } - if (EditorUtils::IsFrameSelectionRequiredToExtendSelection( - aDirectionAndAmount, *SelectionRefPtr())) { - // Although AutoRangeArray::ExtendAnchorFocusRangeFor() will use - // nsFrameSelection, if it still has dirty frame, nsFrameSelection doesn't - // extend selection since we block script. - if (RefPtr presShell = GetPresShell()) { - presShell->FlushPendingNotifications(FlushType::Layout); - if (NS_WARN_IF(Destroyed())) { - editActionData.Abort(); - return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); - } - } + if (!FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + aDirectionAndAmount)) { + NS_WARNING("Flusing pending notifications caused destroying the editor"); + editActionData.Abort(); + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } - // TODO: This should be done when selection is not collapsed and the edit - // action requires to delete the range first. - if (IsHTMLEditor() && editActionData.NeedsToDispatchBeforeInputEvent()) { - AutoRangeArray rangesToDelete(*SelectionRefPtr()); - if (!rangesToDelete.Ranges().IsEmpty()) { - nsresult rv = - MOZ_KnownLive(AsHTMLEditor()) - ->ComputeTargetRanges(aDirectionAndAmount, rangesToDelete); - if (rv == NS_ERROR_EDITOR_DESTROYED) { - NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor"); - return NS_ERROR_EDITOR_DESTROYED; - } - NS_WARNING_ASSERTION( - NS_SUCCEEDED(rv), - "HTMLEditor::ComputeTargetRanges() failed, but ignored"); - for (auto& range : rangesToDelete.Ranges()) { - RefPtr staticRange = - StaticRange::Create(range, IgnoreErrors()); - if (NS_WARN_IF(!staticRange)) { - continue; - } - editActionData.AppendTargetRange(*staticRange); - } - } - } - - nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); + nsresult rv = + editActionData.MaybeDispatchBeforeInputEvent(aDirectionAndAmount); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent() failed"); @@ -3742,6 +3733,11 @@ nsresult EditorBase::DeleteSelectionAsSubAction( nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers) { MOZ_ASSERT(IsEditActionDataAvailable()); + // If handling edit action is for table editing, this may be called with + // selecting an any table element by the caller, but it's not usual work of + // this so that `MayEditActionDeleteSelection()` returns false. + MOZ_ASSERT(MayEditActionDeleteSelection(GetEditAction()) || + IsEditActionTableEditing(GetEditAction())); MOZ_ASSERT(mPlaceholderBatch); MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); NS_ASSERTION(IsHTMLEditor() || aStripWrappers == nsIEditor::eNoStrip, @@ -5307,11 +5303,14 @@ bool EditorBase::AutoEditActionDataSetter::IsBeforeInputEventEnabled() const { return true; } -nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent() { +nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent( + nsIEditor::EDirection aDeleteDirectionAndAmount /* nsIEditor::eNone */) { MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), "We've already handled beforeinput event"); MOZ_ASSERT(CanHandle()); MOZ_ASSERT(!IsBeforeInputEventEnabled() || NeedsToDispatchBeforeInputEvent()); + MOZ_ASSERT_IF(!MayEditActionDeleteAroundCollapsedSelection(mEditAction), + aDeleteDirectionAndAmount == nsIEditor::eNone); mHasTriedToDispatchBeforeInputEvent = true; @@ -5333,28 +5332,63 @@ nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent() { } OwningNonNull textEditor = *mEditorBase.AsTextEditor(); EditorInputType inputType = ToInputType(mEditAction); - // If mTargetRanges has not been initialized yet, it means that we may need - // to set it to selection ranges. - if (textEditor->AsHTMLEditor() && mTargetRanges.IsEmpty() && - MayHaveTargetRangesOnHTMLEditor(inputType)) { - if (uint32_t rangeCount = textEditor->SelectionRefPtr()->RangeCount()) { - mTargetRanges.SetCapacity(rangeCount); - for (uint32_t i = 0; i < rangeCount; i++) { - const nsRange* range = textEditor->SelectionRefPtr()->GetRangeAt(i); - if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) { - continue; + if (textEditor->IsHTMLEditor() && mTargetRanges.IsEmpty()) { + // If the edit action will delete selected ranges, compute the range + // strictly. + if (MayEditActionDeleteAroundCollapsedSelection(mEditAction) || + (!textEditor->SelectionRefPtr()->IsCollapsed() && + MayEditActionDeleteSelection(mEditAction))) { + if (!textEditor + ->FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + aDeleteDirectionAndAmount)) { + NS_WARNING( + "Flusing pending notifications caused destroying the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + + AutoRangeArray rangesToDelete(*textEditor->SelectionRefPtr()); + if (!rangesToDelete.Ranges().IsEmpty()) { + nsresult rv = MOZ_KnownLive(textEditor->AsHTMLEditor()) + ->ComputeTargetRanges(aDeleteDirectionAndAmount, + rangesToDelete); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor"); + return NS_ERROR_EDITOR_DESTROYED; } - // Now, we need to fix the offset of target range because it may - // be referred after modifying the DOM tree and range boundaries - // of `range` may have not computed offset yet. - RefPtr targetRange = StaticRange::Create( - range->GetStartContainer(), range->StartOffset(), - range->GetEndContainer(), range->EndOffset(), IgnoreErrors()); - if (NS_WARN_IF(!targetRange) || - NS_WARN_IF(!targetRange->IsPositioned())) { - continue; + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::ComputeTargetRanges() failed, but ignored"); + for (auto& range : rangesToDelete.Ranges()) { + RefPtr staticRange = + StaticRange::Create(range, IgnoreErrors()); + if (NS_WARN_IF(!staticRange)) { + continue; + } + AppendTargetRange(*staticRange); + } + } + } + // Otherwise, just set target ranges to selection ranges. + else if (MayHaveTargetRangesOnHTMLEditor(inputType)) { + if (uint32_t rangeCount = textEditor->SelectionRefPtr()->RangeCount()) { + mTargetRanges.SetCapacity(rangeCount); + for (uint32_t i = 0; i < rangeCount; i++) { + const nsRange* range = textEditor->SelectionRefPtr()->GetRangeAt(i); + if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) { + continue; + } + // Now, we need to fix the offset of target range because it may + // be referred after modifying the DOM tree and range boundaries + // of `range` may have not computed offset yet. + RefPtr targetRange = StaticRange::Create( + range->GetStartContainer(), range->StartOffset(), + range->GetEndContainer(), range->EndOffset(), IgnoreErrors()); + if (NS_WARN_IF(!targetRange) || + NS_WARN_IF(!targetRange->IsPositioned())) { + continue; + } + mTargetRanges.AppendElement(std::move(targetRange)); } - mTargetRanges.AppendElement(std::move(targetRange)); } } } diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h index 447154110a44..44632be3144c 100644 --- a/editor/libeditor/EditorBase.h +++ b/editor/libeditor/EditorBase.h @@ -861,11 +861,16 @@ class EditorBase : public nsIEditor, * dispatch "beforeinput" event or not. Then, * mHasTriedToDispatchBeforeInputEvent is set to true. * + * @param aDeleteDirectionAndAmount + * If `MayEditActionDeleteAroundCollapsedSelection( + * mEditAction)` returns true, this must be set. + * Otherwise, don't set explicitly. * @return If this method actually dispatches "beforeinput" event * and it's canceled, returns * NS_ERROR_EDITOR_ACTION_CANCELED. */ - [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent(); + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent( + nsIEditor::EDirection aDeleteDirectionAndAmount = nsIEditor::eNone); /** * MarkAsBeforeInputHasBeenDispatched() should be called only when updating @@ -1879,6 +1884,16 @@ class EditorBase : public nsIEditor, */ void UndefineCaretBidiLevel() const; + /** + * Flushing pending notifications if nsFrameSelection requires the latest + * layout information to compute deletion range. This may destroy the + * editor instance itself. When this returns false, don't keep doing + * anything. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool + FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + nsIEditor::EDirection aDirectionAndAmount) const; + /** * DeleteSelectionAsSubAction() removes selection content or content around * caret with transactions. This should be used for handling it as an diff --git a/testing/web-platform/meta/input-events/input-events-get-target-ranges-backspace.tentative.html.ini b/testing/web-platform/meta/input-events/input-events-get-target-ranges-backspace.tentative.html.ini index 28399f44720b..d2c7069b12f9 100644 --- a/testing/web-platform/meta/input-events/input-events-get-target-ranges-backspace.tentative.html.ini +++ b/testing/web-platform/meta/input-events/input-events-get-target-ranges-backspace.tentative.html.ini @@ -1,6 +1,6 @@ [input-events-get-target-ranges-backspace.tentative.html] - max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly. - min-asserts: 0 # An assertion in the constructor of TextFragmentData hits twice or not. + max-asserts: 2 # An assertion in the constructor of TextFragmentData + min-asserts: 0 # But sometimes not counted correctly prefs: [editor.hr_element.allow_to_delete_from_following_line:true] [Alt + Backspace at "

abc def[\] ghi

"] expected: @@ -40,12 +40,6 @@ [Backspace at "
abc
  • [\] def
ghi
"] expected: FAIL - [Backspace at "
abc [
  • \] def
ghi
"] - expected: FAIL - - [Backspace at "

abc[

}

"] - expected: FAIL - [Backspace at "

abcdef[\]ghi

"] expected: FAIL diff --git a/testing/web-platform/meta/input-events/input-events-get-target-ranges-forwarddelete.tentative.html.ini b/testing/web-platform/meta/input-events/input-events-get-target-ranges-forwarddelete.tentative.html.ini index 4b6b46cc6136..68eee6b66fd8 100644 --- a/testing/web-platform/meta/input-events/input-events-get-target-ranges-forwarddelete.tentative.html.ini +++ b/testing/web-platform/meta/input-events/input-events-get-target-ranges-forwarddelete.tentative.html.ini @@ -1,6 +1,6 @@ [input-events-get-target-ranges-forwarddelete.tentative.html] - max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly. - min-asserts: 0 # An assertion in the constructor of TextFragmentData hits twice or not. + max-asserts: 5 # An assertion in the constructor of TextFragmentData + min-asserts: 0 # But sometimes not counted correctly [Alt + Delete at "

abc [\]def ghi

"] expected: if (os == "android"): FAIL @@ -69,14 +69,8 @@ [Delete at "
abc [\]
  • def
ghi
"] expected: FAIL - [Delete at "
abc [
  • \] def
ghi
"] - expected: FAIL - [Delete at "
abc
  • def[\]
ghi
"] expected: FAIL - [Delete at "

{

\]abc

"] - expected: FAIL - [Delete at "

abc[\]defghi

"] expected: FAIL diff --git a/testing/web-platform/meta/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html.ini b/testing/web-platform/meta/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html.ini new file mode 100644 index 000000000000..897d1cf33fc7 --- /dev/null +++ b/testing/web-platform/meta/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html.ini @@ -0,0 +1,32 @@ +[input-events-get-target-ranges-non-collapsed-selection.tentative.html?Backspace] + max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly. + min-asserts: 0 + prefs: [editor.hr_element.allow_to_delete_from_following_line:true] + [Backspace at "
abc [
  • \] def
ghi
"] + expected: FAIL + + [Backspace at "

abc[

}

"] + expected: FAIL + + +[input-events-get-target-ranges-non-collapsed-selection.tentative.html?Delete] + max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly. + min-asserts: 0 + [Delete at "

abc[

}

"] + expected: FAIL + + [Delete at "
abc [
  • \] def
ghi
"] + expected: FAIL + + [Delete at "

{

\]abc

"] + expected: FAIL + + +[input-events-get-target-ranges-non-collapsed-selection.tentative.html?TypingA] + max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly. + min-asserts: 0 + [TypingA at "
abc [
  • \] def
ghi
"] + expected: FAIL + + [TypingA at "

abc[

}

"] + expected: FAIL \ No newline at end of file diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html index 50d5cb96e84a..a67bc76364d9 100644 --- a/testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html +++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html @@ -2,177 +2,25 @@ InputEvent.getTargetRanges() at Backspace +
+ -
-
+ + + + + + diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges.js b/testing/web-platform/tests/input-events/input-events-get-target-ranges.js new file mode 100644 index 000000000000..4d09c4da521f --- /dev/null +++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges.js @@ -0,0 +1,223 @@ +"use strict"; + +const kBackspaceKey = "\uE003"; +const kDeleteKey = "\uE017"; +const kArrowRight = "\uE014"; +const kArrowLeft = "\uE012"; +const kShift = "\uE008"; +const kMeta = "\uE03d"; +const kControl = "\uE009"; +const kAlt = "\uE00A"; +const kKeyA = "a"; + +const kImgSrc = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg=="; + +let gSelection = getSelection(); +let gEditor = document.querySelector("div[contenteditable]"); +let gBeforeinput = []; +let gInput = []; +gEditor.addEventListener("beforeinput", e => { + // NOTE: Blink makes `getTargetRanges()` return empty range after propagation, + // but this test wants to check the result during propagation. + // Therefore, we need to cache the result, but will assert if + // `getTargetRanges()` returns different ranges after checking the + // cached ranges. + e.cachedRanges = e.getTargetRanges(); + gBeforeinput.push(e); +}); +gEditor.addEventListener("input", e => { + e.cachedRanges = e.getTargetRanges(); + gInput.push(e); +}); + +function initializeTest(aInnerHTML) { + gEditor.innerHTML = aInnerHTML; + gEditor.focus(); + gBeforeinput = []; + gInput = []; +} + +function getRangeDescription(range) { + function getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + case Node.COMMENT_NODE: + case Node.CDATA_SECTION_NODE: + return `${node.nodeName} "${node.data}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}>`; + default: + return `${node.nodeName}`; + } + } + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; +} + +function getArrayOfRangesDescription(arrayOfRanges) { + if (arrayOfRanges === null) { + return "null"; + } + if (arrayOfRanges === undefined) { + return "undefined"; + } + if (!Array.isArray(arrayOfRanges)) { + return "Unknown Object"; + } + if (arrayOfRanges.length === 0) { + return "[]"; + } + let result = "["; + for (let range of arrayOfRanges) { + result += `{${getRangeDescription(range)}},`; + } + result += "]"; + return result; +} + +function sendDeleteKey(modifier) { + if (!modifier) { + return new test_driver.Actions() + .keyDown(kDeleteKey) + .keyUp(kDeleteKey) + .send(); + } + return new test_driver.Actions() + .keyDown(modifier) + .keyDown(kDeleteKey) + .keyUp(kDeleteKey) + .keyUp(modifier) + .send(); +} + +function sendBackspaceKey(modifier) { + if (!modifier) { + return new test_driver.Actions() + .keyDown(kBackspaceKey) + .keyUp(kBackspaceKey) + .send(); + } + return new test_driver.Actions() + .keyDown(modifier) + .keyDown(kBackspaceKey) + .keyUp(kBackspaceKey) + .keyUp(modifier) + .send(); +} + +function sendKeyA() { + return new test_driver.Actions() + .keyDown(kKeyA) + .keyUp(kKeyA) + .send(); +} + +function sendArrowLeftKey() { + return new test_driver.Actions() + .keyDown(kArrowLeft) + .keyUp(kArrowLeft) + .send(); +} + +function sendArrowRightKey() { + return new test_driver.Actions() + .keyDown(kArrowRight) + .keyUp(kArrowRight) + .send(); +} + +function checkGetTargetRangesKeepReturningSameValue(event) { + // https://github.com/w3c/input-events/issues/114 + assert_equals( + getArrayOfRangesDescription(event.getTargetRanges()), + getArrayOfRangesDescription(event.cachedRanges), + `${event.type}.getTargetRanges() should keep returning the same array of ranges even after its propagation finished` + ); +} + +function checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRange) { + assert_equals( + gBeforeinput.length, + 1, + "One beforeinput event should be fired if the key operation deletes something" + ); + assert_true( + Array.isArray(gBeforeinput[0].cachedRanges), + "gBeforeinput[0].getTargetRanges() should return an array of StaticRange instances during propagation" + ); + // Before checking the length of array of ranges, we should check the first + // range first because the first range data is more important than whether + // there are additional unexpected ranges. + if (gBeforeinput[0].cachedRanges.length > 0) { + assert_equals( + getRangeDescription(gBeforeinput[0].cachedRanges[0]), + getRangeDescription(expectedRange), + `gBeforeinput[0].getTargetRanges() should return expected range (inputType is "${gBeforeinput[0].inputType}")` + ); + assert_equals( + gBeforeinput[0].cachedRanges.length, + 1, + "gBeforeinput[0].getTargetRanges() should return one range within an array" + ); + } + assert_equals( + gBeforeinput[0].cachedRanges.length, + 1, + "One range should be returned from getTargetRanges() when the key operation deletes something" + ); + checkGetTargetRangesKeepReturningSameValue(gBeforeinput[0]); +} + +function checkGetTargetRangesOfInputOnDeleteSomething() { + assert_equals( + gInput.length, + 1, + "One input event should be fired if the key operation deletes something" + ); + // https://github.com/w3c/input-events/issues/113 + assert_true( + Array.isArray(gInput[0].cachedRanges), + "gInput[0].getTargetRanges() should return an array of StaticRange instances during propagation" + ); + assert_equals( + gInput[0].cachedRanges.length, + 0, + "gInput[0].getTargetRanges() should return empty array during propagation" + ); + checkGetTargetRangesKeepReturningSameValue(gInput[0]); +} + +function checkGetTargetRangesOfInputOnDoNothing() { + assert_equals( + gInput.length, + 0, + "input event shouldn't be fired when the key operation does not cause modifying the DOM tree" + ); +} + +function checkBeforeinputAndInputEventsOnNOOP() { + assert_equals( + gBeforeinput.length, + 0, + "beforeinput event shouldn't be fired when the key operation does not cause modifying the DOM tree" + ); + assert_equals( + gInput.length, + 0, + "input event shouldn't be fired when the key operation does not cause modifying the DOM tree" + ); +}