Files
tubestation/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-range-across-editing-host-boundaries.tentative.html
Masayuki Nakano e8b2d148cd Bug 1677566 - part 3: Ignore non-deletable ranges in HTMLEditor::HandleDeleteSelection() r=m_kato
For making delete handlers simpler, and set better target ranges to the
corresponding `beforeinput` event, we should ignore non-editable ranges
before handling deletion.

This patch makes editor stop handling deleteion when a range crosses editing
host boundaries.  In this case, Gecko has done nothing, but fired
`beforeinput` event.  Note that Blink deletes editable contents in the range
**until** it meets first non-editable content, but I don't think this is
a good behavior because it makes things complicated.  Therefore, I filed
a spec issue: https://github.com/w3c/editing/issues/283

On the other hand, this behavior change causes different behavior in
https://searchfox.org/mozilla-central/source/editor/libeditor/crashtests/1345015.html

It tries to insert paragraph into `<html>` element, but our editor currently
does not support it.  Therefore, it hits `MOZ_ASSERT`.  Therefore, this patch
added a new check into `HTMLEditor::InsertParagraphSeparatorAsSubAction()`.

Differential Revision: https://phabricator.services.mozilla.com/D107588
2021-03-09 23:57:54 +00:00

599 lines
32 KiB
HTML

