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
This commit is contained in:
Masayuki Nakano 2020-09-27 04:49:41 +00:00
Родитель 8d848ca717
Коммит d3fe79f0e9
10 изменённых файлов: 1844 добавлений и 2379 удалений

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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> 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> 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 =
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> 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<StaticRange> 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 =
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<StaticRange> 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));
}
}
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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 "<p>abc def[\] ghi</p>"]
expected:
@ -40,12 +40,6 @@
[Backspace at "<div>abc <ul><li>[\] def </li></ul> ghi</div>"]
expected: FAIL
[Backspace at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
expected: FAIL
[Backspace at "<p>abc[</p><p>}<br></p>"]
expected: FAIL
[Backspace at "<p>abc<span contenteditable=\"false\">def</span>[\]ghi</p>"]
expected: FAIL

Просмотреть файл

@ -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 "<p>abc [\]def ghi</p>"]
expected:
if (os == "android"): FAIL
@ -69,14 +69,8 @@
[Delete at "<div>abc [\]<ul><li> def </li></ul> ghi</div>"]
expected: FAIL
[Delete at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
expected: FAIL
[Delete at "<div>abc <ul><li> def[\] </li></ul> ghi</div>"]
expected: FAIL
[Delete at "<p>{<br></p><p>\]abc</p>"]
expected: FAIL
[Delete at "<p>abc[\]<span contenteditable=\"false\">def</span>ghi</p>"]
expected: FAIL

Просмотреть файл

@ -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 "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
expected: FAIL
[Backspace at "<p>abc[</p><p>}<br></p>"]
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 "<p>abc[</p><p>}<br></p>"]
expected: FAIL
[Delete at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
expected: FAIL
[Delete at "<p>{<br></p><p>\]abc</p>"]
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 "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
expected: FAIL
[TypingA at "<p>abc[</p><p>}<br></p>"]
expected: FAIL

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,566 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="timeout" content="long">
<meta name="variant" content="?Backspace">
<meta name="variant" content="?Delete">
<meta name="variant" content="?TypingA">
<title>InputEvent.getTargetRanges() with non-collapsed selection</title>
<div contenteditable></div>
<script src="input-events-get-target-ranges.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script>
"use strict";
let action = location.search.substring(1);
function run() {
switch (action) {
case "Backspace":
return sendBackspaceKey();
case "Delete":
return sendDeleteKey();
case "TypingA":
return sendKeyA();
default:
throw "Unhandled variant";
}
}
let insertedHTML = action === "TypingA" ? "a" : "";
// Invisible leading white-spaces in current block and invisible trailing
// white-spaces in the previous block should be deleted for avoiding they
// becoming visible when the blocks are joined. Perhaps, they should be
// contained by the range of `getTargetRanges()`, but needs discussion.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
initializeTest("<p>abc </p><p> def</p>");
let p1 = gEditor.firstChild;
let abc = p1.firstChild;
let p2 = p1.nextSibling;
let def = p2.firstChild;
gSelection.setBaseAndExtent(abc, 6, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>abc [</p><p>] def</p>"`);
promise_test(async () => {
initializeTest("<p>abc</p><p>def</p>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p + p").firstChild;
gSelection.setBaseAndExtent(abc, 2, def, 1);
await run();
assert_equals(gEditor.innerHTML, `<p>ab${insertedHTML}ef</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 2,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>ab[c</p><p>d]ef</p>"`);
promise_test(async () => {
initializeTest("<p>abc </p><p> def</p>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p + p").firstChild;
gSelection.setBaseAndExtent(abc, 2, def, 2);
await run();
assert_equals(gEditor.innerHTML, `<p>ab${insertedHTML}ef</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 2,
endContainer: def,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>ab[c </p><p> d]ef</p>"`);
promise_test(async () => {
initializeTest("<p>abc </p><p> def</p>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p + p").firstChild;
gSelection.setBaseAndExtent(abc, 2, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<p>ab${insertedHTML}def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 2,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>ab[c </p><p>] def</p>"`);
promise_test(async () => {
initializeTest("<p>abc </p><p> def</p>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p + p").firstChild;
gSelection.setBaseAndExtent(abc, 4, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>abc [</p><p>] def</p>"`);
promise_test(async () => {
initializeTest("<p>abc </p><p> def</p>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p + p").firstChild;
gSelection.setBaseAndExtent(abc, 4, def, 1);
await run();
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>abc [</p><p> ]def</p>"`);
// Different from collapsed range around an atomic content, non-collapsed
// range may not be shrunken to select only the atomic content for avoid
// to waste runtime cost.
promise_test(async () => {
initializeTest(`<p>abc<img src="${kImgSrc}">def</p>`);
let p = gEditor.querySelector("p");
let abc = p.firstChild;
let def = p.lastChild;
gSelection.setBaseAndExtent(abc, 3, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>abc[<img>]def</p>"`);
promise_test(async () => {
initializeTest(`<div>abc<hr>def</div>`);
let div = gEditor.querySelector("div");
let abc = div.firstChild;
let def = div.lastChild;
gSelection.setBaseAndExtent(abc, 3, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc[<hr>]def</div>"`);
promise_test(async () => {
initializeTest(`<div>abc <hr>def</div>`);
let div = gEditor.querySelector("div");
let abc = div.firstChild;
let def = div.lastChild;
gSelection.setBaseAndExtent(abc, 4, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc [<hr>]def</div>"`);
promise_test(async () => {
initializeTest(`<div>abc <hr> def</div>`);
let div = gEditor.querySelector("div");
let abc = div.firstChild;
let def = div.lastChild;
gSelection.setBaseAndExtent(abc, 4, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc [<hr>] def</div>"`);
promise_test(async () => {
initializeTest(`<div>abc <hr> def</div>`);
let div = gEditor.querySelector("div");
let abc = div.firstChild;
let def = div.lastChild;
gSelection.setBaseAndExtent(div, 1, div, 2);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc {<hr>} def</div>"`);
// Deleting visible `<br>` element should be contained by a range of
// `getTargetRanges()`. However, when only the `<br>` element is selected,
// the range shouldn't start from nor end by surrounding text nodes?
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
initializeTest("<p>abc<br>def</p>");
gSelection.setBaseAndExtent(gEditor.firstChild, 1, gEditor.firstChild, 2);
await run();
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: gEditor.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>abc{<br>}def</p>"`);
promise_test(async () => {
initializeTest("<div>abc<p>def<br>ghi</p></div>");
let p = gEditor.querySelector("p");
let def = p.firstChild;
let abc = gEditor.firstChild.firstChild;
gSelection.setBaseAndExtent(abc, 3, def, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<div>abc${insertedHTML}def<p>ghi</p></div>`,
`<div>abc${insertedHTML}def<br><p>ghi</p></div>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc[<p>]def<br>ghi</p></div>"`);
promise_test(async () => {
initializeTest("<div>abc <p> def<br>ghi</p></div>");
let p = gEditor.querySelector("p");
let def = p.firstChild;
let abc = gEditor.firstChild.firstChild;
gSelection.setBaseAndExtent(abc, abc.length, def, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<div>abc${insertedHTML}def<p>ghi</p></div>`,
`<div>abc${insertedHTML}def<br><p>ghi</p></div>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc [<p>] def<br>ghi</p></div>"`);
promise_test(async () => {
initializeTest("<div><p>abc</p>def</div>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p").nextSibling;
gSelection.setBaseAndExtent(abc, 3, def, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<div><p>abc${insertedHTML}def</p></div>`,
`<div><p>abc${insertedHTML}def<br></p></div>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div><p>abc[</p>]def</div>"`);
promise_test(async () => {
initializeTest("<div><p>abc </p> def</div>");
let abc = gEditor.querySelector("p").firstChild;
let def = gEditor.querySelector("p").nextSibling;
gSelection.setBaseAndExtent(abc, abc.length, def, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<div><p>abc${insertedHTML}def</p></div>`,
`<div><p>abc${insertedHTML}def<br></p></div>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div><p>abc [</p>] def</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
let abc = gEditor.querySelector("div").firstChild;
let def = gEditor.querySelector("li").firstChild;
gSelection.setBaseAndExtent(abc, 3, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}defghi</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc[<ul><li>]def</li></ul>ghi</div>"`);
promise_test(async () => {
initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
let abc = gEditor.querySelector("div").firstChild;
let def = gEditor.querySelector("li").firstChild;
gSelection.setBaseAndExtent(abc, abc.length, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}defghi</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc [<ul><li>] def </li></ul> ghi</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
let def = gEditor.querySelector("li").firstChild;
let ghi = gEditor.querySelector("ul").nextSibling;
gSelection.setBaseAndExtent(def, 3, ghi, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def${insertedHTML}ghi</li></ul></div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: def,
startOffset: 3,
endContainer: ghi,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc<ul><li>def[</li></ul>]ghi</div>"`);
promise_test(async () => {
initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
let def = gEditor.querySelector("li").firstChild;
let ghi = gEditor.querySelector("ul").nextSibling;
gSelection.setBaseAndExtent(def, def.length, ghi, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<div>abc <ul><li> def${insertedHTML}ghi</li></ul></div>`,
`<div>abc <ul><li>def${insertedHTML}ghi</li></ul></div>`,
`<div>abc<ul><li>def${insertedHTML}ghi</li></ul></div>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: def,
startOffset: 5,
endContainer: ghi,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc <ul><li> def [</li></ul>] ghi</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
let abc = gEditor.querySelector("div").firstChild;
let def = gEditor.querySelector("li").firstChild;
gSelection.setBaseAndExtent(abc, 3, def, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def<ul><li>ghi</li></ul>jkl</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc[<ul><li>]def</li><li>ghi</li></ul>jkl</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
let abc = gEditor.querySelector("div").firstChild;
let def = gEditor.querySelector("li").firstChild;
let ghi = gEditor.querySelector("li + li").firstChild;
gSelection.setBaseAndExtent(abc, 3, ghi, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<div>abc${insertedHTML}ghijkl</div>`,
`<div>abc${insertedHTML}ghijkl<br></div>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: ghi,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc[<ul><li>def</li><li>]ghi</li></ul>jkl</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
let def = gEditor.querySelector("li").firstChild;
let ghi = gEditor.querySelector("li + li").firstChild;
gSelection.setBaseAndExtent(def, 3, ghi, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def${insertedHTML}ghi</li></ul>jkl</div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: def,
startOffset: 3,
endContainer: ghi,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc<ul><li>def[</li><li>]ghi</li></ul>jkl</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
let ghi = gEditor.querySelector("li + li").firstChild;
let jkl = gEditor.querySelector("ul").nextSibling;
gSelection.setBaseAndExtent(ghi, 3, jkl, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def</li><li>ghi${insertedHTML}jkl</li></ul></div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: ghi,
startOffset: 3,
endContainer: jkl,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc<ul><li>def</li><li>ghi[</li></ul>]jkl</div>"`);
promise_test(async () => {
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
let def = gEditor.querySelector("li").firstChild;
let jkl = gEditor.querySelector("ul").nextSibling;
gSelection.setBaseAndExtent(def, 3, jkl, 0);
await run();
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def${insertedHTML}jkl</li></ul></div>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: def,
startOffset: 3,
endContainer: jkl,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<div>abc<ul><li>def[</li><li>ghi</li></ul>]jkl</div>"`);
promise_test(async () => {
initializeTest("<p>abc</p><p><br></p>");
let p1 = gEditor.querySelector("p");
let abc = p1.firstChild;
let p2 = p1.nextSibling;
gSelection.setBaseAndExtent(abc, 3, p2, 0);
await run();
assert_in_array(gEditor.innerHTML, [`<p>abc${insertedHTML}</p>`,
`<p>abc${insertedHTML}<br></p>`]);
if (gEditor.innerHTML === "<p>abc</p>") {
// Include the invisible `<br>` element if it's deleted.
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: p2,
endOffset: 1,
});
} else {
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: p2,
endOffset: 0,
});
}
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<p>abc[</p><p>}<br></p>"`);
promise_test(async () => {
initializeTest("<p>abc<span contenteditable=\"false\">def</span>ghi</p>");
let p = gEditor.querySelector("p");
let abc = p.firstChild;
let ghi = p.lastChild;
gSelection.setBaseAndExtent(abc, 3, ghi, 0);
await run();
assert_in_array(gEditor.innerHTML, ["<p>abc<span contenteditable=\"false\">def</span>ghi</p>",
`<p>abc${insertedHTML}ghi</p>`,
`<p>abc${insertedHTML}ghi<br></p>`]);
// Don't need to shrink the range for avoiding to waste runtime cost.
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: ghi,
endOffset: 0,
});
if (gEditor.innerHTML === "<p>abc<span contenteditable=\"false\">def</span>ghi</p>") {
checkGetTargetRangesOfInputOnDoNothing();
} else {
checkGetTargetRangesOfInputOnDeleteSomething();
}
}, `${action} at "<p>abc[<span contenteditable=\"false\">def</span>]ghi</p>"`);
// The table structure shouldn't be modified when deleting cell contents,
// in this case, getTargetRanges() should return multiple ranges in each
// cell?
promise_test(async () => {
initializeTest("<table><tr><td>abc</td><td>def</td></tr></table>");
let abc = gEditor.querySelector("td").firstChild;
let def = gEditor.querySelector("td + td").firstChild;
gSelection.setBaseAndExtent(abc, 2, def, 1);
await run();
assert_equals(gEditor.innerHTML, `<table><tbody><tr><td>ab${insertedHTML}</td><td>ef</td></tr></tbody></table>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 2,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<table><tr><td>ab[c</td><td>d]ef</td></tr></table>"`);
promise_test(async () => {
initializeTest("<table><tr><td>abc</td><td>def</td></tr><tr><td>ghi</td><td>jkl</td></tr></table>");
let abc = gEditor.querySelector("td").firstChild;
let jkl = gEditor.querySelector("tr + tr > td + td").firstChild;
gSelection.setBaseAndExtent(abc, 2, jkl, 1);
await run();
assert_in_array(gEditor.innerHTML, [`<table><tbody><tr><td>ab${insertedHTML}</td><td></td></tr><tr><td></td><td>kl</td></tr></tbody></table>`,
`<table><tbody><tr><td>ab${insertedHTML}</td><td><br></td></tr><tr><td><br></td><td>kl</td></tr></tbody></table>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 2,
endContainer: jkl,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<table><tr><td>ab[c</td><td>def</td></tr><tr><td>ghi</td><td>j]kl</td></tr></table>"`);
promise_test(async () => {
initializeTest("<table><tr><td>abc</td><td>def</td></tr></table><table><tr><td>ghi</td><td>jkl</td></tr></table>");
let abc = gEditor.querySelector("td").firstChild;
let jkl = gEditor.querySelector("table + table td + td").firstChild;
gSelection.setBaseAndExtent(abc, 2, jkl, 1);
await run();
assert_in_array(gEditor.innerHTML, [`<table><tbody><tr><td>ab${insertedHTML}</td><td></td></tr></tbody></table><table><tbody><tr><td></td><td>kl</td></tr></tbody></table>`,
`<table><tbody><tr><td>ab${insertedHTML}</td><td><br></td></tr></tbody></table><table><tbody><tr><td><br></td><td>kl</td></tr></tbody></table>`]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 2,
endContainer: jkl,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, `${action} at "<table><tr><td>ab[c</td><td>def</td></tr></table><table><tr><td>ghi</td><td>j]kl</td></tr></table>"`);
</script>

Просмотреть файл

@ -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"
);
}