<!DOCTYPE html>
<meta charset="utf-8">
<title>InputEvent.getTargetRanges() of deleting a range across editing host boundaries</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";
// This test just check whether the deleted content range(s) and target ranges of `beforeinput`
// are match or different. The behavior should be defined by editing API.
// https://github.com/w3c/editing/issues/283
promise_test(async () => {
initializeTest('<p>ab[c<span contenteditable="false">no]n-editable</span>def</p>');
await sendBackspaceKey();
const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>def</p>';
const kOnlyEditableTextDeletedCase = '<p>ab<span contenteditable="false">non-editable</span>def</p>';
const kNonEditableElementDeleteCase = '<p>abdef</p>';
if (gEditor.innerHTML === kNothingDeletedCase) {
if (gBeforeinput.length === 0) {
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
return;
}
assert_equals(gBeforeinput.length, 1,
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
getRangeDescription(gBeforeinput[0].cachedRanges[0])
})`);
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
assert_equals(gInput.length, 0,
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
return;
}
if (gEditor.innerHTML === kOnlyEditableTextDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If only editable text is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If only editable text is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild,
startOffset: 2,
endContainer: gEditor.firstChild.firstChild,
endOffset: 3,
}),
"If only editable text is deleted, its target range should be the deleted text range");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If only editable text is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If only editable text is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeleteCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
assert_in_array(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
[
getRangeDescription({
startContainer: gEditor.firstChild.firstChild,
startOffset: 2,
endContainer: gEditor.firstChild,
endOffset: 2,
}),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild,
startOffset: 2,
endContainer: gEditor.firstChild.firstChild.nextSibling,
endOffset: 0,
}),
],
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
assert_in_array(gEditor.innerHTML,
[
kNothingDeletedCase,
kOnlyEditableTextDeletedCase,
kNonEditableElementDeleteCase,
], "The result content is unexpected");
}, 'Backspace at "<p>ab[c<span contenteditable="false">no]n-editable</span>def</p>"');
promise_test(async () => {
initializeTest('<p>abc<span contenteditable="false">non-[editable</span>de]f</p>');
await sendBackspaceKey();
const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>def</p>';
const kOnlyEditableTextDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>f</p>';
const kNonEditableElementDeletedCase = '<p>abcf</p>';;
if (gEditor.innerHTML === kNothingDeletedCase) {
if (gBeforeinput.length === 0) {
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
return;
}
assert_equals(gBeforeinput.length, 1,
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
getRangeDescription(gBeforeinput[0].cachedRanges[0])
})`);
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
assert_equals(gInput.length, 0,
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
return;
}
if (gEditor.innerHTML === kOnlyEditableTextDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If only editable text is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If only editable text is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.lastChild,
startOffset: 0,
endContainer: gEditor.firstChild.lastChild,
endOffset: 2,
}),
"If only editable text is deleted, its target range should be the deleted text range");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If only editable text is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If only editable text is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.lastChild,
endOffset: 2,
}),
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
assert_in_array(gEditor.innerHTML,
[
kNothingDeletedCase,
kOnlyEditableTextDeletedCase,
kNonEditableElementDeletedCase,
], "The result content is unexpected");
}, 'Backspace at "<p>abc<span contenteditable="false">non-[editable</span>de]f</p>"');
promise_test(async () => {
initializeTest('<p contenteditable="false"><span contenteditable>a[bc</span>non-editable<span contenteditable>de]f</span></p>');
let firstRange = gSelection.getRangeAt(0);
if (!firstRange ||
firstRange.startContainer != gEditor.firstChild.firstChild.firstChild ||
firstRange.startOffset != 1 ||
firstRange.endContainer != gEditor.firstChild.lastChild.firstChild ||
firstRange.endOffset != 2) {
assert_true(true, "Selection couldn't set across editing host boundaries");
return;
}
await sendBackspaceKey();
const kNothingDeletedCase = '<p contenteditable="false"><span contenteditable="">abc</span>non-editable<span contenteditable="">def</span></p>';
const kOnlyEditableContentDeletedCase = '<p contenteditable="false"><span contenteditable="">a</span>non-editable<span contenteditable="">f</span></p>';
const kNonEditableElementDeletedCase = '<p contenteditable="false"><span contenteditable="">af</span></p>';
const kDeleteEditableContentBeforeNonEditableContentCase = '<p contenteditable="false"><span contenteditable="">a</span>non-editable<span contenteditable="">def</span></p>';
const kDeleteEditableContentAfterNonEditableContentCase = '<p contenteditable="false"><span contenteditable="">abc</span>non-editable<span contenteditable="">f</span></p>';
if (gEditor.innerHTML === kNothingDeletedCase) {
if (gBeforeinput.length === 0) {
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
return;
}
assert_equals(gBeforeinput.length, 1,
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
getRangeDescription(gBeforeinput[0].cachedRanges[0])
})`);
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
assert_equals(gInput.length, 0,
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
return;
}
if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If only editable text is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 2,
"If only editable text is deleted, `beforeinput` event should have 2 target ranges");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild.firstChild,
startOffset: 1,
endContainer: gEditor.lastChild,
endOffset: 3,
}),
"If only editable text is deleted, its first target range should be the deleted text range in the first text node");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
getRangeDescription({
startContainer: gEditor.firstChild.last.firstChild,
startOffset: 0,
endContainer: gEditor.firstChild.last.firstChild,
endOffset: 2,
}),
"If only editable text is deleted, its second target range should be the deleted text range in the last text node");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If only editable text is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If only editable text is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.lastChild.firstChild,
endOffset: 2,
}),
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text before non-editable element is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.firstChild.firstChild,
endOffset: 3,
}),
"If editable text before non-editable element is deleted, its target range should be only the deleted text");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text before non-editable element is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text before non-editable element is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text after non-editable element is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.lastChild.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.lastChild.firstChild,
endOffset: 3,
}),
"If editable text after non-editable element is deleted, its target range should be only the deleted text");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text after non-editable element is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text after non-editable element is deleted, `input` event should be fired");
return;
}
assert_in_array(gEditor.innerHTML,
[
kNothingDeletedCase,
kOnlyEditableContentDeletedCase,
kNonEditableElementDeletedCase,
kDeleteEditableContentBeforeNonEditableContentCase,
kDeleteEditableContentAfterNonEditableContentCase,
], "The result content is unexpected");
}, 'Backspace at "<p contenteditable="false"><span contenteditable>a[bc</span>non-editable<span contenteditable>de]f</span></p>"');
promise_test(async () => {
initializeTest('<p>a[bc<span contenteditable="false">non-editable<span contenteditable>de]f</span></span></p>');
let firstRange = gSelection.getRangeAt(0);
if (!firstRange ||
firstRange.startContainer != gEditor.firstChild.firstChild ||
firstRange.startOffset != 1 ||
firstRange.endContainer != gEditor.querySelector("span span").firstChild ||
firstRange.endOffset != 2) {
assert_true(true, "Selection couldn't set across editing host boundaries");
return;
}
await sendBackspaceKey();
const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable<span contenteditable="">def</span></span></p>';
const kOnlyEditableContentDeletedCase = '<p>a<span contenteditable="false">non-editable<span contenteditable="">f</span></span></p>';
const kNonEditableElementDeletedCase1 = '<p>af</p>';
const kNonEditableElementDeletedCase2 = '<p>a<span contenteditable="">f</span></p>';
const kDeleteEditableContentBeforeNonEditableContentCase ='<p>a<span contenteditable="false">non-editable<span contenteditable="">def</span></span></p>';
const kDeleteEditableContentAfterNonEditableContentCase ='<p>abc<span contenteditable="false">non-editable<span contenteditable="">f</span></span></p>';
if (gEditor.innerHTML === kNothingDeletedCase) {
if (gBeforeinput.length === 0) {
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
return;
}
assert_equals(gBeforeinput.length, 1,
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
getRangeDescription(gBeforeinput[0].cachedRanges[0])
})`);
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
assert_equals(gInput.length, 0,
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
return;
}
if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If only editable text is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 2,
"If only editable text is deleted, `beforeinput` event should have 2 target ranges");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.firstChild,
endOffset: 3,
}),
"If only editable text is deleted, its first target range should be the deleted text range in the first text node");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
getRangeDescription({
startContainer: gEditor.querySelector("span span").firstChild,
startOffset: 0,
endContainer: gEditor.querySelector("span span").firstChild,
endOffset: 2,
}),
"If only editable text is deleted, its second target range should be the deleted text range in the last text node");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If only editable text is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If only editable text is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeletedCase1) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
// XXX If the text nodes are merged, we need to cache it for here.
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.lastChild,
endOffset: 2,
}),
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeletedCase2) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild,
startOffset: 1,
endContainer: gEditor.querySelector("span").firstChild,
endOffset: 2,
}),
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text before non-editable element is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.firstChild,
endOffset: 3,
}),
"If editable text before non-editable element is deleted, its target range should be only the deleted text");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text before non-editable element is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text before non-editable element is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text after non-editable element is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.querySelector("span").firstChild,
startOffset: 0,
endContainer: gEditor.querySelector("span").firstChild,
endOffset: 2,
}),
"If editable text after non-editable element is deleted, its target range should be only the deleted text");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text after non-editable element is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text after non-editable element is deleted, `input` event should be fired");
return;
}
assert_in_array(gEditor.innerHTML,
[
kNothingDeletedCase,
kOnlyEditableContentDeletedCase,
kNonEditableElementDeletedCase1,
kNonEditableElementDeletedCase2,
kDeleteEditableContentBeforeNonEditableContentCase,
kDeleteEditableContentAfterNonEditableContentCase,
], "The result content is unexpected");
}, 'Backspace at "<p>a[bc<span contenteditable="false">non-editable<span contenteditable>de]f</span></span></p>"');
promise_test(async () => {
initializeTest('<p><span contenteditable="false"><span contenteditable>a[bc</span>non-editable</span>de]f</p>');
let firstRange = gSelection.getRangeAt(0);
if (!firstRange ||
firstRange.startContainer != gEditor.querySelector("span span").firstChild ||
firstRange.startOffset != 1 ||
firstRange.endContainer != gEditor.firstChild.lastChild.firstChild ||
firstRange.endOffset != 2) {
assert_true(true, "Selection couldn't set across editing host boundaries");
return;
}
await sendBackspaceKey();
const kNothingDeletedCase = '<p><span contenteditable="false"><span contenteditable="">abc</span>non-editable</span>def</p>';
const kOnlyEditableContentDeletedCase = '<p><span contenteditable="false"><span contenteditable="">a</span>non-editable</span>f</p>';
const kNonEditableElementDeletedCase1 = '<p><span contenteditable="false"><span contenteditable="">af</span></span></p>';
const kNonEditableElementDeletedCase2 = '<p><span contenteditable="false"><span contenteditable="">a</span></span>f</p>';
const kDeleteEditableContentBeforeNonEditableContentCase = '<p><span contenteditable="false"><span contenteditable="">a</span>non-editable</span>def</p>';
const kDeleteEditableContentAfterNonEditableContentCase = '<p><span contenteditable="false"><span contenteditable="">abc</span>non-editable</span>f</p>';
if (gEditor.innerHTML === kNothingDeletedCase) {
if (gBeforeinput.length === 0) {
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
return;
}
assert_equals(gBeforeinput.length, 1,
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
getRangeDescription(gBeforeinput[0].cachedRanges[0])
})`);
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
assert_equals(gInput.length, 0,
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
return;
}
if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
assert_equals(gBeforeinput.length, 1,
"If only editable text is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 2,
"If only editable text is deleted, `beforeinput` event should have 2 target ranges");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.querySelector("span span").firstChild,
startOffset: 1,
endContainer: gEditor.querySelector("span span").firstChild,
endOffset: 3,
}),
"If only editable text is deleted, its first target range should be the deleted text range in the first text node");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
getRangeDescription({
startContainer: gEditor.firstChild.lastChild,
startOffset: 0,
endContainer: gEditor.firstChild.lastChild,
endOffset: 2,
}),
"If only editable text is deleted, its second target range should be the deleted text range in the last text node");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If only editable text is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If only editable text is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeletedCase1) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
// XXX If the text nodes are merged, we need to cache it for here.
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.querySelector("span span").firstChild,
startOffset: 1,
endContainer: gEditor.querySelector("span span").lastChild,
endOffset: 2,
}),
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kNonEditableElementDeletedCase2) {
assert_equals(gBeforeinput.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.querySelector("span span").firstChild,
startOffset: 1,
endContainer: gEditor.firstChild.lastChild,
endOffset: 2,
}),
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text and non-editable element are deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text and non-editable element are deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text before non-editable element is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.querySelector("span span").firstChild,
startOffset: 1,
endContainer: gEditor.querySelector("span span").firstChild,
endOffset: 3,
}),
"If editable text before non-editable element is deleted, its target range should be only the deleted text");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text before non-editable element is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text before non-editable element is deleted, `input` event should be fired");
return;
}
if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
assert_equals(gBeforeinput.length, 1,
"If editable text after non-editable element is deleted, `beforeinput` event should be fired");
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
"If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
getRangeDescription({
startContainer: gEditor.firstChild.lastChild,
startOffset: 0,
endContainer: gEditor.firstChild.lastChild,
endOffset: 2,
}),
"If editable text after non-editable element is deleted, its target range should be only the deleted text");
assert_equals(gBeforeinput[0].inputType, "deleteContent",
"If editable text after non-editable element is deleted, its input type should be deleteContent");
assert_equals(gInput.length, 1,
"If editable text after non-editable element is deleted, `input` event should be fired");
return;
}
assert_in_array(gEditor.innerHTML,
[
kNothingDeletedCase,
kOnlyEditableContentDeletedCase,
kNonEditableElementDeletedCase1,
kNonEditableElementDeletedCase2,
kDeleteEditableContentBeforeNonEditableContentCase,
kDeleteEditableContentAfterNonEditableContentCase,
], "The result content is unexpected");
}, 'Backspace at "<p><span contenteditable="false"><span contenteditable>a[bc</span>non-editable</span>de]f</p>"');
</script>