summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/input-events
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/input-events')
-rw-r--r--testing/web-platform/tests/input-events/META.yml4
-rw-r--r--testing/web-platform/tests/input-events/idlharness.window.js14
-rw-r--r--testing/web-platform/tests/input-events/input-events-cut-paste.html141
-rw-r--r--testing/web-platform/tests/input-events/input-events-exec-command.html230
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html2131
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html1744
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-range-across-editing-host-boundaries.tentative.html598
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-during-and-after-dispatch.tentative.html63
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-forwarddelete.tentative.html2209
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-element-and-another-list.tentative.html285
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-elements.tentative.html605
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html741
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges.html154
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges.js518
-rw-r--r--testing/web-platform/tests/input-events/input-events-typing.html285
-rw-r--r--testing/web-platform/tests/input-events/select-event-drag-remove.html32
16 files changed, 9754 insertions, 0 deletions
diff --git a/testing/web-platform/tests/input-events/META.yml b/testing/web-platform/tests/input-events/META.yml
new file mode 100644
index 0000000000..79e0cbe522
--- /dev/null
+++ b/testing/web-platform/tests/input-events/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/input-events/
+suggested_reviewers:
+ - johanneswilm
+ - siusin
diff --git a/testing/web-platform/tests/input-events/idlharness.window.js b/testing/web-platform/tests/input-events/idlharness.window.js
new file mode 100644
index 0000000000..3a9a34837e
--- /dev/null
+++ b/testing/web-platform/tests/input-events/idlharness.window.js
@@ -0,0 +1,14 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+idl_test(
+ ['input-events'],
+ ['uievents', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ InputEvent: ['new InputEvent("foo")'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/input-events/input-events-cut-paste.html b/testing/web-platform/tests/input-events/input-events-cut-paste.html
new file mode 100644
index 0000000000..f2ca5a0a65
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-cut-paste.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cut and Paste should trigger corresponding InputEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script type="text/javascript" src="pointerevent_support.js"></script>
+<p>To manually run this test, please follow the steps below:<br/>
+1. Select 'plain' => Cut (e.g. Ctrl/Cmd-X) => Paste (e.g. Ctrl/Cmd-V).<br/>
+2. Select 'rich' => Cut => Paste.<br/>
+3. Select 'prevent' => Paste.<br/>
+4. Select 'prevent' => Cut => Select 'normal' => Paste.<br/>
+<br/>
+If a "PASS" result appears the test passes, otherwise it fails</p>
+<textarea id="test1_plain">plain</textarea>
+<p id="test2_editable" contenteditable><b>rich</b></p>
+<p id="test3_editable_prevent" contenteditable>prevent</p>
+<p id="test3_editable_normal" contenteditable>normal</p>
+<script>
+
+function resolveWhen(condition) {
+ return new Promise((resolve, reject) => {
+ function tick() {
+ if (condition())
+ resolve();
+ else
+ requestAnimationFrame(tick.bind(this));
+ }
+ tick();
+ });
+}
+
+let modifier_key = "\uE009";
+if(navigator.platform.includes('Mac'))
+ modifier_key = "\uE03D";
+const commands = {
+ COPY: 'copy',
+ CUT: 'cut',
+ PASTE: 'paste',
+}
+function selectTextCommand(target, command) {
+ let command_key = "";
+ if(command == "copy")
+ command_key = "c";
+ else if (command == "cut")
+ command_key = "x";
+ else if (command == "paste")
+ command_key = "v";
+ return new test_driver.Actions()
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown()
+ .pointerUp()
+ .addTick()
+ .keyDown(modifier_key)
+ .keyDown("a")
+ .keyUp("a")
+ .keyDown(command_key)
+ .keyUp(command_key)
+ .keyUp(modifier_key)
+ .send();
+}
+
+function selectTextCutAndPaste(target1, target2) {
+ return selectTextCommand(target1, commands.CUT).then(() => {
+ return selectTextCommand(target2, commands.PASTE);
+ })
+}
+
+promise_test(async test => {
+ const expectedEventLog = [
+ 'cut-[null]', 'beforeinput-deleteByCut', 'input-deleteByCut',
+ 'paste-[null]', 'beforeinput-insertFromPaste', 'input-insertFromPaste'];
+ const actualEventLog = [];
+ const text1 = document.getElementById("test1_plain");
+
+ for (let eventType of ['beforeinput', 'input', 'cut', 'paste']) {
+ text1.addEventListener(eventType, test.step_func(function() {
+ if (event.type === 'beforeinput' && event.inputType === 'insertFromPaste') {
+ assert_equals(event.data, 'plain');
+ assert_equals(event.dataTransfer, null);
+ }
+
+ actualEventLog.push(`${event.type}-${event.inputType || '[null]'}`);
+ }));
+ }
+ await selectTextCutAndPaste(text1, text1);
+ await resolveWhen(() => { return actualEventLog.length == expectedEventLog.length });
+ assert_array_equals(actualEventLog, expectedEventLog,
+ `Expected: ${expectedEventLog}; Actual: ${actualEventLog}.`);
+}, 'Event order and data on textarea.');
+
+promise_test(async test => {
+ const expectedEventLog = [
+ 'cut-[null]', 'beforeinput-deleteByCut', 'input-deleteByCut',
+ 'paste-[null]', 'beforeinput-insertFromPaste', 'input-insertFromPaste'];
+ const actualEventLog = [];
+ const text2 = document.getElementById("test2_editable");
+
+ for (let eventType of ['beforeinput', 'input', 'cut', 'paste']) {
+ text2.addEventListener(eventType, test.step_func(function() {
+ if (event.type === 'beforeinput' && event.inputType === 'insertFromPaste') {
+ assert_equals(event.data, null);
+ assert_equals(event.dataTransfer.getData('text/plain'), 'rich');
+ assert_regexp_match(event.dataTransfer.getData('text/html'), /<b.*>rich<\/b>$/);
+ }
+
+ actualEventLog.push(`${event.type}-${event.inputType || '[null]'}`);
+ }));
+ }
+ await selectTextCutAndPaste(text2, text2);
+ await resolveWhen(() => { return actualEventLog.length == expectedEventLog.length });
+ assert_array_equals(actualEventLog, expectedEventLog,
+ `Expected: ${expectedEventLog}; Actual: ${actualEventLog}.`);
+}, 'Event order and dataTransfer on contenteditable.');
+
+promise_test(async test => {
+ const prevent = document.getElementById('test3_editable_prevent');
+ const normal = document.getElementById('test3_editable_normal');
+ prevent.addEventListener('beforeinput', test.step_func(function() {
+ if (event.inputType === 'deleteByCut' ||
+ event.inputType === 'insertFromPaste') {
+ event.preventDefault();
+ }
+ }));
+
+ normal.addEventListener('input', test.step_func(function() {
+ if (event.inputType === 'insertFromPaste') {
+ assert_equals(prevent.textContent, 'prevent');
+ assert_equals(normal.textContent, 'prevent');
+ }
+ }));
+
+ await selectTextCommand(prevent, commands.PASTE);
+ await selectTextCutAndPaste(prevent, normal);
+ await resolveWhen(() => { return normal.textContent == 'prevent' });
+ assert_equals(prevent.textContent, 'prevent');
+ assert_equals(normal.textContent, 'prevent');
+}, 'preventDefault() should prevent DOM modification but allow clipboard updates.');
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-exec-command.html b/testing/web-platform/tests/input-events/input-events-exec-command.html
new file mode 100644
index 0000000000..8f8493651e
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-exec-command.html
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>execCommand() should only trigger 'input'</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="txt" contenteditable></div>
+<script>
+(function() {
+ let lastBeforeInputType = '';
+ let lastBeforeInputData = '';
+ let lastBeforeInputDataTransfer = undefined;
+ let lastInputType = '';
+ let lastInputData = '';
+ let lastInputDataTransfer = undefined;
+ const txt = document.getElementById('txt');
+ txt.addEventListener('beforeinput', function(event) {
+ assert_true(event instanceof InputEvent);
+ assert_false(event.isComposing);
+ lastBeforeInputType = event.inputType;
+ lastBeforeInputData = event.data;
+ lastBeforeInputDataTransfer = event.dataTransfer;
+ });
+ txt.addEventListener('input', function(event) {
+ assert_true(event instanceof InputEvent);
+ assert_false(event.isComposing);
+ lastInputType = event.inputType;
+ lastInputData = event.data;
+ lastInputDataTransfer = event.dataTransfer;
+ });
+
+ const NO_INPUT_EVENT_FIRED = 'NO_INPUT_EVENT_FIRED';
+ function testExecCommandInputType(command, args, inputType, data, dataTransfer) {
+ const description = `Calling execCommand("${command}", false, ${args})`;
+ lastBeforeInputType = NO_INPUT_EVENT_FIRED;
+ lastBeforeInputData = NO_INPUT_EVENT_FIRED;
+ lastBeforeInputDataTransfer = NO_INPUT_EVENT_FIRED;
+ lastInputType = NO_INPUT_EVENT_FIRED;
+ lastInputData = NO_INPUT_EVENT_FIRED;
+ lastInputDataTransfer = NO_INPUT_EVENT_FIRED;
+ test(function() {
+ try {
+ document.execCommand(command, false, args);
+ } catch (e) {
+ assert_true(false, `execCommand shouldn't cause any exception: ${e}`);
+ }
+ }, description + " (calling execCommand)");
+ test(function() {
+ assert_equals(lastBeforeInputType, NO_INPUT_EVENT_FIRED);
+ assert_equals(lastBeforeInputData, NO_INPUT_EVENT_FIRED);
+ assert_equals(lastBeforeInputDataTransfer, NO_INPUT_EVENT_FIRED);
+ }, description + " (shouldn't fire beforeinput)");
+ test(function() {
+ assert_equals(lastInputType, inputType);
+ }, description + " (inputType value)");
+ if (lastInputType === NO_INPUT_EVENT_FIRED) {
+ return;
+ }
+ test(function() {
+ assert_equals(lastInputData, data);
+ }, description + " (data value)");
+ if (dataTransfer === null) {
+ test(function() {
+ assert_equals(lastInputDataTransfer, dataTransfer,
+ `${description} should produce dataTransfer: null`);
+ }, description + " (dataTransfer value)");
+ } else {
+ for (let item of dataTransfer) {
+ test(function() {
+ try {
+ assert_equals(lastInputDataTransfer.getData(item.type), item.data,
+ `${description} should produce dataTransfer.getData(${item.type}): ${item.data}`);
+ } catch (e) {
+ assert_true(false, `calling dataTransfer.getData(${item.type}) caused exception: ${e}`);
+ }
+ }, `${description} (dataTransfer value, ${item.type})`);
+ test(function() {
+ try {
+ lastInputDataTransfer.clearData(item.type);
+ } catch (e) {
+ assert_true(false, `calling dataTransfer.clearData(${item.type}) caused exception: ${e}`);
+ }
+ assert_equals(lastInputDataTransfer.getData(item.type), item.data,
+ `${description} dataTransfer.clearData(${item.type}) should do nothing`);
+ }, `${description} (dataTransfer.clearData(${item.type}))`);
+ test(function() {
+ try {
+ lastInputDataTransfer.setData(item.type, "foo");
+ } catch (e) {
+ assert_true(false, `calling dataTransfer.setData(${item.type}) caused exception: ${e}`);
+ }
+ assert_equals(lastInputDataTransfer.getData(item.type), item.data,
+ `${description} dataTransfer.setData(${item.type}, "foo") should do nothing`);
+ }, `${description} (dataTransfer.setData(${item.type}))`);
+ }
+ }
+ }
+
+ txt.focus();
+ // InsertText
+ testExecCommandInputType('insertText', 'a', 'insertText', 'a', null);
+ testExecCommandInputType('insertText', 'bc', 'insertText', 'bc', null);
+ test(function() {
+ assert_equals(txt.innerHTML, 'abc');
+ }, "execCommand(\"insertText\") should insert \"abc\" into the editor");
+ testExecCommandInputType('insertOrderedList', null, 'insertOrderedList', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<ol><li>abc</li></ol>');
+ }, "execCommand(\"insertOrderedList\") should make <ol> and wrap the text with it");
+ testExecCommandInputType('insertUnorderedList', null, 'insertUnorderedList', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<ul><li>abc</li></ul>');
+ }, "execCommand(\"insertUnorderedList\") should make <ul> and wrap the text with it");
+ testExecCommandInputType('insertLineBreak', null, 'insertLineBreak', null, null);
+ testExecCommandInputType('insertParagraph', null, 'insertParagraph', null, null);
+ txt.innerHTML = '';
+ testExecCommandInputType('insertHorizontalRule', null, 'insertHorizontalRule', null, null);
+
+ // Styling
+ txt.innerHTML = 'abc';
+ var selection = window.getSelection();
+ selection.collapse(txt, 0);
+ selection.extend(txt, 1);
+ testExecCommandInputType('bold', null, 'formatBold', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<b>abc</b>');
+ }, "execCommand(\"bold\") should wrap selected text with <b> element");
+ testExecCommandInputType('italic', null, 'formatItalic', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<b><i>abc</i></b>');
+ }, "execCommand(\"italic\") should wrap selected text with <i> element");
+ testExecCommandInputType('underline', null, 'formatUnderline', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<b><i><u>abc</u></i></b>');
+ }, "execCommand(\"underline\") should wrap selected text with <u> element");
+ testExecCommandInputType('strikeThrough', null, 'formatStrikeThrough', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<b><i><u><strike>abc</strike></u></i></b>');
+ }, "execCommand(\"strikeThrough\") should wrap selected text with <strike> element");
+ testExecCommandInputType('superscript', null, 'formatSuperscript', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<b><i><u><strike><sup>abc</sup></strike></u></i></b>');
+ }, "execCommand(\"superscript\") should wrap selected text with <sup> element");
+ testExecCommandInputType('subscript', null, 'formatSubscript', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<b><i><u><strike><sub>abc</sub></strike></u></i></b>');
+ }, "execCommand(\"subscript\") should wrap selected text with <sub> element");
+ txt.innerHTML = 'abc';
+ selection.collapse(txt, 0);
+ selection.extend(txt, 1);
+ for (let test of [{command: 'backColor', inputType: 'formatBackColor'},
+ {command: 'foreColor', inputType: 'formatFontColor'},
+ {command: 'hiliteColor', inputType: 'formatBackColor'}]) {
+ testExecCommandInputType(test.command, '#FF0000', test.inputType, 'rgb(255, 0, 0)', null);
+ testExecCommandInputType(test.command, '#00FF00FF', test.inputType, 'rgb(0, 255, 0)', null);
+ testExecCommandInputType(test.command, '#0000FF88', test.inputType, 'rgba(0, 0, 255, 0.533)', null);
+ testExecCommandInputType(test.command, 'orange', test.inputType, 'rgb(255, 165, 0)', null);
+ testExecCommandInputType(test.command, 'Inherit', test.inputType, 'inherit', null);
+ testExecCommandInputType(test.command, 'Initial', test.inputType, 'initial', null);
+ testExecCommandInputType(test.command, 'Reset', test.inputType, 'reset', null);
+ testExecCommandInputType(test.command, 'transparent', test.inputType, 'rgba(0, 0, 0, 0)', null);
+ testExecCommandInputType(test.command, 'CurrentColor', test.inputType, 'currentcolor', null);
+ testExecCommandInputType(test.command, 'Invalid-Value', test.inputType, 'Invalid-Value', null);
+ }
+
+ testExecCommandInputType('fontName', 'monospace', 'formatFontName', 'monospace', null);
+ testExecCommandInputType('fontName', ' monospace ', 'formatFontName', ' monospace ', null);
+ testExecCommandInputType('fontName', ' monospace ', 'formatFontName', ' monospace ', null);
+
+ // Formating
+ txt.innerHTML = 'abc';
+ testExecCommandInputType('justifyCenter', null, 'formatJustifyCenter', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<div style="text-align: center;">abc</div>');
+ }, "execCommand(\"justifyCenter\") should wrap the text with <div> element whose text-align is center");
+ testExecCommandInputType('justifyFull', null, 'formatJustifyFull', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<div style="text-align: justify;">abc</div>');
+ }, "execCommand(\"justifyFull\") should wrap the text with <div> element whose text-align is justify");
+ testExecCommandInputType('justifyRight', null, 'formatJustifyRight', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<div style="text-align: right;">abc</div>');
+ }, "execCommand(\"justifyRight\") should wrap the text with <div> element whose text-align is right");
+ testExecCommandInputType('justifyLeft', null, 'formatJustifyLeft', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<div style="text-align: left;">abc</div>');
+ }, "execCommand(\"justifyLeft\") should wrap the text with <div> element whose text-align is left");
+ selection.collapse(txt, 0);
+ selection.extend(txt, 1);
+ testExecCommandInputType('removeFormat', null, 'formatRemove', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<div style="">abc</div>');
+ }, "execCommand(\"removeFormat\") should remove the style of current block");
+ testExecCommandInputType('indent', null, 'formatIndent', null, null);
+ testExecCommandInputType('outdent', null, 'formatOutdent', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, '<div style="">abc</div>');
+ }, "Set of execCommand(\"indent\") and execCommand(\"outdent\") should keep same DOM tree");
+
+ // Copy shouldn't fire 'input'.
+ txt.innerHTML = 'ab<b>c</b>def';
+ selection.collapse(txt.firstChild, 1);
+ selection.extend(txt.firstChild.nextSibling.nextSibling, 1);
+ testExecCommandInputType('copy', null, NO_INPUT_EVENT_FIRED, NO_INPUT_EVENT_FIRED, NO_INPUT_EVENT_FIRED);
+ // Cut/Paste should fire 'input'.
+ testExecCommandInputType('cut', null, 'deleteByCut', null, null);
+ // XXX How can we test 'text/html' case? The detail of copied HTML fragment depends on browser.
+ testExecCommandInputType('paste', null, 'insertFromPaste', null, [{type: 'text/plain', data: 'bcd'}]);
+
+ // Link and Unlink
+ txt.innerHTML = 'abc';
+ selection.collapse(txt.firstChild, 1);
+ selection.extend(txt.firstChild, 2);
+ testExecCommandInputType('createLink', 'https://example.com/', 'insertLink', 'https://example.com/', null);
+ test(function() {
+ assert_equals(txt.innerHTML, 'a<a href="https://example.com/">b</a>c');
+ }, "execCommand(\"createLink\") should create a link with absolute URL");
+ testExecCommandInputType('unlink', null, '', null, null);
+ test(function() {
+ assert_equals(txt.innerHTML, 'abc');
+ }, "execCommand(\"createLink\") should remove the link");
+
+ txt.innerHTML = 'abc';
+ selection.collapse(txt.firstChild, 1);
+ selection.extend(txt.firstChild, 2);
+ testExecCommandInputType('createLink', 'foo.html', 'insertLink', 'foo.html', null);
+ test(function() {
+ assert_equals(txt.innerHTML, 'a<a href="foo.html">b</a>c');
+ }, "execCommand(\"createLink\") should create a link with relative URL");
+})();
+</script>
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
new file mode 100644
index 0000000000..90755042a6
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html
@@ -0,0 +1,2131 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>InputEvent.getTargetRanges() at Backspace</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";
+
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 0,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Backspace at "<p>[]abc</p>"');
+
+// Simply deletes the previous ASCII character of caret position.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>bc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 0,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>a[]bc</p>"');
+
+// Simply deletes the previous ASCII character of caret position.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 2);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>ac</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 1,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>ab[]c</p>"');
+
+// Simply deletes the previous ASCII character of caret position.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 3);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>ab</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 2,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc[]</p>"');
+
+// Should delete the `<span>` element because it becomes empty.
+// However, we need discussion whether the `<span>` element should be
+// contained by a range of `getTargetRanges()`.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>a<span>b</span>c</p>");
+ let c = gEditor.querySelector("span").nextSibling;
+ gSelection.collapse(c, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>ac</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild,
+ startOffset: 1,
+ endContainer: c,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>a<span>b</span>[]c</p>"');
+
+// Should delete the `<span>` element because it becomes empty.
+// However, we need discussion whether the `<span>` element should be
+// contained by a range of `getTargetRanges()`.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>a<span>b</span>c</p>");
+ let b = gEditor.querySelector("span").firstChild;
+ gSelection.collapse(b, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>ac</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild,
+ startOffset: 1,
+ endContainer: gEditor.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>a<span>b[]</span>c</p>"');
+
+// Invisible leading white-space may be deleted when the first visible
+// character is deleted. If it's deleted, it should be contained by
+// the range of `getTargetRanges()`, but needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p> abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 2);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>bc</p>",
+ "<p> bc</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: gEditor.firstChild.firstChild.length == 2 ? 0 : 1,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p> a[]bc</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p>def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc</p><p>[]def</p>"');
+
+// 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 3);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc </p><p> []def</p>"');
+
+// 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 2);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc </p><p> [] def</p>"');
+
+// 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc </p><p> [] def</p>"');
+
+// 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc </p><p>[] def</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p><b>def</b></p>");
+ let abc = gEditor.querySelector("p").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc<b>def</b></p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc</p><p><b>[]def</b></p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p><b>abc</b></p><p><b>def</b></p>");
+ let abc = gEditor.querySelector("p > b").firstChild;
+ let def = gEditor.querySelector("P + p > b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p><b>abc</b><b>def</b></p>",
+ "<p><b>abcdef</b></p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p><b>abc</b></p><p><b>[]def</b></p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p><i>abc</i></p><p><b>def</b></p>");
+ let abc = gEditor.querySelector("i").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p><i>abc</i><b>def</b></p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p><i>abc</i></p><p><b>[]def</b></p>"');
+
+// Invisible leading white-spaces in the current block should be deleted
+// for avoiding they becoming visible when the blocks are joined, but
+// preformatted trailing white-spaces in the first block shouldn't be
+// deleted. Perhaps, the invisible white-spaces should be contained by
+// the range of `getTargetRanges()`, but needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<pre>abc </pre><p> def</p>");
+ let pre = gEditor.firstChild;
+ let abc = pre.firstChild;
+ let p = pre.nextSibling;
+ let def = p.firstChild;
+ gSelection.collapse(def, 3);
+ await sendBackspaceKey();
+ // https://github.com/w3c/input-events/issues/112
+ // Shouldn't make the invisible white-spaces visible.
+ checkEditorContentResultAsSubTest("<pre>abc def</pre>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 6,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<pre>abc </pre><p> []def</p>"');
+
+// Invisible leading/trailing white-spaces in the current block should be
+// deleted for avoiding they becoming visible when the blocks are joined, but
+// preformatted trailing white-spaces in the first block shouldn't be
+// deleted. Perhaps, the invisible leading white-spaces should be contained
+// by the range of `getTargetRanges()`, but needs discussion.
+// And also not sure whether the trailing white-spaces should be contained
+// by additional range of `getTargetRanges()` or not because of the
+// implementation cost and runtime cost. Needs discuss.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<pre>abc </pre><p> def </p>");
+ let pre = gEditor.firstChild;
+ let abc = pre.firstChild;
+ let p = pre.nextSibling;
+ let def = p.firstChild;
+ gSelection.collapse(def, 3);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<pre>abc def </pre>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 6,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<pre>abc </pre><p> []def </p>"');
+
+// Invisible trailing white-spaces in the first block should be deleted
+// when the block is joined with the preformatted following block, but
+// the leading white-spaces in the preformatted block shouldn't be
+// removed. So, in this case, the invisible trailing white-spaces should
+// be in the range of `getTargetRanges()`, but not so for the preformatted
+// visible leading white-spaces. But needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc </p><pre> def</pre>");
+ let p = gEditor.firstChild;
+ let abc = p.firstChild;
+ let pre = p.nextSibling;
+ let def = pre.firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc &nbsp; def</p>",
+ "<p>abc&nbsp;&nbsp; def</p>",
+ "<p>abc&nbsp; &nbsp;def</p>",
+ "<p>abc &nbsp;&nbsp;def</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 6,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc </p><pre>[] def</pre>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc\ndef</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc\n".length);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc\n".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p style="white-space:pre-line">abc\\n[]def</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc \ndef</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc \n".length);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc \n".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p style="white-space:pre-line">abc \\n[]def</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc\n def</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc\n ".length);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc\n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p style="white-space:pre-line">abc\\n []def</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc \n def</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc \n ".length);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc \n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p style="white-space:pre-line">abc \\n []def</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc \n \n def</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc \n \n ".length);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abc \ndef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc \n".length,
+ endContainer: text,
+ endOffset: "abc \n \n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p style="white-space:pre-line">abc \\n \\n []def</p>"');
+
+// If the first block has invisible `<br>` element and joining it with
+// the following block, the invisible trailing `<br>` element should be
+// deleted and join the blocks. Therefore, the target range should contain
+// the `<br>` element and block boundaries. But maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc<br></p><p>def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<br></p><p>[]def</p>"');
+
+// If the first block has invisible `<br>` element for empty last line and
+// joining it with the following block, the invisible trailing `<br>` element
+// should be deleted and join the blocks. Therefore, the target range should
+// contain the `<br>` element and block boundaries. But maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc<br><br></p><p>def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc<br>def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 2,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<br><br></p><p>[]def</p>"');
+
+// Deleting visible `<br>` element should be contained by a range of
+// `getTargetRanges()`.
+promise_test(async (t) => {
+ initializeTest("<p>abc<br>def</p>");
+ let p = document.querySelector("p");
+ let def = gEditor.querySelector("br").nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<br>[]def</p>"');
+
+// Deleting visible `<br>` element following white-space should not include
+// the preceding white-space in the range.
+promise_test(async (t) => {
+ initializeTest("<p>abc <br>def</p>");
+ let p = gEditor.querySelector("p");
+ let def = gEditor.querySelector("br").nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc <br>[]def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let def = p.lastChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<img>[]def</p>"');
+
+// White-spaces around `<img>` element are visible so that they shouldn't
+// be included into the target ranges.
+promise_test(async (t) => {
+ initializeTest(`<p>abc <img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let def = p.lastChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc <img>[]def</p>"');
+
+// White-spaces around `<img>` element are visible so that they shouldn't
+// be included into the target ranges.
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}"> def</p>`);
+ let p = gEditor.querySelector("p");
+ let def = p.lastChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<img>[] def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(p, 2);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ `<p>abc<img src="${kImgSrc}">def</p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<img>{}<img>def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let def = p.lastChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ `<p>abc<img src="${kImgSrc}">def</p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 2,
+ endContainer: p,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<img><img>[]def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<div>abc<hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let hr = gEditor.querySelector("hr");
+ let def = hr.nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<hr>[]def</div>"');
+
+// White-spaces around block element are invisible white-spaces so that
+// they should be included into the target ranges to avoid they bcome
+// visible.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest(`<div>abc <hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let abc = div.firstChild;
+ let hr = gEditor.querySelector("hr");
+ let def = hr.nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: div,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc <hr>[]def</div>"');
+
+// White-spaces around block element are invisible white-spaces so that
+// they should be included into the target ranges to avoid they bcome
+// visible.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest(`<div>abc<hr> def</div>`);
+ let div = gEditor.querySelector("div");
+ let hr = gEditor.querySelector("hr");
+ let def = hr.nextSibling;
+ gSelection.collapse(def, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<hr> []def</div>"');
+
+// Invisible `<br>` element immediately before `<hr>` element should be
+// deleted once, and both of them should be included in the target range.
+promise_test(async (t) => {
+ initializeTest(`<div>abc<br><hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let def = div.lastChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<br><hr>[]def</div>"');
+
+// Deleting visible `<br>` element followed by white-space should include
+// the following white-space in the range because it shouldn't become
+// visible and should be deleted for avoiding it.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc<br> def</p>");
+ let p = gEditor.querySelector("p");
+ let def = gEditor.querySelector("br").nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<br>[] def</p>"');
+promise_test(async (t) => {
+ initializeTest("<p>abc<br> def</p>");
+ let p = gEditor.querySelector("p");
+ let def = gEditor.querySelector("br").nextSibling;
+ gSelection.collapse(def, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<br> []def</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<p>def<br>ghi</p></div>");
+ let p = gEditor.querySelector("p");
+ let def = p.firstChild;
+ let abc = gEditor.firstChild.firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abcdef<p>ghi</p></div>",
+ "<div>abcdef<br><p>ghi</p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<p>[]def<br>ghi</p></div>"');
+
+// Joining parent block and child block should remove invisible preceding
+// white-spaces of the child block and invisible leading white-spaces in
+// the child block, and they should be contained in a range of
+// `getTargetRanges()`, but maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<div>abc <p> def<br>ghi</p></div>");
+ let p = gEditor.querySelector("p");
+ let def = p.firstChild;
+ let abc = gEditor.firstChild.firstChild;
+ gSelection.collapse(def, 3);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abcdef<p>ghi</p></div>",
+ "<div>abcdef<br><p>ghi</p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc <p> []def<br>ghi</p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<p><b>def</b></p></div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abc<b>def</b></div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<p><b>[]def</b></p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><b>abc</b><p><b>def</b></p></div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("p > b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><b>abc</b><b>def</b></div>",
+ "<div><b>abcdef</b></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><b>abc</b><p><b>[]def</b></p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><i>abc</i><p><b>def</b></p></div>");
+ let abc = gEditor.querySelector("i").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div><i>abc</i><b>def</b></div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><i>abc</i><p><b>[]def</b></p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p>abc</p>def</div>");
+ let abc = gEditor.querySelector("p").firstChild;
+ let def = gEditor.querySelector("p").nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p>abcdef</p></div>",
+ "<div><p>abcdef<br></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><p>abc</p>[]def</div>"');
+
+// Joining child block and parent block should remove invisible trailing
+// white-spaces of the child block and invisible following white-spaces
+// in the parent block, and they should be contained by a range of
+// `getTargetRanges()`, but maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<div><p>abc </p> def</div>");
+ let abc = gEditor.querySelector("p").firstChild;
+ let def = gEditor.querySelector("p").nextSibling;
+ gSelection.collapse(def, 3);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p>abcdef</p></div>",
+ "<div><p>abcdef<br></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><p>abc </p> []def</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p><b>abc</b></p>def</div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("p").nextSibling;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div><p><b>abc</b>def</p></div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><p><b>abc</b></p>[]def</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p><b>abc</b></p><b>def</b></div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("div > b").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p><b>abc</b><b>def</b></p></div>",
+ "<div><p><b>abcdef</b></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><p><b>abc</b></p><b>[]def</b></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p><b>abc</b></p><i>def</i></div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("i").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ "<div><p><b>abc</b><i>def</i></p></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div><p><b>abc</b></p><i>[]def</i></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("li").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdefghi</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<ul><li>[]def</li></ul>ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("li").firstChild;
+ gSelection.collapse(def, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdefghi</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc <ul><li> []def </li></ul> ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("li").firstChild;
+ gSelection.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<div>abcdefghi</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc <ul><li>[] def </li></ul> ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
+ let def = gEditor.querySelector("li").firstChild;
+ let ghi = gEditor.querySelector("ul").nextSibling;
+ gSelection.collapse(ghi, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abc<ul><li>defghi</li></ul></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 3,
+ endContainer: ghi,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<ul><li>def</li></ul>[]ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let def = gEditor.querySelector("li").firstChild;
+ let ghi = gEditor.querySelector("ul").nextSibling;
+ gSelection.collapse(ghi, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abc <ul><li> defghi</li></ul></div>",
+ "<div>abc <ul><li>defghi</li></ul></div>",
+ "<div>abc<ul><li>defghi</li></ul></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 5,
+ endContainer: ghi,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc <ul><li> def </li></ul> []ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let def = gEditor.querySelector("li").firstChild;
+ let ghi = gEditor.querySelector("ul").nextSibling;
+ gSelection.collapse(ghi, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abc <ul><li> defghi</li></ul></div>",
+ "<div>abc <ul><li>defghi</li></ul></div>",
+ "<div>abc<ul><li>defghi</li></ul></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 5,
+ endContainer: ghi,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc <ul><li> def </li></ul>[] ghi</div>"');
+
+promise_test(async (t) => {
+ 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.collapse(def, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abcdef<ul><li>ghi</li></ul>jkl</div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<ul><li>[]def</li><li>ghi</li></ul>jkl</div>"');
+
+promise_test(async (t) => {
+ 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.collapse(ghi, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abc<ul><li>defghi</li></ul>jkl</div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 3,
+ endContainer: ghi,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<ul><li>def</li><li>[]ghi</li></ul>jkl</div>"');
+
+promise_test(async (t) => {
+ 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.collapse(jkl, 0);
+ await sendBackspaceKey();
+ assert_equals(gEditor.innerHTML,
+ "<div>abc<ul><li>def</li><li>ghijkl</li></ul></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: ghi,
+ startOffset: 3,
+ endContainer: jkl,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<div>abc<ul><li>def</li><li>ghi</li></ul>[]jkl</div>"');
+
+// Backspace in empty paragraph should remove the empty paragraph. In this
+// case, it should be treated as joining with the previous paragraph.
+// The target range should include the invisible <br> element in the empty
+// paragraph.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p><br></p>");
+ let p1 = gEditor.querySelector("p");
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ gSelection.collapse(p2, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: p2,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc</p><p>{}<br></p>"');
+
+// Delete ignore the empty span and the other things must be same as the
+// previous test.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p><span></span><br></p>");
+ let p1 = gEditor.querySelector("p");
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let span = p2.firstChild;
+ gSelection.collapse(span, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest("<p>abc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: p2,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc</p><p><span>{}</span><br></p>"');
+
+// If invisible white-spaces are removed with same action as above tests,
+// the range should be included in the target ranges.
+promise_test(async (t) => {
+ initializeTest("<p>abc </p><p><br></p>");
+ let p1 = gEditor.querySelector("p");
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ gSelection.collapse(p2, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc </p>",
+ "<p>abc</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: abc.length,
+ endContainer: p2,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc </p><p>{}<br></p>"');
+
+// If the previous block ends with non-editable content, target range
+// should be after the non-editable content node.
+promise_test(async (t) => {
+ initializeTest("<p>abc<span contenteditable=\"false\">def</span></p><p><br></p>");
+ let p1 = gEditor.querySelector("p");
+ let span = gEditor.querySelector("span");
+ let p2 = p1.nextSibling;
+ gSelection.collapse(p2, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ "<p>abc<span contenteditable=\"false\">def</span></p>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 2,
+ endContainer: p2,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc<span contenteditable="false">def</span></p><p>{}<br></p>"');
+
+// If previous non-editable paragraph is deleted, target range should begin
+// with end of the text node in the first paragraph. Otherwise, start from
+// after the non-editable paragraph.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p contenteditable=\"false\">def</p><p><br></p>");
+ let p1 = gEditor.querySelector("p");
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let p3 = p2.nextSibling;
+ gSelection.collapse(p3, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc</p>",
+ "<p>abc</p><p contenteditable=\"false\">def</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p2.isConnected ? gEditor : abc,
+ startOffset: p2.isConnected ? 2 : abc.length,
+ endContainer: p3,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<p>abc</p><p contenteditable=\"false\">def</p><p>{}<br></p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc<span contenteditable=\"false\">def</span>ghi</p>");
+ let p = gEditor.querySelector("p");
+ let ghi = p.lastChild;
+ gSelection.collapse(ghi, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc<span contenteditable=\"false\">def</span>ghi</p>",
+ "<p>abcghi</p>",
+ "<p>abcghi<br></p>",
+ ],
+ t.name
+ );
+ if (gEditor.innerHTML === "<p>abc<span contenteditable=\"false\">def</span>ghi</p>") {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: ghi,
+ startOffset: 0,
+ endContainer: ghi,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else {
+ // If the non-editable `<span>` is deleted, it should be treated as
+ // an atomic node.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Backspace at "<p>abc<span contenteditable=\"false\">def</span>[]ghi</p>"');
+
+// If just removes the paragraph, target range should start from after the
+// table element.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell</td></tr></table><p><br></p>");
+ let table = gEditor.querySelector("table");
+ let p = table.nextSibling;
+ gSelection.collapse(p, 0);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td>cell</td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell</td></tr></tbody></table><p><br></p>",
+ ],
+ t.name
+ );
+ if (p.isConnected) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Backspace at "<table><tr><td>cell</td></tr></table><p>{}<br></p>"');
+
+// If table cell won't be joined, target range should be collapsed in the
+// cell.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td><br></td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = cell1.nextSibling;
+ gSelection.collapse(cell2, 0);
+ await sendBackspaceKey();
+ assert_equals(gEditor.innerHTML,
+ "<table><tbody><tr><td>cell1</td><td><br></td></tr></tbody></table>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: cell2,
+ startOffset: 0,
+ endContainer: cell2,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Backspace at "<table><tr><td>cell1</td><td>{}<br></td></tr></table>"');
+
+// If table caption won't be deleted, target range should be collapsed in the
+// caption element.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><table><caption><br></caption><tr><td>cell</td></tr></table>");
+ let caption = gEditor.querySelector("caption");
+ gSelection.collapse(caption, 0);
+ await sendBackspaceKey();
+ assert_equals(gEditor.innerHTML,
+ "<p>abc</p><table><caption><br></caption><tbody><tr><td>cell</td></tr></tbody></table>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: caption,
+ startOffset: 0,
+ endContainer: caption,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Backspace at "<p>abc</p><table><caption>{}<br></caption><tr><td>cell</td></tr></table>"');
+
+// If a table cell element is selected, only its content should be deleted.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let tr = cell1.parentNode;
+ gSelection.setBaseAndExtent(tr, 0, tr, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tr,
+ startOffset: 0,
+ endContainer: tr,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell1</td>}<td>cell2</td></tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr></table>");
+ let cell2 = gEditor.querySelector("td + td");
+ let tr = cell2.parentNode;
+ gSelection.setBaseAndExtent(tr, 1, tr, 2);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td>cell1</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell1</td><td><br></td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tr,
+ startOffset: 1,
+ endContainer: tr,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr><td>cell1</td>{<td>cell2</td>}</tr></table>"');
+
+// If the last table cell element is selected, what browsers should do?
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell</td></tr></table>");
+ let cell = gEditor.querySelector("td");
+ let tr = cell.parentNode;
+ let table = gEditor.querySelector("table");
+ gSelection.setBaseAndExtent(tr, 0, tr, 1);
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td></tr></tbody></table>",
+ "<br>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("table")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tr,
+ startOffset: 0,
+ endContainer: tr,
+ endOffset: 1,
+ });
+ } else {
+ // If it causes deleting entire the table, the `<table>` element should
+ // be in the target range.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell</td>}</tr></table>"');
+
+// Testing multiple cell selection mode.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell4.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr><tr><td>cell3</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr><tr><td>cell3</td><td><br></td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell1</td>}<td>cell2</td></tr><tr><td>cell3</td>{<td>cell4</td>}</tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell3 = gEditor.querySelector("tr + tr > td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell3.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell3);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr><tr><td></td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr><tr><td><br></td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell2</td></tr><tr><td>cell4</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell3 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 0,
+ endContainer: tr2,
+ endOffset: 1,
+ },
+ ]);
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell1</td>}<td>cell2</td></tr><tr>{<td>cell3</td>}<td>cell4</td></tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = gEditor.querySelector("td + td");
+ let tr1 = cell1.parentNode;
+ let tbody = tr1.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell2);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td></td></tr><tr><td>cell3</td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td><br></td></tr><tr><td>cell3</td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell3</td><td>cell4</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("tr + tr")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell2 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr1,
+ startOffset: 1,
+ endContainer: tr1,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tbody,
+ startOffset: 0,
+ endContainer: tbody,
+ endOffset: 1,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell1</td>}{<td>cell2</td>}</tr><tr><td>cell3</td><td>cell4</td></tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell3 = gEditor.querySelector("tr + tr > td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr2 = cell3.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell3);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td>cell1</td><td>cell2</td></tr><tr><td></td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell1</td><td>cell2</td></tr><tr><td><br></td><td><br></td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell1</td><td>cell2</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("tr + tr")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell3 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr2,
+ startOffset: 0,
+ endContainer: tr2,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tbody,
+ startOffset: 1,
+ endContainer: tbody,
+ endOffset: 2,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr><td>cell1</td><td>cell2</td></tr><tr>{<td>cell3</td>}{<td>cell4</td>}</tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = gEditor.querySelector("td + td");
+ let cell3 = gEditor.querySelector("tr + tr > td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell3.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell2);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell3);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 4, "Should support multiple cell selection");
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td></td></tr><tr><td></td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td><br></td></tr><tr><td><br></td><td><br></td></tr></tbody></table>",
+ "<br>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("table")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1, cell2, cell3 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr1,
+ startOffset: 1,
+ endContainer: tr1,
+ endOffset: 2,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 0,
+ endContainer: tr2,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell1</td>}{<td>cell2</td>}</tr><tr>{<td>cell3</td>}{<td>cell4</td>}</tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = gEditor.querySelector("td + td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell4.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.setStart(cell2.firstChild, 1);
+ range.setEnd(cell2.firstChild, 4);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 3, "Should support multiple cell selection");
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr><tr><td>cell3</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr><tr><td>cell3</td><td><br></td></tr></tbody></table>",
+ "<table><tbody><tr><td></td><td>c2</td></tr><tr><td>cell3</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>c2</td></tr><tr><td>cell3</td><td><br></td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (cell2.firstChild.length == "cell2".length) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: cell2.firstChild,
+ startOffset: 1,
+ endContainer: cell2.firstChild,
+ endOffset: 4,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Backspace at "<table><tr>{<td>cell1</td>}<td>c[ell]2</td></tr><tr>{<td>cell3</td>}<td>cell4</td></tr></table>"');
+
+// If caret is not adjacent of deleting character, browser may not delete the
+// character, but update the caret position for next deletion.
+promise_test(async (t) => {
+ initializeTest("<p>&#x5E9;&#x5DC;&#x5D5;&#x5DD;hello</p>");
+ let text1 = gEditor.querySelector("p").firstChild;
+ let text2 = text1.nextSibling;
+ gSelection.collapse(text2 ? text2 : text1, text2 ? 1 : 5);
+ await sendArrowLeftKey();
+ await sendBackspaceKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>\u05E9\u05DC\u05D5\u05DDhello</p>",
+ "<p>\u05DC\u05D5\u05DDhello</p>",
+ "<p>\u05E9\u05DC\u05D5hello</p>",
+ ],
+ t.name
+ );
+ if (gEditor.innerHTML === "<p>\u05E9\u05DC\u05D5\u05DDhello</p>") {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text2 ? text2 : text1,
+ startOffset: text2 ? 0 : 4,
+ endContainer: text2 ? text2 : text1,
+ endOffset: text2 ? 0 : 4,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else if (gEditor.innerHTML === "<p>\u05DC\u05D5\u05DDhello</p>") {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text1,
+ startOffset: 0,
+ endContainer: text1,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text1,
+ startOffset: 3,
+ endContainer: text1,
+ endOffset: 4,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Backspace at "<p>&#x5E9;&#x5DC;&#x5D5;&#x5DD;[]hello</p>"');
+
+// The following tests check whether the range returned from
+// `beforeinput[0].getTargetRanges()` is modified or different range is
+// modified instead. I.e., they don't test which type of deletion should
+// occur. Therefore, their result depends on browser's key bindings,
+// system settings and running OS.
+
+function getFirstDifferentOffset(currentString, originalString) {
+ for (let i = 0; i < currentString.length; i++) {
+ if (currentString.charAt(i) !== originalString.charAt(i) &&
+ (originalString.charAt(i) !== " " || !currentString.charAt("\u00A0"))) {
+ return i;
+ }
+ }
+ return currentString.length;
+}
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc def".length);
+ await sendBackspaceKey(kShift);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Shift + Backspace at "<p>abc def[] ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc def".length);
+ await sendBackspaceKey(kControl);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Control + Backspace at "<p>abc def[] ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc def".length);
+ await sendBackspaceKey(kAlt);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Alt + Backspace at "<p>abc def[] ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc def".length);
+ await sendBackspaceKey(kMeta);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Meta + Backspace at "<p>abc def[] ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p> ${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc".length);
+ await sendBackspaceKey(kShift);
+ let visibleText = p.firstChild.data.replace(/^\s+/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${invisibleWhiteSpaces + kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Shift + Backspace at "<p> abc[] def</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p> ${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc".length);
+ await sendBackspaceKey(kControl);
+ let visibleText = p.firstChild.data.replace(/^\s+/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${invisibleWhiteSpaces + kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Control + Backspace at "<p> abc[] def</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p> ${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc".length);
+ await sendBackspaceKey(kAlt);
+ let visibleText = p.firstChild.data.replace(/^\s+/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${invisibleWhiteSpaces + kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Alt + Backspace at "<p> abc[] def</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p> ${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc".length);
+ await sendBackspaceKey(kMeta);
+ let visibleText = p.firstChild.data.replace(/^\s+/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${invisibleWhiteSpaces + kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Meta + Backspace at "<p> abc[] def</p>"');
+
+// If editing host is nested, editing in the nested one shouldn't cause
+// target ranges extended to outside of it.
+promise_test(async t => {
+ const innerHTML = "<div contenteditable=\"false\"><div contenteditable=\"\"><p><br></p></div></div>";
+ initializeTest(innerHTML);
+ const p = gEditor.querySelector("p");
+ const innerEditingHost = p.parentNode;
+ document.activeElement?.blur();
+ p.parentNode.focus();
+ gSelection.collapse(p, 0);
+ await sendBackspaceKey();
+ if (gEditor.innerHTML == innerHTML) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else {
+ checkEditorContentResultAsSubTest(
+ "<div contenteditable=\"false\"><div contenteditable=\"\"><br></div></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: innerEditingHost,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Backspace at "<div contenteditable="false"><div contenteditable=""><p>{}<br></p></div></div>');
+
+promise_test(async t => {
+ const innerHTML = "<div contenteditable=\"false\">\n <div contenteditable=\"\"><p><br></p></div></div>";
+ initializeTest(innerHTML);
+ const p = gEditor.querySelector("p");
+ const innerEditingHost = p.parentNode;
+ document.activeElement?.blur();
+ p.parentNode.focus();
+ gSelection.collapse(p, 0);
+ await sendBackspaceKey();
+ if (gEditor.innerHTML == innerHTML) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else {
+ checkEditorContentResultAsSubTest(
+ "<div contenteditable=\"false\">\n <div contenteditable=\"\"><br></div></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: innerEditingHost,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Backspace at "<div contenteditable="false">\n <div contenteditable=""><p>{}<br></p></div></div>');
+
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html
new file mode 100644
index 0000000000..7c225cd051
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html
@@ -0,0 +1,1744 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<meta name="variant" content="?Backspace,ul">
+<meta name="variant" content="?Backspace,ol">
+<meta name="variant" content="?Delete,ul">
+<meta name="variant" content="?Delete,ol">
+<title>InputEvent.getTargetRanges() at deleting in/around/across list item elements</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";
+
+const [action, list] = location.search.substring(1).split(",");
+function run() {
+ switch (action) {
+ case "Backspace":
+ return sendBackspaceKey();
+ case "Delete":
+ return sendDeleteKey();
+ default:
+ throw "Unhandled variant";
+ }
+}
+
+/**
+ * @param innerHTML Initial `innerHTML` value of the editor.
+ * @param data
+ * expectedInnerHTML
+ * Expected `innerHTML` of the editor after calling
+ * `run()`. This can be array of string if there are
+ * some acceptable differences like whether there is
+ * an invisible `<br>` element at end of list item.
+ * expectedTargetRanges
+ * `null` or `unspecified` if `beforeinput` event shouldn't
+ * be fired.
+ * Otherwise, function returning an array of objects
+ * which have `startContainer`, `startOffset`,
+ * `endContainer`, `endOffset`. This will be called
+ * before calling `run()` and compared with
+ * `getTargetRanges()` after that.
+ * expectInputEvent:
+ * `true` if it should cause an `input` event.
+ */
+function addPromiseTest(innerHTML, data) {
+ promise_test(async (t) => {
+ initializeTest(innerHTML);
+ let expectedTargetRanges =
+ typeof data.expectedTargetRanges === "function"
+ ? data.expectedTargetRanges()
+ : null;
+ await run();
+ checkEditorContentResultAsSubTest(data.expectedInnerHTML, t.name);
+ if (expectedTargetRanges !== null) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedTargetRanges);
+ if (data.expectInputEvent) {
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ } else {
+ checkGetTargetRangesOfInputOnDoNothing();
+ }
+ } else {
+ checkBeforeinputAndInputEventsOnNOOP();
+ }
+ }, `${action} at "${innerHTML}"`);
+}
+
+addPromiseTest(
+ `<${list}><li>list[-item1</li><li>list]-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: "list".length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: "list".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-[item1</li><li>]list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: "list-".length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-[item1</li><li>}list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: "list-".length,
+ endContainer: gEditor.querySelector("li + li"),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1[</li><li>list]-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: "list".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1{</li><li>list]-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li"),
+ startOffset: 1,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: "list".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1[</li><li>]list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ action === "Backspace"
+ ? `<${list}><li>list-item1</li><li>[]list-item2</li></${list}>`
+ : `<${list}><li>list-item1[]</li><li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ action === "Backspace"
+ ? `<${list}><li>list-item1<br></li><li>[]list-item2</li></${list}>`
+ : `<${list}><li>list-item1[]<br></li><li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return action === "Backspace"
+ ? [
+ {
+ startContainer: gEditor.querySelector("li"),
+ startOffset: 1,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ]
+ : [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ action === "Backspace"
+ ? `<${list}><li>list-item1<br><br></li><li>[]list-item2</li></${list}>`
+ : `<${list}><li>list-item1[]<br><br></li><li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><li>list-item1<br>list-item2</li></${list}>`,
+ `<${list}><li>list-item1<br>list-item2<br></li></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return action === "Backspace"
+ ? [
+ {
+ startContainer: gEditor.querySelector("li"),
+ startOffset: 1,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ]
+ : [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ action === "Backspace"
+ ? `<${list}><li>list-item1</li><li>[]list-item2<br>second line of list-item2</li></${list}>`
+ : `<${list}><li>list-item1[]</li><li>list-item2<br>second line of list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li><li>second line of list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return action === "Backspace"
+ ? [
+ {
+ startContainer: gEditor.querySelector("li"),
+ startOffset: 1,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ]
+ : [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ action === "Backspace"
+ ? `<${list}><li><p>list-item1</p></li><li>[]list-item2</li></${list}>`
+ : `<${list}><li><p>list-item1[]</p></li><li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><p>list-item1list-item2</p></li></${list}>`,
+ expectedTargetRanges: () => {
+ return action === "Backspace"
+ ? [
+ {
+ startContainer: gEditor.querySelector("p").firstChild,
+ startOffset: gEditor.querySelector("p").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ]
+ : [
+ {
+ startContainer: gEditor.querySelector("p").firstChild,
+ startOffset: gEditor.querySelector("p").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ action === "Backspace"
+ ? `<${list}><li>list-item1</li><li><p>[]list-item2</p></li></${list}>`
+ : `<${list}><li>list-item1[]</li><li><p>list-item2</p></li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return action === "Backspace"
+ ? [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("p").firstChild,
+ endOffset: 0,
+ },
+ ]
+ : [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("p").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>[list-item1]</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: gEditor.querySelector("li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>{list-item1}</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: gEditor.querySelector("li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// Even if the last list item is selected, don't delete the list and
+// the last list item element. This is a triple click case on Gecko.
+addPromiseTest(
+ `<${list}>{<li>list-item1</li>}</${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: gEditor.querySelector("li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// A list item is selected and it's not the last one, can delete it.
+addPromiseTest(
+ `<${list}>{<li>list-item1</li>}<li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// Delete list element when deleting from empty last list item.
+addPromiseTest(
+ `<${list}><li>{}<br></li></${list}>`,
+ {
+ expectedInnerHTML: ["", "<br>", "<div><br></div>"],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `{<${list}><li><br></li></${list}>}`,
+ {
+ expectedInnerHTML: ["", "<br>", "<div><br></div>"],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li><br></li></${list}>}</div>`,
+ {
+ expectedInnerHTML: ["<div><br></div>", "<div><div><br></div></div>"],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("div"),
+ startOffset: 0,
+ endContainer: gEditor.querySelector("div"),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// It may be better to ignore the invisible white-space and take same action
+// as above, but it requires more expensive check before deleting. So perhaps,
+// this behavior is reasonable.
+addPromiseTest(
+ `<div>{ <${list}><li><br></li></${list}> }</div>`,
+ {
+ expectedInnerHTML: ["<div><br></div>", "<div><div><br></div></div>"],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("div"),
+ startOffset: 0,
+ endContainer: gEditor.querySelector("div"),
+ endOffset: 3,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div><${list}><li>{}<br></li></${list}></div>`,
+ {
+ expectedInnerHTML: ["<div><br></div>", "<div><div><br></div></div>"],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("div"),
+ startOffset: 0,
+ endContainer: gEditor.querySelector("div"),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// XXX Blink does not delete the list element if its first or last <li> element
+// is not editable. However, it means that user cannot delete the list
+// element, and it's not consistent behavior when only middle list item(s)
+// are not editable. Perhaps, once it makes the list element has only
+// one empty list item element, then, another deleting operation allows to
+// delete the list element.
+addPromiseTest(
+ `<div>{<${list}><li contenteditable="false"><br></li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(list),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(list),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li contenteditable="false">list-item1</li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(list),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(list),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li contenteditable="false">list-item1</li><li><br></li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(list),
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li"),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li contenteditable="false">list-item1</li><li>list-item2</li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(list),
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li><br></li><li contenteditable="false">list-item2</li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(list),
+ endOffset: 2,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li>list-item1</li><li contenteditable="false">list-item2</li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(list),
+ endOffset: 2,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li><br></li><li contenteditable="false">list-item2</li><li><br></li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li + li"),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<div>{<${list}><li>list-item1</li><li contenteditable="false">list-item2</li><li>list-item3</li></${list}>}</div>`,
+ {
+ expectedInnerHTML: `<div><${list}><li><br></li></${list}></div>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1</li>{<li>list-item2</li>}<li>list-item3</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li>list-item3</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 1,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 2,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// Selecting last list item element shouldn't delete the list item.
+addPromiseTest(
+ `<${list}><li>list-item1</li>{<li>list-item2</li>}</${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li + li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li + li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} > li + li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1</li><li>list-item2</li>{<li>list-item3</li>}</${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li>list-item2</li><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li + li + li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li + li + li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} > li + li + li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+for (let childList of ["ul", "ol"]) {
+ addPromiseTest(
+ `<${list}><li>list-item1</li>{<li>list-item2</li>}<li><${childList}><li><br></li></${childList}></li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li><${childList}><li><br></li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 1,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 2,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ // Invalid nested list elements cases. Treat the nested list element as a list item element.
+ addPromiseTest(
+ `<${list}><li>list-item1</li>{<li>list-item2</li>}<${childList}><li><br></li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><${childList}><li><br></li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 1,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 2,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1</li><li>list-item2</li>{<${childList}><li><br></li></${childList}>}</${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li>list-item2</li><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 2,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 3,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Don't delete list and joined list items when only there content are selected.
+addPromiseTest(
+ `<${list}><li>[list-item1</li><li>list-item2]</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>[list-item1</li><li>list-item2]</li><li>list-item3</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li><li>list-item3</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1</li><li>[list-item2]</li><li>list-item3</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li><br></li><li>list-item3</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li + li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+addPromiseTest(
+ `<${list}><li>list-item1</li><li>[list-item2</li><li>list-item3]</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li + li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li + li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+);
+
+// Ported tests from editing/delete.js and editing/forwarddelete.js
+for (let otherList of ["ul", "ol"]) {
+ if (action === "Backspace") {
+ addPromiseTest(
+ `<${otherList}><li>list-item1</li></${otherList}><${list}><li>l[]ist-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${otherList}><li>list-item1</li></${otherList}><${list}><li>ist-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${otherList} + ${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${otherList} + ${list} > li`).firstChild,
+ endOffset: "l".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1[]</li></${list}><${otherList}><li>list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item</li></${list}><${otherList}><li>list-item2</li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: "list-item".length,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: "list-item1".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+ } else {
+ addPromiseTest(
+ `<${list}><li>list-item[]1</li></${list}><${otherList}><li>list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item</li></${list}><${otherList}><li>list-item2</li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: "list-item".length,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: "list-item1".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${otherList}><li>list-item1</li></${otherList}><${list}><li>[]list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${otherList}><li>list-item1</li></${otherList}><${list}><li>ist-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${otherList} + ${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${otherList} + ${list} > li`).firstChild,
+ endOffset: "l".length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+ }
+
+ addPromiseTest(
+ `<${list}><li>list-item1[</li><li>list-item2]</li></${list}><${otherList}><li>list-item3</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li></${list}><${otherList}><li>ist-item3</li><li>ist-item4</li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector("li + li").firstChild,
+ endOffset: gEditor.querySelector("li + li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+
+// Invalid nested list element cases. Traditionally, all browser engines
+// insert child list element without wrapping it with a list item element.
+// So, keeping the behavior in these cases are important for backward
+// compatibility.
+// https://bugzilla.mozilla.org/show_bug.cgi?id=487524
+for (let childList of ["ul", "ol"]) {
+ addPromiseTest(
+ `<${list}><li>[list-item1</li><${childList}><li>}list-item2</li></ul></${list}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><${childList}><li>list-item2</li></${childList}></${list}>`,
+ `<${list}><${childList}><li>list-item2<br></li></${childList}></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li><${childList}><li>list-item2]</li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li><br></li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} > ${childList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>[list-item1</li></${childList}><li>}list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><${childList}><li>list-item2</li></${childList}></${list}>`,
+ `<${list}><${childList}><li>list-item2<br></li></${childList}></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>[list-item1</li></${childList}><li>list-item2]</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li><br></li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>list-item1</li><li>[list-item2</li></${childList}><li>}list-item3</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li>list-item1</li><li>list-item3</li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > ${childList} > li + li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li><${childList}><li>list-item2</li><li>}list-item3</li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><${childList}><li>list-item3</li></${childList}></${list}>`,
+ `<${list}><${childList}><li>list-item3<br></li></${childList}></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > ${childList} > li + li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1</li><li>[list-item2</li><${childList}><li>list-item3</li><li>}list-item4</li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><${childList}><li>list-item4</li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li + li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > ${childList} > li + li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ // Valid sub list element cases.
+ addPromiseTest(
+ `<${list}><li>[list-item1</li><li><${childList}><li>list-item2]</li></${childList}></li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><${childList}><li><br></li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li><${childList}><li>[list-item1</li></${childList}><li>}list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><li><${childList}><li>list-item2</li></${childList}></li></${list}>`,
+ `<${list}><li><${childList}><li>list-item2<br></li></${childList}></li></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li + li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li><${childList}><li>[list-item1</li></${childList}><li>list-item2]</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><${childList}><li><br></li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} > li + li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} > li + li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// When deleting the last list item in a sub list, only the list should
+// be removed. This makes users feel like doing outdent.
+for (let childList of ["ul", "ol"]) {
+ addPromiseTest(
+ `<${list}><li><${childList}><li>{}<br></li></${childList}></li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li`),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li`),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li><${childList}><li>[list-item1]</li></${childList}></li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><${childList}><li><br></li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li li`).firstChild,
+ endOffset: gEditor.querySelector(`li li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>{<${childList}><li>list-item1</li></${childList}>}</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><${childList}><li><br></li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li li`).firstChild,
+ endOffset: gEditor.querySelector(`li li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li><${childList}><li>{}<br></li></${childList}></li><li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li><li>list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li`),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li`),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1</li><li><${childList}><li>{}<br></li></${childList}></li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li + li`),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li + li`),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ // Invalid cases.
+ addPromiseTest(
+ `<${list}><${childList}><li>{}<br></li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>[list-item1]</li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li><br></li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li`).firstChild,
+ endOffset: gEditor.querySelector(`li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}>{<${childList}><li>list-item1</li></${childList}>}</${list}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li><br></li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`li`).firstChild,
+ endOffset: gEditor.querySelector(`li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>{}<br></li></${childList}><li>list-item2</li></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li><li>list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 1,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1</li><${childList}><li>{}<br></li></${childList}></${list}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list}`),
+ startOffset: 1,
+ endContainer: gEditor.querySelector(`${list}`),
+ endOffset: 2,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Joining same level list elements.
+for (let otherList of ["ul", "ol"]) {
+ addPromiseTest(
+ `<${list}><li>[list-item1</li></${list}><${otherList}><li>}list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><li>list-item2</li></${list}>`,
+ `<${list}><li>list-item2<br></li></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li></${list}><${otherList}><li>list-item2]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><li>}list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>first line in list-item1<br>list-item1[</li></${list}><${otherList}><li>}list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>first line in list-item1<br>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li > br`).nextSibling,
+ startOffset: gEditor.querySelector(`${list} > li > br`).nextSibling.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><li>}list-item2<br>second line in list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}><${otherList}><li>second line in list-item2</li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1</li><li>list-item2[</li></${list}><${otherList}><li>}list-item3</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1</li><li>list-item2list-item3</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li + li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li + li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><li>}list-item2</li><li>list-item3</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}><${otherList}><li>list-item3</li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Joining nested left list and right list element. Move the content in first line from selection end in the right
+// list item element into end of the left list item element.
+for (let childList of ["ul", "ol"]) {
+ for (let otherList of ["ul", "ol"]) {
+ addPromiseTest(
+ `<${list}><li><${childList}><li>[list-item1</li></${childList}></li></${list}><${otherList}><li>}list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><li><${childList}><li>list-item2</li></${childList}></li></${list}>`,
+ `<${list}><li><${childList}><li>list-item2<br></li></${childList}></li></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li><${childList}><li>[list-item1</li></${childList}></li></${list}><${otherList}><li>list-item2]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li><${childList}><li><br></li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li><${childList}><li>list-item1[</li></${childList}></li></${list}><${otherList}><li>list-item2]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li><${childList}><li>list-item1</li></${childList}></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li > ${childList} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ // Invalid cases.
+ addPromiseTest(
+ `<${list}><${childList}><li>[list-item1</li></${childList}></${list}><${otherList}><li>}list-item2</li></${otherList}>`,
+ {
+ expectedInnerHTML: [
+ `<${list}><${childList}><li>list-item2</li></${childList}></${list}>`,
+ `<${list}><${childList}><li>list-item2<br></li></${childList}></${list}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>[list-item1</li></${childList}></${list}><${otherList}><li>list-item2]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li><br></li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><${childList}><li>list-item1[</li></${childList}></${list}><${otherList}><li>list-item2]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><${childList}><li>list-item1</li></${childList}></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > ${childList} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+ }
+}
+
+// Joining left list and nested right list element. Basically, the first line from the selection end should
+// be moved into the end of the left list item element, but if all content in the left list is being deleted,
+// keep the right list elements.
+for (let childList of ["ul", "ol"]) {
+ for (let otherList of ["ul", "ol"]) {
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><li><${childList}><li>}list-item2</li></${childList}></li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li></${list}><${otherList}><li><${childList}><li>}list-item2</li></${childList}></li></${otherList}>`,
+ {
+ expectedInnerHTML: [
+ `<${otherList}><li><${childList}><li>list-item2</li></${childList}></li></${otherList}>`,
+ `<${otherList}><li><${childList}><li>list-item2<br></li></${childList}></li></${otherList}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li></${list}><${otherList}><li><${childList}><li>list-item2]</li></${childList}></li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li > ${childList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > li > ${childList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><li><${childList}><li>}list-item2<br>second line of list-item2</li></${childList}></li></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}><${otherList}><li><${childList}><li>second line of list-item2</li></${childList}></li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > li > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ // Invalid cases.
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><${childList}><li>}list-item2</li></${childList}></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li></${list}><${otherList}><${childList}><li>}list-item2</li></${childList}></${otherList}>`,
+ {
+ expectedInnerHTML: [
+ `<${otherList}><${childList}><li>list-item2</li></${childList}></${otherList}>`,
+ `<${otherList}><${childList}><li>list-item2<br></li></${childList}></${otherList}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>[list-item1</li></${list}><${otherList}><${childList}><li>list-item2]</li></${childList}></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li><br></li></${list}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > ${childList} > li`).firstChild,
+ endOffset: gEditor.querySelector(`${list} + ${otherList} > ${childList} > li`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${list}><li>list-item1[</li></${list}><${otherList}><${childList}><li>}list-item2<br>second line of list-item2</li></${childList}></${otherList}>`,
+ {
+ expectedInnerHTML: `<${list}><li>list-item1list-item2</li></${list}><${otherList}><${childList}><li>second line of list-item2</li></${childList}></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${list} > li`).firstChild,
+ startOffset: gEditor.querySelector(`${list} > li`).firstChild.length,
+ endContainer: gEditor.querySelector(`${list} + ${otherList} > ${childList} > li`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-range-across-editing-host-boundaries.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-range-across-editing-host-boundaries.tentative.html
new file mode 100644
index 0000000000..04f632a929
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-deleting-range-across-editing-host-boundaries.tentative.html
@@ -0,0 +1,598 @@
+<!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>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-during-and-after-dispatch.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-during-and-after-dispatch.tentative.html
new file mode 100644
index 0000000000..be4656d04a
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-during-and-after-dispatch.tentative.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>InputEvent.getTargetRanges() should return same array in various timings</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";
+
+// https://github.com/w3c/input-events/issues/114
+function checkGetTargetRangesKeepReturningSameValue(event, desc) {
+ assert_equals(
+ getArrayOfRangesDescription(event.getTargetRanges()),
+ getArrayOfRangesDescription(event.cachedRanges),
+ `getTargetRanges() of ${event.type} event should keep returning the same array of ranges ${desc}`);
+}
+
+promise_test(async () => {
+ initializeTest("<p>abc</p>");
+ let textNode = gEditor.firstChild.firstChild;
+ gSelection.collapse(textNode, 2);
+ await sendBackspaceKey();
+ assert_equals(gEditor.innerHTML, "<p>ac</p>", 'Should "b" be deleted');
+ checkGetTargetRangesKeepReturningSameValue(gBeforeinput[0], "even after its propagation");
+ checkGetTargetRangesKeepReturningSameValue(gInput[0], "even after its propagation");
+}, "Check consistency of getTargetRanges() result between during propagation and after propagation");
+
+promise_test(async () => {
+ initializeTest("<p>abc</p>");
+ let textNode = gEditor.firstChild.firstChild;
+ gSelection.collapse(textNode, 2);
+ gEditor.addEventListener("beforeinput", (event) => {
+ assert_false(typeof event.cachedRanges === "undefined", "The beforeinput event should have cache of getTargetRanges()");
+ gSelection.collapse(textNode, 3);
+ checkGetTargetRangesKeepReturningSameValue(event, "even after changing selection");
+ }, {once: true});
+ await sendBackspaceKey();
+ assert_equals(gEditor.innerHTML, "<p>ab</p>", 'Should "c" be deleted');
+}, "Check consistency of getTargetRanges() result between before and after changing selection in an event listener");
+
+promise_test(async () => {
+ initializeTest("<p>abc</p>");
+ let textNode = gEditor.firstChild.firstChild;
+ gSelection.collapse(textNode, 2);
+ window.addEventListener("beforeinput", (event) => {
+ assert_true(typeof event.cachedRanges === "undefined", "The beforeinput event shouldn't have cached ranges yet");
+ gSelection.collapse(textNode, 3);
+ }, {once: true, capture: true});
+ await sendBackspaceKey();
+ assert_equals(gEditor.innerHTML, "<p>ab</p>", 'Should "c" be deleted');
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: textNode,
+ startOffset: 1,
+ endContainer: textNode,
+ endOffset: 2,
+ });
+}, "The result of getTargetRanges() of beforeinput event should be fixed at being dispatched");
+
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-forwarddelete.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-forwarddelete.tentative.html
new file mode 100644
index 0000000000..aa282bb126
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-forwarddelete.tentative.html
@@ -0,0 +1,2209 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>InputEvent.getTargetRanges() at Delete (forward delete)</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";
+
+// Simply deletes the next ASCII character of caret position.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 2);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>ab</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 2,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>ab[]c</p>"');
+
+// Simply deletes the next ASCII character of caret position.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>ac</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 1,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>a[]bc</p>"');
+
+// Simply deletes the next ASCII character of caret position.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>bc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 0,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>[]abc</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ let abc = gEditor.querySelector("p").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: abc,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Delete at "<p>abc[]</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc<br></p>");
+ let abc = gEditor.querySelector("p").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc<br></p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: abc,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Delete at "<p>abc[]<br></p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p><img src="${kImgSrc}"><br></p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ `<p><img src="${kImgSrc}"><br></p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Delete at "<p><img>[]<br></p>"');
+
+// Should delete the `<span>` element because it becomes empty.
+// However, we need discussion whether the `<span>` element should be
+// contained by a range of `getTargetRanges()`.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>a<span>b</span>c</p>");
+ let a = gEditor.querySelector("span").previousSibling;
+ gSelection.collapse(a, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>ac</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: a,
+ startOffset: 1,
+ endContainer: gEditor.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>a[]<span>b</span>c</p>"');
+
+// Should delete the `<span>` element because it becomes empty.
+// However, we need discussion whether the `<span>` element should be
+// contained by a range of `getTargetRanges()`.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>a<span>b</span>c</p>");
+ let b = gEditor.querySelector("span").firstChild;
+ gSelection.collapse(b, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>ac</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild,
+ startOffset: 1,
+ endContainer: gEditor.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>a<span>[]b</span>c</p>"');
+
+// Invisible trailing white-space may be deleted when the last visible
+// character is deleted. If it's deleted, it should be contained by
+// the range of `getTargetRanges()`, but needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc </p>");
+ gSelection.collapse(gEditor.firstChild.firstChild, 2);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>ab</p>",
+ "<p>ab </p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild.firstChild,
+ startOffset: 2,
+ endContainer: gEditor.firstChild.firstChild,
+ endOffset: gEditor.firstChild.firstChild.data.length == 2 ? 4 : 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>ab[]c </p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p>def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]</p><p>def</p>"');
+
+// Invisible trailing white-spaces in current block and invisible leading
+// white-spaces in the following 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[] </p><p> def</p>"');
+
+// Invisible trailing white-spaces in current block and invisible leading
+// white-spaces in the following 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(abc, 4);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc [] </p><p> def</p>"');
+
+// Invisible trailing white-spaces in current block and invisible leading
+// white-spaces in the following 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(abc, 5);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc [] </p><p> def</p>"');
+
+// Invisible trailing white-spaces in current block and invisible leading
+// white-spaces in the following 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 (t) => {
+ initializeTest("<p>abc </p><p> def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(abc, 6);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc []</p><p> def</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc</p><p><b>def</b></p>");
+ let abc = gEditor.querySelector("p").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc<b>def</b></p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]</p><p><b>def</b></p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p><b>abc</b></p><p><b>def</b></p>");
+ let abc = gEditor.querySelector("p > b").firstChild;
+ let def = gEditor.querySelector("P + p > b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p><b>abc</b><b>def</b></p>",
+ "<p><b>abcdef</b></p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p><b>abc[]</b></p><p><b>def</b></p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p><i>abc</i></p><p><b>def</b></p>");
+ let abc = gEditor.querySelector("i").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p><i>abc</i><b>def</b></p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p><i>abc[]</i></p><p><b>def</b></p>"');
+
+// Invisible leading white-spaces in the following block should be deleted
+// for avoiding they becoming visible when the blocks are joined, but
+// preformatted trailing white-spaces in the first block shouldn't be
+// deleted. Perhaps, the invisible white-spaces should be contained by
+// the range of `getTargetRanges()`, but needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<pre>abc </pre><p> def</p>");
+ let pre = gEditor.firstChild;
+ let abc = pre.firstChild;
+ let p = pre.nextSibling;
+ let def = p.firstChild;
+ gSelection.collapse(abc, 6);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<pre>abc def</pre>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 6,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<pre>abc []</pre><p> def</p>"');
+
+// Invisible leading/trailing white-spaces in the following block should be
+// deleted for avoiding they becoming visible when the blocks are joined, but
+// preformatted trailing white-spaces in the first block shouldn't be
+// deleted. Perhaps, the invisible leading white-spaces should be contained
+// by the range of `getTargetRanges()`, but needs discussion.
+// And also not sure whether the trailing white-spaces should be contained
+// by additional range of `getTargetRanges()` or not because of the
+// implementation cost and runtime cost. Needs discuss.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<pre>abc </pre><p> def</p>");
+ let pre = gEditor.firstChild;
+ let abc = pre.firstChild;
+ let p = pre.nextSibling;
+ let def = p.firstChild;
+ gSelection.collapse(abc, 6);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<pre>abc def</pre>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 6,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<pre>abc []</pre><p> def </p>"');
+
+// Invisible trailing white-spaces in the first block should be deleted
+// when the block is joined with the preformatted following block, but
+// the leading white-spaces in the preformatted block shouldn't be
+// removed. So, in this case, the invisible trailing white-spaces should
+// be in the range of `getTargetRanges()`, but not so for the preformatted
+// visible leading white-spaces. But needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc </p><pre> def</pre>");
+ let p = gEditor.firstChild;
+ let abc = p.firstChild;
+ let pre = p.nextSibling;
+ let def = pre.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc &nbsp; def</p>",
+ "<p>abc&nbsp;&nbsp; def</p>",
+ "<p>abc&nbsp; &nbsp;def</p>",
+ "<p>abc &nbsp;&nbsp;def</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[] </p><pre> def</pre>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc\ndef</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc".length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc\n".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p style="white-space:pre-line">abc[]\\ndef</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc\n def</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc".length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc\n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p style="white-space:pre-line">abc[]\\n def</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc \ndef</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc".length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc \n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p style="white-space:pre-line">abc[] \\ndef</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc \n def</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc".length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abcdef</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc \n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p style="white-space:pre-line">abc[] \\n def</p>"');
+
+promise_test(async (t) => {
+ initializeTest('<p style="white-space:pre-line">abc \n \n def</p>');
+ const p = gEditor.firstChild;
+ const text = p.firstChild;
+ gSelection.collapse(text, "abc".length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ '<p style="white-space:pre-line">abc\n def</p>',
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text,
+ startOffset: "abc".length,
+ endContainer: text,
+ endOffset: "abc \n \n ".length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p style="white-space:pre-line">abc[] \\n \\n def</p>"');
+
+// Deleting from before invisible trailing `<br>` element of a block
+// should delete the `<br>` element and join the blocks. Therefore,
+// the target range should contain the `<br>` element and block boundaries.
+// But maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc<br></p><p>def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]<br></p><p>def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p><img src="${kImgSrc}"><br></p><p>def</p>`);
+ let p1 = gEditor.firstChild;
+ let img = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(p1, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(`<p><img src="${kImgSrc}">def</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `Delete at "<p><img>{}<br></p><p>def</p>"`);
+
+// Deleting from last empty line in the first block should delete the
+// invisible `<br>` element for the last empty line and join the blocks.
+// In this case, the invisible `<br>` element should be contained in the
+// range of `getTargetRanges()`, but needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc<br><br></p><p>def</p>");
+ let p1 = gEditor.firstChild;
+ let abc = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let def = p2.firstChild;
+ gSelection.collapse(p1, 2);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc<br>def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 2,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc<br>{}<br></p><p>def</p>"');
+
+// Deleting visible `<br>` element should be contained by a range of
+// `getTargetRanges()`.
+promise_test(async (t) => {
+ initializeTest("<p>abc<br>def</p>");
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]<br>def</p>"');
+
+// Deleting visible `<br>` element following white-space should not include
+// the preceding white-space in the range.
+promise_test(async (t) => {
+ initializeTest("<p>abc <br>def</p>");
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 4);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc []<br>def</p>"');
+
+// Deleting visible `<br>` element followed by white-space should include
+// the following white-space in the range because it shouldn't become
+// visible and should be deleted for avoiding it.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<p>abc<br> def</p>");
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ let def = gEditor.querySelector("br").nextSibling;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]<br> def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abcdef</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]<img>def</p>"');
+
+// White-spaces around `<img>` element are visible so that they shouldn't
+// be included into the target ranges.
+promise_test(async (t) => {
+ initializeTest(`<p>abc <img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 4);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc []<img>def</p>"');
+
+// White-spaces around `<img>` element are visible so that they shouldn't
+// be included into the target ranges.
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}"> def</p>`);
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc def</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]<img> def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ `<p>abc<img src="${kImgSrc}">def</p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc[]<img><img>def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p, 2);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ `<p>abc<img src="${kImgSrc}">def</p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 2,
+ endContainer: p,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>abc<img>{}<img>def</p>"');
+
+promise_test(async (t) => {
+ initializeTest(`<div>abc<hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let abc = div.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<hr>def</div>"');
+
+// White-spaces around block element are invisible white-spaces so that
+// they should be included into the target ranges to avoid they become
+// visible.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest(`<div>abc <hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let abc = div.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[] <hr>def</div>"');
+
+// White-spaces around block element are invisible white-spaces so that
+// they should be included into the target ranges to avoid they become
+// visible.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest(`<div>abc<hr> def</div>`);
+ let div = gEditor.querySelector("div");
+ let abc = div.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<hr> def</div>"');
+
+// Invisible `<br>` element immediately before `<hr>` element should be
+// delete once, and both of them should be included in the target range.
+promise_test(async (t) => {
+ initializeTest(`<div>abc<br><hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let abc = div.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdef</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<br><hr>def</div>"');
+
+promise_test(async (t) => {
+ initializeTest(`<div><img src="${kImgSrc}"><br><hr>def</div>`);
+ let div = gEditor.querySelector("div");
+ let img = div.firstChild;
+ gSelection.collapse(div, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ `<div><img src="${kImgSrc}">def</div>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: div,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><img>{}<br><hr>def</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<p>def<br>ghi</p></div>");
+ let p = gEditor.querySelector("p");
+ let def = p.firstChild;
+ let abc = gEditor.firstChild.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abcdef<p>ghi</p></div>",
+ "<div>abcdef<br><p>ghi</p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<p>def<br>ghi</p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<br><p>def<br>ghi</p></div>");
+ let div = gEditor.firstChild;
+ let p = gEditor.querySelector("p");
+ let def = p.firstChild;
+ let abc = div.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abcdef<p>ghi</p></div>",
+ "<div>abcdef<br><p>ghi</p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<br><p>def<br>ghi</p></div>"');
+
+promise_test(async (t) => {
+ initializeTest(`<div><img src="${kImgSrc}"><br><p>def<br>ghi</p></div>`);
+ let div = gEditor.firstChild;
+ let p = gEditor.querySelector("p");
+ let def = p.firstChild;
+ let abc = div.firstChild;
+ gSelection.collapse(div, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div><img src="${kImgSrc}">def<p>ghi</p></div>`,
+ `<div><img src="${kImgSrc}">def<br><p>ghi</p></div>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: div,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><img>{}<br><p>def<br>ghi</p></div>"');
+
+// Joining parent block and child block should remove invisible preceding
+// white-spaces of the child block and invisible leading white-spaces in
+// the child block, and they should be contained in a range of
+// `getTargetRanges()`, but maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<div>abc <p> def<br>ghi</p></div>");
+ let p = gEditor.querySelector("p");
+ let def = p.firstChild;
+ let abc = gEditor.firstChild.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abcdef<p>ghi</p></div>",
+ "<div>abcdef<br><p>ghi</p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[] <p> def<br>ghi</p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<p><b>def</b></p></div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abc<b>def</b></div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<p><b>def</b></p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><b>abc</b><p><b>def</b></p></div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("p > b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><b>abc</b><b>def</b></div>",
+ "<div><b>abcdef</b></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><b>abc[]</b><p><b>def</b></p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><i>abc</i><p><b>def</b></p></div>");
+ let abc = gEditor.querySelector("i").firstChild;
+ let def = gEditor.querySelector("b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div><i>abc</i><b>def</b></div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><i>abc[]</i><p><b>def</b></p></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p>abc</p>def</div>");
+ let abc = gEditor.querySelector("p").firstChild;
+ let def = gEditor.querySelector("p").nextSibling;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p>abcdef</p></div>",
+ "<div><p>abcdef<br></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p>abc[]</p>def</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p>abc<br></p>def</div>");
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ let def = p.nextSibling;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p>abcdef</p></div>",
+ "<div><p>abcdef<br></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p>abc[]<br></p>def</div>"');
+
+promise_test(async (t) => {
+ initializeTest(`<div><p><img src="${kImgSrc}"><br></p>def</div>`);
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ let def = p.nextSibling;
+ gSelection.collapse(p, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div><p><img src="${kImgSrc}">def</p></div>`,
+ `<div><p><img src="${kImgSrc}">def<br></p></div>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p><img>{}<br></p>def</div>"');
+
+// Joining child block and parent block should remove invisible trailing
+// white-spaces of the child block and invisible following white-spaces
+// in the parent block, and they should be contained by a range of
+// `getTargetRanges()`, but maybe needs discussion.
+// https://github.com/w3c/input-events/issues/112
+promise_test(async (t) => {
+ initializeTest("<div><p>abc </p> def</div>");
+ let abc = gEditor.querySelector("p").firstChild;
+ let def = gEditor.querySelector("p").nextSibling;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p>abcdef</p></div>",
+ "<div><p>abcdef<br></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p>abc[] </p> def</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p><b>abc</b></p>def</div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("p").nextSibling;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div><p><b>abc</b>def</p></div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p><b>abc[]</b></p>def</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p><b>abc</b></p><b>def</b></div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("div > b").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div><p><b>abc</b><b>def</b></p></div>",
+ "<div><p><b>abcdef</b></p></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p><b>abc[]</b></p><b>def</b></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div><p><b>abc</b></p><i>def</i></div>");
+ let abc = gEditor.querySelector("b").firstChild;
+ let def = gEditor.querySelector("i").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<div><p><b>abc</b><i>def</i></p></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div><p><b>abc[]</b></p><i>def</i></div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("li").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdefghi</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<ul><li>def</li></ul>ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("li").firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdefghi</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[] <ul><li> def </li></ul> ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let abc = gEditor.querySelector("div").firstChild;
+ let def = gEditor.querySelector("li").firstChild;
+ gSelection.collapse(abc, abc.length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<div>abcdefghi</div>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc []<ul><li> def </li></ul> ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
+ let def = gEditor.querySelector("li").firstChild;
+ let ghi = gEditor.querySelector("ul").nextSibling;
+ gSelection.collapse(def, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abc<ul><li>defghi</li></ul></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 3,
+ endContainer: ghi,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc<ul><li>def[]</li></ul>ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let def = gEditor.querySelector("li").firstChild;
+ let ghi = gEditor.querySelector("ul").nextSibling;
+ gSelection.collapse(def, 5);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abc <ul><li> defghi</li></ul></div>",
+ "<div>abc <ul><li>defghi</li></ul></div>",
+ "<div>abc<ul><li>defghi</li></ul></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 5,
+ endContainer: ghi,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc <ul><li> def[] </li></ul> ghi</div>"');
+
+promise_test(async (t) => {
+ initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
+ let def = gEditor.querySelector("li").firstChild;
+ let ghi = gEditor.querySelector("ul").nextSibling;
+ gSelection.collapse(def, def.length);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<div>abc <ul><li> defghi</li></ul></div>",
+ "<div>abc <ul><li>defghi</li></ul></div>",
+ "<div>abc<ul><li>defghi</li></ul></div>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 5,
+ endContainer: ghi,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc <ul><li> def []</li></ul> ghi</div>"');
+
+promise_test(async (t) => {
+ 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.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abcdef<ul><li>ghi</li></ul>jkl</div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc[]<ul><li>def</li><li>ghi</li></ul>jkl</div>"');
+
+promise_test(async (t) => {
+ 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.collapse(def, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abc<ul><li>defghi</li></ul>jkl</div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 3,
+ endContainer: ghi,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc<ul><li>def[]</li><li>ghi</li></ul>jkl</div>"');
+
+promise_test(async (t) => {
+ 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.collapse(ghi, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<div>abc<ul><li>def</li><li>ghijkl</li></ul></div>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: ghi,
+ startOffset: 3,
+ endContainer: jkl,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<div>abc<ul><li>def</li><li>ghi[]</li></ul>jkl</div>"');
+
+// Delete in empty paragraph should remove the empty paragraph. In this
+// case, it should be treated as joining with the previous paragraph.
+// The target range should include the invisible <br> element in the empty
+// paragraph.
+promise_test(async (t) => {
+ initializeTest("<p><br></p><p>abc</p>");
+ let p1 = gEditor.querySelector("p");
+ let p2 = p1.nextSibling;
+ let abc = p2.firstChild;
+ gSelection.collapse(p1, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 0,
+ endContainer: abc,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>{}<br></p><p>abc</p>"');
+
+// Delete ignore the empty span and the other things must be same as the
+// previous test.
+promise_test(async (t) => {
+ initializeTest("<p><span></span><br></p><p>abc</p>");
+ let p1 = gEditor.querySelector("p");
+ let span = p1.firstChild;
+ let p2 = p1.nextSibling;
+ let abc = p2.firstChild;
+ gSelection.collapse(span, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest("<p>abc</p>", t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 0,
+ endContainer: abc,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p><span>{}</span><br></p><p>abc</p>"');
+
+// If invisible white-spaces are removed with same action as above tests,
+// the range should be included in the target ranges.
+promise_test(async (t) => {
+ initializeTest("<p><br></p><p> abc</p>");
+ let p1 = gEditor.querySelector("p");
+ let p2 = p1.nextSibling;
+ let abc = p2.firstChild;
+ gSelection.collapse(p1, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p> abc</p>",
+ "<p>abc</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 0,
+ endContainer: abc,
+ endOffset: 5 - abc.length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>{}<br></p><p> abc</p>"');
+
+// If the next block begins with non-editable content, target range
+// should be at the non-editable content node.
+promise_test(async (t) => {
+ initializeTest("<p><br></p><p><span contenteditable=\"false\">abc</span>def</p>");
+ let p1 = gEditor.querySelector("p");
+ let p2 = p1.nextSibling;
+ let span = gEditor.querySelector("span");
+ gSelection.collapse(p1, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<p><span contenteditable=\"false\">abc</span>def</p>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 0,
+ endContainer: p2,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>{}<br></p><p><span contenteditable="false">abc</span>def</p>"');
+
+// If next non-editable paragraph is deleted, target range should end
+// with start of the text node in the last paragraph. Otherwise, ends at
+// the non-editable paragraph.
+promise_test(async (t) => {
+ initializeTest("<p><br></p><p contenteditable=\"false\">abc</p><p>def</p>");
+ let p1 = gEditor.querySelector("p");
+ let p2 = p1.nextSibling;
+ let p3 = p2.nextSibling;
+ let def = p3.firstChild;
+ gSelection.collapse(p3, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>def</p>",
+ "<p contenteditable=\"false\">abc</p><p>def</p>",
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p1,
+ startOffset: 0,
+ endContainer: p2.isConnected ? gEditor : p3,
+ endOffset: p2.isConnected ? 1 : 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<p>{}<br></p><p contenteditable=\"false\">abc</p><p>def</p>"');
+
+promise_test(async (t) => {
+ initializeTest("<p>abc<span contenteditable=\"false\">def</span>ghi</p>");
+ let p = gEditor.querySelector("p");
+ let abc = p.firstChild;
+ gSelection.collapse(abc, 3);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc<span contenteditable=\"false\">def</span>ghi</p>",
+ "<p>abcghi</p>",
+ "<p>abcghi<br></p>",
+ ],
+ t.name
+ );
+ if (gEditor.innerHTML === "<p>abc<span contenteditable=\"false\">def</span>ghi</p>") {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: abc,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else {
+ // If the non-editable `<span>` is deleted, it should be treated as
+ // an atomic node.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 1,
+ endContainer: p,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Delete at "<p>abc[]<span contenteditable=\"false\">def</span>ghi</p>"');
+
+// If just removes the paragraph, target range should end at the table element.
+promise_test(async (t) => {
+ initializeTest("<p><br></p><table><tr><td>cell</td></tr></table>");
+ let cell = gEditor.querySelector("td");
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td>cell</td></tr></tbody></table>",
+ "<p><br></p><table><tbody><tr><td>cell</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (p.isConnected) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Delete at "<p>{}<br></p><table><tr><td>cell</td></tr></table>"');
+
+// If table cell won't be joined, target range should be collapsed in the
+// cell.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td><br></td><td>cell2</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = cell1.nextSibling;
+ gSelection.collapse(cell1, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr></tbody></table>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: cell1,
+ startOffset: 0,
+ endContainer: cell1,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Delete at "<table><tr><td>{}<br></td><td>cell2</td></tr></table>"');
+
+// If table caption won't be deleted, target range should be collapsed in the
+// caption element.
+promise_test(async (t) => {
+ initializeTest("<table><caption><br></caption><tr><td>cell</td></tr></table>");
+ let caption = gEditor.querySelector("caption");
+ gSelection.collapse(caption, 0);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ "<table><caption><br></caption><tbody><tr><td>cell</td></tr></tbody></table>",
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: caption,
+ startOffset: 0,
+ endContainer: caption,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+}, 'Delete at "<table><caption>{}<br></caption><tr><td>cell</td></tr></table>"');
+
+// If a table cell element is selected, only its content should be deleted.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let tr = cell1.parentNode;
+ gSelection.setBaseAndExtent(tr, 0, tr, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tr,
+ startOffset: 0,
+ endContainer: tr,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell1</td>}<td>cell2</td></tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr></table>");
+ let cell2 = gEditor.querySelector("td + td");
+ let tr = cell2.parentNode;
+ gSelection.setBaseAndExtent(tr, 1, tr, 2);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td>cell1</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell1</td><td><br></td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tr,
+ startOffset: 1,
+ endContainer: tr,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr><td>cell1</td>{<td>cell2</td>}</tr></table>"');
+
+// If the last table cell element is selected, what browsers should do?
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell</td></tr></table>");
+ let cell = gEditor.querySelector("td");
+ let tr = cell.parentNode;
+ let table = gEditor.querySelector("table");
+ gSelection.setBaseAndExtent(tr, 0, tr, 1);
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td></tr></tbody></table>",
+ "<br>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("table")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tr,
+ startOffset: 0,
+ endContainer: tr,
+ endOffset: 1,
+ });
+ } else {
+ // If it causes deleting entire the table, the `<table>` element should
+ // be in the target range.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell</td>}</tr></table>"');
+
+// Testing multiple cell selection mode.
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell4.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr><tr><td>cell3</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr><tr><td>cell3</td><td><br></td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell1</td>}<td>cell2</td></tr><tr><td>cell3</td>{<td>cell4</td>}</tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell3 = gEditor.querySelector("tr + tr > td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell3.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell3);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr><tr><td></td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr><tr><td><br></td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell2</td></tr><tr><td>cell4</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell3 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 0,
+ endContainer: tr2,
+ endOffset: 1,
+ },
+ ]);
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell1</td>}<td>cell2</td></tr><tr>{<td>cell3</td>}<td>cell4</td></tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = gEditor.querySelector("td + td");
+ let tr1 = cell1.parentNode;
+ let tbody = tr1.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell2);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td></td></tr><tr><td>cell3</td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td><br></td></tr><tr><td>cell3</td><td>cell4</td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell3</td><td>cell4</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("tr + tr")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell2 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr1,
+ startOffset: 1,
+ endContainer: tr1,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tbody,
+ startOffset: 0,
+ endContainer: tbody,
+ endOffset: 1,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell1</td>}{<td>cell2</td>}</tr><tr><td>cell3</td><td>cell4</td></tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell3 = gEditor.querySelector("tr + tr > td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr2 = cell3.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell3);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 2, "Should support multiple cell selection");
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td>cell1</td><td>cell2</td></tr><tr><td></td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell1</td><td>cell2</td></tr><tr><td><br></td><td><br></td></tr></tbody></table>",
+ "<table><tbody><tr><td>cell1</td><td>cell2</td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("tr + tr")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell3 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr2,
+ startOffset: 0,
+ endContainer: tr2,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: tbody,
+ startOffset: 1,
+ endContainer: tbody,
+ endOffset: 2,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr><td>cell1</td><td>cell2</td></tr><tr>{<td>cell3</td>}{<td>cell4</td>}</tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = gEditor.querySelector("td + td");
+ let cell3 = gEditor.querySelector("tr + tr > td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell3.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell2);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell3);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 4, "Should support multiple cell selection");
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td></td></tr><tr><td></td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td><br></td></tr><tr><td><br></td><td><br></td></tr></tbody></table>",
+ "<br>",
+ ],
+ t.name
+ );
+ if (gEditor.querySelector("table")) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1, cell2, cell3 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr1,
+ startOffset: 1,
+ endContainer: tr1,
+ endOffset: 2,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 0,
+ endContainer: tr2,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor,
+ startOffset: 0,
+ endContainer: gEditor,
+ endOffset: 1,
+ });
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell1</td>}{<td>cell2</td>}</tr><tr>{<td>cell3</td>}{<td>cell4</td>}</tr></table>"');
+
+promise_test(async (t) => {
+ initializeTest("<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>");
+ let cell1 = gEditor.querySelector("td");
+ let cell2 = gEditor.querySelector("td + td");
+ let cell4 = gEditor.querySelector("tr + tr > td + td");
+ let tr1 = cell1.parentNode;
+ let tr2 = cell4.parentNode;
+ gSelection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell1);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.setStart(cell2.firstChild, 1);
+ range.setEnd(cell2.firstChild, 4);
+ gSelection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell4);
+ gSelection.addRange(range);
+ assert_equals(gSelection.rangeCount, 3, "Should support multiple cell selection");
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<table><tbody><tr><td></td><td>cell2</td></tr><tr><td>cell3</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>cell2</td></tr><tr><td>cell3</td><td><br></td></tr></tbody></table>",
+ "<table><tbody><tr><td></td><td>c2</td></tr><tr><td>cell3</td><td></td></tr></tbody></table>",
+ "<table><tbody><tr><td><br></td><td>c2</td></tr><tr><td>cell3</td><td><br></td></tr></tbody></table>",
+ ],
+ t.name
+ );
+ if (cell2.firstChild.length == "cell2".length) {
+ // XXX Perhaps, target range should be selecting only all children of
+ // cell1 and cell4 instead.
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething([
+ {
+ startContainer: tr1,
+ startOffset: 0,
+ endContainer: tr1,
+ endOffset: 1,
+ },
+ {
+ startContainer: cell2.firstChild,
+ startOffset: 1,
+ endContainer: cell2.firstChild,
+ endOffset: 4,
+ },
+ {
+ startContainer: tr2,
+ startOffset: 1,
+ endContainer: tr2,
+ endOffset: 2,
+ },
+ ]);
+ }
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Delete at "<table><tr>{<td>cell1</td>}<td>c[ell]2</td></tr><tr>{<td>cell3</td>}<td>cell4</td></tr></table>"');
+
+// If caret is not adjacent of deleting character, browser may not delete the
+// character, but update the caret position for next deletion.
+promise_test(async (t) => {
+ initializeTest("<p>hello&#x5E9;&#x5DC;&#x5D5;&#x5DD;</p>");
+ let text1 = gEditor.querySelector("p").firstChild;
+ let text2 = text1.nextSibling;
+ gSelection.collapse(text1, 4);
+ await sendArrowRightKey();
+ await sendDeleteKey();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>hello\u05E9\u05DC\u05D5\u05DD</p>",
+ "<p>hello\u05DC\u05D5\u05DD</p>",
+ "<p>hello\u05E9\u05DC\u05D5</p>",
+ ],
+ t.name
+ );
+ if (gEditor.innerHTML === "<p>hello\u05E9\u05DC\u05D5\u05DD</p>") {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text2 ? text2 : text1,
+ startOffset: text2 ? 0 : 5,
+ endContainer: text2 ? text2 : text1,
+ endOffset: text2 ? 0 : 5,
+ });
+ checkGetTargetRangesOfInputOnDoNothing();
+ } else if (gEditor.innerHTML === "<p>hello\u05DC\u05D5\u05DD</p>") {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text2 ? text2: text1,
+ startOffset: text2 ? 0 : 5,
+ endContainer: text2 ? text2 : text1,
+ endOffset: text2 ? 1 : 6,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ } else {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: text2 ? text2: text1,
+ startOffset: text2 ? 3 : 8,
+ endContainer: text2 ? text2 : text1,
+ endOffset: text2 ? 4 : 9,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ }
+}, 'Delete at "<p>hello[]&#x5E9;&#x5DC;&#x5D5;&#x5DD;</p>"');
+
+// The following tests check whether the range returned from
+// `beforeinput[0].getTargetRanges()` is modified or different range is
+// modified instead. I.e., they don't test which type of deletion should
+// occur. Therefore, their result depends on browser's key bindings,
+// system settings and running OS.
+
+function getFirstDifferentOffset(currentString, originalString) {
+ for (let i = 0; i < currentString.length; i++) {
+ if (currentString.charAt(i) !== originalString.charAt(i) &&
+ (originalString.charAt(i) !== " " || !currentString.charAt("\u00A0"))) {
+ return i;
+ }
+ }
+ return currentString.length;
+}
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kShift);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Shift + Delete at "<p>abc []def ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kControl);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Control + Delete at "<p>abc []def ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kAlt);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Alt + Delete at "<p>abc []def ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def ghi";
+ initializeTest(`<p>${kText}</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kMeta);
+ let startOffset = getFirstDifferentOffset(p.firstChild.data, kText);
+ let length = kText.length - p.firstChild.data.length;
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length)}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Meta + Delete at "<p>abc []def ghi</p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p>${kText} </p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kShift);
+ let visibleText = p.firstChild.data.replace(/%s+$/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length) + invisibleWhiteSpaces}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Shift + Delete at "<p>abc []def </p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p>${kText} </p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kControl);
+ let visibleText = p.firstChild.data.replace(/%s+$/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length) + invisibleWhiteSpaces}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Control + Delete at "<p>abc []def </p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p>${kText} </p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kAlt);
+ let visibleText = p.firstChild.data.replace(/%s+$/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length) + invisibleWhiteSpaces}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Alt + Delete at "<p>abc []def </p>"');
+
+promise_test(async (t) => {
+ const kText = "abc def";
+ initializeTest(`<p>${kText} s</p>`);
+ let p = gEditor.querySelector("p");
+ gSelection.collapse(p.firstChild, "abc ".length);
+ await sendDeleteKey(kMeta);
+ let visibleText = p.firstChild.data.replace(/%s+$/, "");
+ let invisibleWhiteSpaces = " ".repeat(p.firstChild.data.length - visibleText.length);
+ let startOffset = invisibleWhiteSpaces.length + getFirstDifferentOffset(visibleText, kText);
+ let length = kText.length + 3 - p.firstChild.data.length;
+ // If invisible white-spaces are deleted, they should be contained in the target range.
+ checkEditorContentResultAsSubTest(
+ `<p>${kText.substr(0, startOffset) + kText.substr(startOffset + length) + invisibleWhiteSpaces}</p>`,
+ t.name,
+ { ignoreWhiteSpaceDifference: true }
+ );
+ if (startOffset === kText.length) {
+ checkBeforeinputAndInputEventsOnNOOP();
+ return;
+ }
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p.firstChild,
+ startOffset: startOffset,
+ endContainer: p.firstChild,
+ endOffset: startOffset + length,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, 'Meta + Delete at "<p>abc []def</p>"');
+
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-element-and-another-list.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-element-and-another-list.tentative.html
new file mode 100644
index 0000000000..cebddac4c5
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-element-and-another-list.tentative.html
@@ -0,0 +1,285 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<meta name="variant" content="?Backspace">
+<meta name="variant" content="?Delete">
+<title>InputEvent.getTargetRanges() at joining dl element and ol or ul element</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";
+
+const action = location.search.substring(1);
+function run() {
+ switch (action) {
+ case "Backspace":
+ return sendBackspaceKey();
+ case "Delete":
+ return sendDeleteKey();
+ default:
+ throw "Unhandled variant";
+ }
+}
+
+/**
+ * @param innerHTML Initial `innerHTML` value of the editor.
+ * @param data
+ * expectedInnerHTML
+ * Expected `innerHTML` of the editor after calling
+ * `run()`. This can be array of string if there are
+ * some acceptable differences like whether there is
+ * an invisible `<br>` element at end of list item.
+ * expectedTargetRanges
+ * `null` or `unspecified` if `beforeinput` event shouldn't
+ * be fired.
+ * Otherwise, function returning an array of objects
+ * which have `startContainer`, `startOffset`,
+ * `endContainer`, `endOffset`. This will be called
+ * before calling `run()` and compared with
+ * `getTargetRanges()` after that.
+ * expectInputEvent:
+ * `true` if it should cause an `input` event.
+ */
+function addPromiseTest(innerHTML, data) {
+ promise_test(async (t) => {
+ initializeTest(innerHTML);
+ let expectedTargetRanges =
+ typeof data.expectedTargetRanges === "function"
+ ? data.expectedTargetRanges()
+ : null;
+ await run();
+ checkEditorContentResultAsSubTest(data.expectedInnerHTML, t.name);
+ if (expectedTargetRanges !== null) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedTargetRanges);
+ if (data.expectInputEvent) {
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ } else {
+ checkGetTargetRangesOfInputOnDoNothing();
+ }
+ } else {
+ checkBeforeinputAndInputEventsOnNOOP();
+ }
+ }, `${action} at "${innerHTML}"`);
+}
+
+// TODO: This file does not have tests for the cases joining dl elements and
+// parent or child ul/ol elements. They should be added, but perhaps,
+// they are edge cases.
+
+// Joining dl with ul/ol list element
+for (let otherList of ["ul", "ol"]) {
+ for (let firstItem of ["dt", "dd"]) {
+ addPromiseTest(
+ action === "Backspace"
+ ? `<${otherList}><li>list-item1</li></${otherList}><dl><${firstItem}>[]list-item2</${firstItem}></dl>`
+ : `<${otherList}><li>list-item1[]</li></${otherList}><dl><${firstItem}>list-item2</${firstItem}></dl>`,
+ {
+ expectedInnerHTML: `<${otherList}><li>list-item1list-item2</li></${otherList}>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector(firstItem).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ for (let secondItem of ["dt", "dd"]) {
+ addPromiseTest(
+ action === "Backspace"
+ ? `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}}>list-item2</${secondItem}></dl><${otherList}><li>[]list-item3</li></${otherList}>`
+ : `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}>list-item2[]</${secondItem}></dl><${otherList}><li>list-item3</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}>list-item2list-item3</${secondItem}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${firstItem} + ${secondItem}`).firstChild,
+ startOffset: gEditor.querySelector(`${firstItem} + ${secondItem}`).firstChild.length,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}>[list-item2</${secondItem}></dl><${otherList}><li>}list-item3</li></${otherList}>`,
+ {
+ expectedInnerHTML:
+ secondItem === "dt"
+ ? [
+ `<dl><${firstItem}>list-item1</${firstItem}></dl><${otherList}><li>list-item3</li></${otherList}>`,
+ `<dl><${firstItem}>list-item1</${firstItem}></dl><${otherList}><li>list-item3<br></li></${otherList}>`,
+ ]
+ : [
+ `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}>list-item3</${secondItem}></dl>`,
+ `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}>list-item3<br></${secondItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${firstItem} + ${secondItem}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li"),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}>[list-item2</${secondItem}></dl><${otherList}><li>list-item3]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<dl><${firstItem}>list-item1</${firstItem}><${secondItem}><br></${secondItem}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(secondItem).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: gEditor.querySelector("li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${firstItem}>[list-item1</${firstItem}><${secondItem}>list-item2</${secondItem}></dl><${otherList}><li>list-item3]</li></${otherList}>`,
+ {
+ expectedInnerHTML: `<dl><${firstItem}><br></${firstItem}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(firstItem).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector("li").firstChild,
+ endOffset: gEditor.querySelector("li").firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ action === "Backspace"
+ ? `<${otherList}><li>list-item1</li></${otherList}><dl><${firstItem}>[]list-item2</${firstItem}><${secondItem}>list-item3</${secondItem}></dl>`
+ : `<${otherList}><li>list-item1[]</li></${otherList}><dl><${firstItem}>list-item2</${firstItem}><${secondItem}>list-item3</${secondItem}></dl>`,
+ {
+ expectedInnerHTML: `<${otherList}><li>list-item1list-item2</li></${otherList}><dl><${secondItem}>list-item3</${secondItem}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector(firstItem).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${otherList}><li>[list-item1</li></${otherList}><dl><${firstItem}>}list-item2</${firstItem}><${secondItem}>list-item3</${secondItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<${otherList}><li>list-item2</li></${otherList}><dl><${secondItem}>list-item3</${secondItem}></dl>`,
+ `<${otherList}><li>list-item2<br></li></${otherList}><dl><${secondItem}>list-item3</${secondItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(firstItem),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${otherList}><li>[list-item1</li></${otherList}><dl><${firstItem}>list-item2]</${firstItem}><${secondItem}>list-item3</${secondItem}></dl>`,
+ {
+ expectedInnerHTML: `<${otherList}><li><br></li></${otherList}><dl><${secondItem}>list-item3</${secondItem}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(firstItem).firstChild,
+ endOffset: gEditor.querySelector(firstItem).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${otherList}><li>list-item1[</li></${otherList}><dl><${firstItem}>}list-item2</${firstItem}><${secondItem}>list-item3</${secondItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<${otherList}><li>list-item1list-item2</li></${otherList}><dl><${secondItem}>list-item3</${secondItem}></dl>`,
+ `<${otherList}><li>list-item1list-item2<br></li></${otherList}><dl><${secondItem}>list-item3</${secondItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector(firstItem),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<${otherList}><li>list-item1[</li></${otherList}><dl><${firstItem}>list-item2</${firstItem}><${secondItem}>}list-item3</${secondItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<${otherList}><li>list-item1list-item3</li></${otherList}>`,
+ `<${otherList}><li>list-item1list-item3<br></li></${otherList}>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector("li").firstChild,
+ startOffset: gEditor.querySelector("li").firstChild.length,
+ endContainer: gEditor.querySelector(secondItem),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+ }
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-elements.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-elements.tentative.html
new file mode 100644
index 0000000000..3e51dab775
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-joining-dl-elements.tentative.html
@@ -0,0 +1,605 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<meta name="variant" content="?Backspace">
+<meta name="variant" content="?Delete">
+<title>InputEvent.getTargetRanges() at deleting in/around/across list item elements</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";
+
+const action = location.search.substring(1);
+function run() {
+ switch (action) {
+ case "Backspace":
+ return sendBackspaceKey();
+ case "Delete":
+ return sendDeleteKey();
+ default:
+ throw "Unhandled variant";
+ }
+}
+
+/**
+ * @param innerHTML Initial `innerHTML` value of the editor.
+ * @param data
+ * expectedInnerHTML
+ * Expected `innerHTML` of the editor after calling
+ * `run()`. This can be array of string if there are
+ * some acceptable differences like whether there is
+ * an invisible `<br>` element at end of list item.
+ * expectedTargetRanges
+ * `null` or `unspecified` if `beforeinput` event shouldn't
+ * be fired.
+ * Otherwise, function returning an array of objects
+ * which have `startContainer`, `startOffset`,
+ * `endContainer`, `endOffset`. This will be called
+ * before calling `run()` and compared with
+ * `getTargetRanges()` after that.
+ * expectInputEvent:
+ * `true` if it should cause an `input` event.
+ */
+function addPromiseTest(innerHTML, data) {
+ promise_test(async (t) => {
+ initializeTest(innerHTML);
+ let expectedTargetRanges =
+ typeof data.expectedTargetRanges === "function"
+ ? data.expectedTargetRanges()
+ : null;
+ await run();
+ checkEditorContentResultAsSubTest(data.expectedInnerHTML, t.name);
+ if (expectedTargetRanges !== null) {
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedTargetRanges);
+ if (data.expectInputEvent) {
+ checkGetTargetRangesOfInputOnDeleteSomething();
+ } else {
+ checkGetTargetRangesOfInputOnDoNothing();
+ }
+ } else {
+ checkBeforeinputAndInputEventsOnNOOP();
+ }
+ }, `${action} at "${innerHTML}"`);
+}
+
+// Join left list element and right list element, both have one item.
+function test_join_1_1(leftItem, rightItem) {
+ addPromiseTest(
+ action === "Backspace"
+ ? `<dl><${leftItem}>list-item1</${leftItem}></dl><dl><${rightItem}>[]list-item2</${rightItem}></dl>`
+ : `<dl><${leftItem}>list-item1[]</${leftItem}></dl><dl><${rightItem}>list-item2</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItem}>list-item1list-item2</${leftItem}></dl>`,
+ `<dl><${leftItem}>list-item1list-item2<br></${leftItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItem}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>list-item1[</${leftItem}></dl><dl><${rightItem}>}list-item2</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItem}>list-item1list-item2</${leftItem}></dl>`,
+ `<dl><${leftItem}>list-item1list-item2<br></${leftItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItem}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>[list-item1</${leftItem}></dl><dl><${rightItem}>}list-item2</${rightItem}></dl>`,
+ {
+ expectedInnerHTML:
+ leftItem === "dt" && rightItem === "dd"
+ ? [
+ `<dl><${rightItem}>list-item2</${rightItem}></dl>`,
+ `<dl><${rightItem}>list-item2<br></${rightItem}></dl>`,
+ ]
+ : [
+ `<dl><${leftItem}>list-item2</${leftItem}></dl>`,
+ `<dl><${leftItem}>list-item2<br></${leftItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>list-item1[</${leftItem}></dl><dl><${rightItem}>list-item2]</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItem}>list-item1</${leftItem}></dl>`,
+ `<dl><${leftItem}>list-item1<br></${leftItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>[list-item1</${leftItem}></dl><dl><${rightItem}>list-item2]</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItem}><br></${leftItem}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Join left list element which has 2 items and right list element which has one item.
+function test_join_2_1(leftItems, rightItem) {
+ addPromiseTest(
+ action === "Backspace"
+ ? `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl><dl><${rightItem}>[]list-item3</${rightItem}></dl>`
+ : `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2[]</${leftItems[1]}></dl><dl><${rightItem}>list-item3</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3</${leftItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3<br></${leftItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2[</${leftItems[1]}></dl><dl><${rightItem}>}list-item3</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3</${leftItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3<br></${leftItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>[list-item2</${leftItems[1]}></dl><dl><${rightItem}>}list-item3</${rightItem}></dl>`,
+ {
+ expectedInnerHTML:
+ leftItems[1] === "dt" && rightItem === "dd"
+ ? [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}></dl><dl><${rightItem}>list-item3</${rightItem}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}></dl><dl><${rightItem}>list-item3<br></${rightItem}></dl>`,
+ ]
+ : [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item3</${leftItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item3<br></${leftItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>[list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl><dl><${rightItem}>}list-item3</${rightItem}></dl>`,
+ {
+ expectedInnerHTML:
+ leftItems[0] === "dt" && rightItem === "dd"
+ ? [
+ `<dl><${rightItem}>list-item3</${rightItem}></dl>`,
+ `<dl><${rightItem}>list-item3<br></${rightItem}></dl>`,
+ ]
+ : [
+ `<dl><${leftItems[0]}>list-item3</${leftItems[0]}></dl>`,
+ `<dl><${leftItems[0]}>list-item3<br></${leftItems[0]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2[</${leftItems[1]}></dl><dl><${rightItem}>list-item3]</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>[list-item2</${leftItems[1]}></dl><dl><${rightItem}>list-item3]</${rightItem}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}><br></${leftItems[1]}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItem}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Join left list element which has one item and right list element which has 2 items.
+function test_join_1_2(leftItem, rightItems) {
+ addPromiseTest(
+ action === "Backspace"
+ ? `<dl><${leftItem}>list-item1</${leftItem}></dl><dl><${rightItems[0]}>[]list-item2</${rightItems[0]}><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`
+ : `<dl><${leftItem}>list-item1[]</${leftItem}></dl><dl><${rightItems[0]}>list-item2</${rightItems[0]}><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItem}>list-item1list-item2</${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ `<dl><${leftItem}>list-item1list-item2<br></${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItem}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>list-item1[</${leftItem}></dl><dl><${rightItems[0]}>}list-item2</${rightItems[0]}><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItem}>list-item1list-item2</${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ `<dl><${leftItem}>list-item1list-item2<br></${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItem}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>list-item1[</${leftItem}></dl><dl><${rightItems[0]}>list-item2]</${rightItems[0]}><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItem}>list-item1</${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ `<dl><${leftItem}>list-item1<br></${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}</dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItem}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>[list-item1</${leftItem}></dl><dl><${rightItems[0]}>list-item2]</${rightItems[0]}><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItem}><br></${leftItem}></dl><dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItem}>[list-item1</${leftItem}></dl><dl><${rightItems[0]}>list-item2</${rightItems[0]}><${rightItems[1]}>}list-item3</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML:
+ leftItem === "dt" && rightItems[1] === "dd"
+ ? [
+ `<dl><${rightItems[1]}>list-item3</${rightItems[1]}></dl>`,
+ `<dl><${rightItems[1]}>list-item3<br></${rightItems[1]}></dl>`,
+ ]
+ : [
+ `<dl><${leftItem}>list-item3</${leftItem}></dl>`,
+ `<dl><${leftItem}>list-item3<br></${leftItem}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItem}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]} + ${rightItems[1]}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Join left list element and right list element, both have 2 items.
+function test_join_2_2(leftItems, rightItems) {
+ addPromiseTest(
+ action === "Backspace"
+ ? `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl><dl><${rightItems[0]}>[]list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`
+ : `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2[]</${leftItems[1]}></dl><dl><${rightItems[0]}>list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3</${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3<br></${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild,
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2[</${leftItems[1]}></dl><dl><${rightItems[0]}>}list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3</${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2list-item3<br></${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ // XXX This and next one's expectation come from Blink's behavior.
+ // I'm not sure whether this is intentional complicated handling
+ // or not.
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>[list-item2</${leftItems[1]}></dl><dl><${rightItems[0]}>}list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML:
+ leftItems[1] === "dt" && rightItems[0] === "dd"
+ ? [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}></dl><dl><${rightItems[0]}>list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}></dl><dl><${rightItems[0]}>list-item3<br></${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ ]
+ : [
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item3</${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item3<br></${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>[list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl><dl><${rightItems[0]}>}list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML:
+ leftItems[0] === "dt" && rightItems[0] === "dd"
+ ? [
+ `<dl><${rightItems[0]}>list-item3</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ `<dl><${rightItems[0]}>list-item3<br></${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ ]
+ : [
+ `<dl><${leftItems[0]}>list-item3</${leftItems[0]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ `<dl><${leftItems[0]}>list-item3<br></${leftItems[0]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ ],
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`),
+ endOffset: 0,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2[</${leftItems[1]}></dl><dl><${rightItems[0]}>list-item3]</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild.length,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}>[list-item2</${leftItems[1]}></dl><dl><${rightItems[0]}>list-item3]</${rightItems[0]}><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItems[0]}>list-item1</${leftItems[0]}><${leftItems[1]}><br></${leftItems[1]}></dl><dl><${rightItems[1]}>list-item4</${rightItems[1]}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]} + ${leftItems[1]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItems[0]}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+
+ addPromiseTest(
+ `<dl><${leftItems[0]}>[list-item1</${leftItems[0]}><${leftItems[1]}>list-item2</${leftItems[1]}></dl><dl><${rightItems[0]}>list-item3</${rightItems[0]}><${rightItems[1]}>list-item4]</${rightItems[1]}></dl>`,
+ {
+ expectedInnerHTML: `<dl><${leftItems[0]}><br></${leftItems[0]}></dl>`,
+ expectedTargetRanges: () => {
+ return [
+ {
+ startContainer: gEditor.querySelector(`${leftItems[0]}`).firstChild,
+ startOffset: 0,
+ endContainer: gEditor.querySelector(`dl + dl > ${rightItems[0]} + ${rightItems[1]}`).firstChild,
+ endOffset: gEditor.querySelector(`dl + dl > ${rightItems[0]} + ${rightItems[1]}`).firstChild.length,
+ },
+ ];
+ },
+ expectInputEvent: true,
+ }
+ );
+}
+
+// Joining dl elements
+for (let listItem1 of ["dt", "dd"]) {
+ for (let listItem2 of ["dt", "dd"]) {
+ test_join_1_1(listItem1, listItem2);
+ for (let listItem3 of ["dt", "dd"]) {
+ test_join_2_1([listItem1, listItem2], listItem3);
+ test_join_1_2(listItem1, [listItem2, listItem3]);
+ for (let listItem4 of ["dt", "dd"]) {
+ test_join_2_2([listItem1, listItem2], [listItem3, listItem4]);
+ }
+ }
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html
new file mode 100644
index 0000000000..b8732327da
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html
@@ -0,0 +1,741 @@
+<!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" : "";
+
+// If text node is selected, target range should be shrunken to the edge of
+// text node.
+promise_test(async (t) => {
+ initializeTest("<p>abc</p>");
+ let p = gEditor.firstChild;
+ let abc = p.firstChild;
+ gSelection.setBaseAndExtent(p, 0, p, 1);
+ await run();
+ checkEditorContentResultAsSubTest(
+ `<p>${insertedHTML !== "" ? insertedHTML : "<br>"}</p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 0,
+ endContainer: abc,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>{abc}</p>"`);
+
+promise_test(async (t) => {
+ initializeTest("<p>abc<br></p>");
+ let p = gEditor.firstChild;
+ let abc = p.firstChild;
+ gSelection.setBaseAndExtent(p, 0, p, 1);
+ await run();
+ checkEditorContentResultAsSubTest(
+ [
+ `<p>${insertedHTML !== "" ? insertedHTML : "<br>"}</p>`,
+ `<p>${insertedHTML}<br></p>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 0,
+ endContainer: abc,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>{abc}<br></p>"`);
+
+promise_test(async (t) => {
+ initializeTest(`<p><img src="${kImgSrc}"></p>`);
+ let p = gEditor.firstChild;
+ gSelection.setBaseAndExtent(p, 0, p, 1);
+ await run();
+ checkEditorContentResultAsSubTest(
+ `<p>${insertedHTML !== "" ? insertedHTML : "<br>"}</p>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>{<img>}</p>"`);
+
+promise_test(async (t) => {
+ initializeTest(`<p><img src="${kImgSrc}"><br></p>`);
+ let p = gEditor.firstChild;
+ gSelection.setBaseAndExtent(p, 0, p, 1);
+ await run();
+ checkEditorContentResultAsSubTest(
+ [
+ `<p>${insertedHTML !== "" ? insertedHTML : "<br>"}</p>`,
+ `<p>${insertedHTML}<br></p>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: p,
+ startOffset: 0,
+ endContainer: p,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>{<img>}<br></p>"`);
+
+promise_test(async (t) => {
+ initializeTest("<p> abc </p>");
+ let p = gEditor.firstChild;
+ let abc = p.firstChild;
+ gSelection.setBaseAndExtent(p, 0, p, 1);
+ await run();
+ checkEditorContentResultAsSubTest(
+ `<p>${insertedHTML !== "" ? insertedHTML : "<br>"}</p>`,
+ t.name,
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 0,
+ endContainer: abc,
+ endOffset: 5,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>{ abc }</p>"`);
+
+// 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>abc${insertedHTML}def</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>abc [</p><p>] def</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>ab${insertedHTML}ef</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 2,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>ab[c</p><p>d]ef</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>ab${insertedHTML}ef</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 2,
+ endContainer: def,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>ab[c </p><p> d]ef</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>ab${insertedHTML}def</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 2,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>ab[c </p><p>] def</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>abc${insertedHTML}def</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>abc [</p><p>] def</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>abc${insertedHTML}def</p>`, t.name);
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<p>abc${insertedHTML}def</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>abc[<img>]def</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<div>abc${insertedHTML}def</div>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc[<hr>]def</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<div>abc${insertedHTML}def</div>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc [<hr>]def</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<div>abc${insertedHTML}def</div>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc [<hr>] def</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(`<div>abc${insertedHTML}def</div>`, t.name);
+ 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 (t) => {
+ initializeTest("<p>abc<br>def</p>");
+ gSelection.setBaseAndExtent(gEditor.firstChild, 1, gEditor.firstChild, 2);
+ await run();
+ checkEditorContentResultAsSubTest(`<p>abc${insertedHTML}def</p>`, t.name);
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: gEditor.firstChild,
+ startOffset: 1,
+ endContainer: gEditor.firstChild,
+ endOffset: 2,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<p>abc{<br>}def</p>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div>abc${insertedHTML}def<p>ghi</p></div>`,
+ `<div>abc${insertedHTML}def<br><p>ghi</p></div>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc[<p>]def<br>ghi</p></div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div>abc${insertedHTML}def<p>ghi</p></div>`,
+ `<div>abc${insertedHTML}def<br><p>ghi</p></div>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc [<p>] def<br>ghi</p></div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div><p>abc${insertedHTML}def</p></div>`,
+ `<div><p>abc${insertedHTML}def<br></p></div>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div><p>abc[</p>]def</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div><p>abc${insertedHTML}def</p></div>`,
+ `<div><p>abc${insertedHTML}def<br></p></div>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 3,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div><p>abc [</p>] def</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc${insertedHTML}defghi</div>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc[<ul><li>]def</li></ul>ghi</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc${insertedHTML}defghi</div>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: abc,
+ startOffset: 3,
+ endContainer: def,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc [<ul><li>] def </li></ul> ghi</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc<ul><li>def${insertedHTML}ghi</li></ul></div>`,
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 3,
+ endContainer: ghi,
+ endOffset: 0,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc<ul><li>def[</li></ul>]ghi</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<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>`,
+ ],
+ t.name
+ );
+ checkGetTargetRangesOfBeforeinputOnDeleteSomething({
+ startContainer: def,
+ startOffset: 5,
+ endContainer: ghi,
+ endOffset: 1,
+ });
+ checkGetTargetRangesOfInputOnDeleteSomething();
+}, `${action} at "<div>abc <ul><li> def [</li></ul>] ghi</div>"`);
+
+promise_test(async (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc${insertedHTML}def<ul><li>ghi</li></ul>jkl</div>`,
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<div>abc${insertedHTML}ghijkl</div>`,
+ `<div>abc${insertedHTML}ghijkl<br></div>`,
+ ],
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc<ul><li>def${insertedHTML}ghi</li></ul>jkl</div>`,
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc<ul><li>def</li><li>ghi${insertedHTML}jkl</li></ul></div>`,
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<div>abc<ul><li>def${insertedHTML}jkl</li></ul></div>`,
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<p>abc${insertedHTML}</p>`,
+ `<p>abc${insertedHTML}<br></p>`,
+ ],
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ "<p>abc<span contenteditable=\"false\">def</span>ghi</p>",
+ `<p>abc${insertedHTML}ghi</p>`,
+ `<p>abc${insertedHTML}ghi<br></p>`,
+ ],
+ t.name
+ );
+ // 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ `<table><tbody><tr><td>ab${insertedHTML}</td><td>ef</td></tr></tbody></table>`,
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<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>`,
+ ],
+ t.name
+ );
+ 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 (t) => {
+ 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();
+ checkEditorContentResultAsSubTest(
+ [
+ `<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>`,
+ ],
+ t.name
+ );
+ 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>
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges.html b/testing/web-platform/tests/input-events/input-events-get-target-ranges.html
new file mode 100644
index 0000000000..bbb275d8d3
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>InputEvent.getTargetRanges() behavior</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<p>To manually run this test, please follow the steps below:<br/>
+1. Place caret at the end of 'hel<i>lo wo</i><b>rld</b>'.<br/>
+2. Press Ctrl-Backspace (Alt-Backspace on macOS) to delete word backwards.<br/>
+3. Place caret at the end of 'test2' => Press 'a' key.<br/>
+4. Select 'test2a' => Press 'b' key.<br/>
+5. Select 'b' => Bold text through context menu or Command-b on macOS.<br/>
+6. Place caret at the end of 'test3' => Press 'a' key => Press Backspace key.<br/>
+<br/>
+If a "PASS" result appears the test passes, otherwise it fails</p>
+<p id="test1_editable" contenteditable>hel<i>lo wo</i><b>rld</b></p>
+<p id="test2_editable" contenteditable>test2</p>
+<textarea id="test3_plain">test3</textarea>
+<script>
+function resolveWhen(condition) {
+ return new Promise((resolve, reject) => {
+ function tick() {
+ if (condition())
+ resolve();
+ else
+ requestAnimationFrame(tick.bind(this));
+ }
+ tick();
+ });
+}
+
+let modifier_key = "\uE009";
+if(navigator.platform.includes('Mac'))
+ modifier_key = "\uE03D";
+const commands = {
+ COPY: 'copy',
+ CUT: 'cut',
+ PASTE: 'paste',
+ SELECTALL: 'select all',
+ DELETEALL: 'delete all',
+ BOLD: 'bold',
+}
+const backspace = "\uE003";
+
+function clickOnTarget(target) {
+ return new test_driver.Actions()
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown()
+ .pointerUp()
+ .send();
+}
+
+function sendTextCommand(command) {
+ let command_key = "";
+ if(command == "copy")
+ command_key = "c";
+ else if (command == "cut")
+ command_key = "x";
+ else if (command == "paste")
+ command_key = "v";
+ else if (command == "select all")
+ command_key = "a";
+ else if (command == "delete all")
+ command_key = backspace;
+ else if (command == "bold")
+ command_key = "b";
+ return new test_driver.Actions()
+ .keyDown(modifier_key)
+ .keyDown(command_key)
+ .keyUp(command_key)
+ .keyUp(modifier_key)
+ .send();
+}
+
+function sendTextCommandAtTarget(target, command) {
+ return clickOnTarget(target).then(() => {
+ return sendTextCommand(command);
+ });
+}
+
+function addTextAtTarget(target, char) {
+ return test_driver.send_keys(target, char);
+}
+
+promise_test(async test => {
+ const test1_editable = document.getElementById('test1_editable');
+ let lastBeforeInput;
+ test1_editable.addEventListener('beforeinput', test.step_func(function() {
+ assert_equals(event.inputType, 'deleteWordBackward');
+ const ranges = event.getTargetRanges();
+ assert_equals(ranges.length, 1);
+ const range = ranges[0];
+ assert_true(range instanceof StaticRange);
+ assert_equals(range.startOffset, 3);
+ assert_equals(range.startContainer.textContent, 'lo wo');
+ assert_equals(range.endOffset, 3);
+ assert_equals(range.endContainer.textContent, 'rld');
+ assert_equals(test1_editable.innerHTML, 'hel<i>lo wo</i><b>rld</b>');
+ lastBeforeInput = event;
+ }));
+
+ test1_editable.addEventListener('input', test.step_func(function() {
+ assert_equals(event.inputType, 'deleteWordBackward');
+ assert_equals(test1_editable.innerHTML, 'hel<i>lo&nbsp;</i>');
+ assert_equals(lastBeforeInput.inputType, 'deleteWordBackward');
+ assert_equals(lastBeforeInput.getTargetRanges().length, 0,
+ 'getTargetRanges() should be empty after the event has finished dispatching.');
+ }));
+
+ await sendTextCommandAtTarget(test1_editable, commands.DELETEALL);
+ await resolveWhen(() => { return test1_editable.innerHTML == 'hel<i>lo&nbsp;</i>' });
+}, 'getTargetRanges() returns correct range and cleared after dispatch.');
+
+promise_test(async test => {
+ const expectedEventLog = ['test2-5-test2-5', 'test2a-0-test2a-6', 'b-0-b-1'];
+ const actualEventLog = [];
+
+ const test2_editable = document.getElementById('test2_editable');
+ test2_editable.addEventListener('beforeinput', test.step_func(function() {
+ const ranges = event.getTargetRanges();
+ assert_equals(ranges.length, 1);
+ const range = ranges[0];
+ actualEventLog.push(
+ `${range.startContainer.textContent}-${range.startOffset}-${range.endContainer.textContent}-${range.endOffset}`);
+ }));
+
+ await addTextAtTarget(test2_editable, "a");
+ await sendTextCommandAtTarget(test2_editable, commands.SELECTALL);
+ await addTextAtTarget(test2_editable, "b");
+ await sendTextCommandAtTarget(test2_editable, commands.SELECTALL);
+ await sendTextCommand(commands.BOLD);
+ await resolveWhen(() => { return actualEventLog.length == expectedEventLog.length });
+ assert_array_equals(actualEventLog, expectedEventLog,
+ `Expected: ${expectedEventLog}; Actual: ${actualEventLog}.`);
+}, 'Actions other than deletion should have current selection as target ranges.');
+
+promise_test(async test => {
+ const test3_plain = document.getElementById('test3_plain');
+ let event_type;
+ test3_plain.addEventListener('beforeinput', test.step_func(function() {
+ assert_equals(event.getTargetRanges().length, 0,
+ 'getTargetRanges() should return empty array on textarea.');
+
+ if (event.inputType === 'deleteContentBackward')
+ event_type = event.inputType;
+ }));
+
+ await addTextAtTarget(test3_plain, "a");
+ await addTextAtTarget(test3_plain, backspace);
+ await resolveWhen(() => { return event_type == 'deleteContentBackward' });
+}, 'Textarea should have empty target range.');
+</script>
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 0000000000..004416ec2a
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges.js
@@ -0,0 +1,518 @@
+"use strict";
+
+// TODO: extend `EditorTestUtils` in editing/include/edit-test-utils.mjs
+
+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, gEditor, gBeforeinput, gInput;
+
+function initializeTest(aInnerHTML) {
+ function onBeforeinput(event) {
+ // 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.
+ event.cachedRanges = event.getTargetRanges();
+ gBeforeinput.push(event);
+ }
+ function onInput(event) {
+ event.cachedRanges = event.getTargetRanges();
+ gInput.push(event);
+ }
+ if (gEditor !== document.querySelector("div[contenteditable]")) {
+ if (gEditor) {
+ gEditor.isListeningToInputEvents = false;
+ gEditor.removeEventListener("beforeinput", onBeforeinput);
+ gEditor.removeEventListener("input", onInput);
+ }
+ gEditor = document.querySelector("div[contenteditable]");
+ }
+ gSelection = getSelection();
+ gBeforeinput = [];
+ gInput = [];
+ if (!gEditor.isListeningToInputEvents) {
+ gEditor.isListeningToInputEvents = true;
+ gEditor.addEventListener("beforeinput", onBeforeinput);
+ gEditor.addEventListener("input", onInput);
+ }
+
+ setupEditor(aInnerHTML);
+ gBeforeinput = [];
+ gInput = [];
+}
+
+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 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 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 checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRanges) {
+ assert_equals(
+ gBeforeinput.length,
+ 1,
+ "One beforeinput event should be fired if the key operation tries to delete something"
+ );
+ assert_true(
+ Array.isArray(gBeforeinput[0].cachedRanges),
+ "gBeforeinput[0].getTargetRanges() should return an array of StaticRange instances at least during propagation"
+ );
+ let arrayOfExpectedRanges = Array.isArray(expectedRanges)
+ ? expectedRanges
+ : [expectedRanges];
+ // Before checking the length of array of ranges, we should check the given
+ // range first because the ranges are more important than whether there are
+ // redundant additional unexpected ranges.
+ for (
+ let i = 0;
+ i <
+ Math.max(arrayOfExpectedRanges.length, gBeforeinput[0].cachedRanges.length);
+ i++
+ ) {
+ assert_equals(
+ getRangeDescription(gBeforeinput[0].cachedRanges[i]),
+ getRangeDescription(arrayOfExpectedRanges[i]),
+ `gBeforeinput[0].getTargetRanges()[${i}] should return expected range (inputType is "${gBeforeinput[0].inputType}")`
+ );
+ }
+ assert_equals(
+ gBeforeinput[0].cachedRanges.length,
+ arrayOfExpectedRanges.length,
+ `getTargetRanges() of beforeinput event should return ${arrayOfExpectedRanges.length} ranges`
+ );
+}
+
+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 at least during propagation"
+ );
+ assert_equals(
+ gInput[0].cachedRanges.length,
+ 0,
+ "gInput[0].getTargetRanges() should return empty array during propagation"
+ );
+}
+
+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"
+ );
+}
+
+function checkEditorContentResultAsSubTest(
+ expectedResult,
+ description,
+ options = {}
+) {
+ test(() => {
+ if (Array.isArray(expectedResult)) {
+ assert_in_array(
+ options.ignoreWhiteSpaceDifference
+ ? gEditor.innerHTML.replace(/&nbsp;/g, " ")
+ : gEditor.innerHTML,
+ expectedResult
+ );
+ } else {
+ assert_equals(
+ options.ignoreWhiteSpaceDifference
+ ? gEditor.innerHTML.replace(/&nbsp;/g, " ")
+ : gEditor.innerHTML,
+ expectedResult
+ );
+ }
+ }, `${description} - comparing innerHTML`);
+}
+
+// Similar to `setupDiv` in editing/include/tests.js, this method sets
+// innerHTML value of gEditor, and sets multiple selection ranges specified
+// with the markers.
+// - `[` specifies start boundary in a text node
+// - `{` specifies start boundary before a node
+// - `]` specifies end boundary in a text node
+// - `}` specifies end boundary after a node
+function setupEditor(innerHTMLWithRangeMarkers) {
+ const startBoundaries = innerHTMLWithRangeMarkers.match(/\{|\[/g) || [];
+ const endBoundaries = innerHTMLWithRangeMarkers.match(/\}|\]/g) || [];
+ if (startBoundaries.length !== endBoundaries.length) {
+ throw "Should match number of open/close markers";
+ }
+
+ gEditor.innerHTML = innerHTMLWithRangeMarkers;
+ gEditor.focus();
+
+ if (startBoundaries.length === 0) {
+ // Don't remove the range for now since some tests may assume that
+ // setting innerHTML does not remove all selection ranges.
+ return;
+ }
+
+ function getNextRangeAndDeleteMarker(startNode) {
+ function getNextLeafNode(node) {
+ function inclusiveDeepestFirstChildNode(container) {
+ while (container.firstChild) {
+ container = container.firstChild;
+ }
+ return container;
+ }
+ if (node.hasChildNodes()) {
+ return inclusiveDeepestFirstChildNode(node);
+ }
+ if (node.nextSibling) {
+ return inclusiveDeepestFirstChildNode(node.nextSibling);
+ }
+ let nextSibling = (function nextSiblingOfAncestorElement(child) {
+ for (
+ let parent = child.parentElement;
+ parent && parent != gEditor;
+ parent = parent.parentElement
+ ) {
+ if (parent.nextSibling) {
+ return parent.nextSibling;
+ }
+ }
+ return null;
+ })(node);
+ if (!nextSibling) {
+ return null;
+ }
+ return inclusiveDeepestFirstChildNode(nextSibling);
+ }
+ function scanMarkerInTextNode(textNode, offset) {
+ return /[\{\[\]\}]/.exec(textNode.data.substr(offset));
+ }
+ let startMarker = (function scanNextStartMaker(
+ startContainer,
+ startOffset
+ ) {
+ function scanStartMakerInTextNode(textNode, offset) {
+ let scanResult = scanMarkerInTextNode(textNode, offset);
+ if (scanResult === null) {
+ return null;
+ }
+ if (scanResult[0] === "}" || scanResult[0] === "]") {
+ throw "An end marker is found before a start marker";
+ }
+ return {
+ marker: scanResult[0],
+ container: textNode,
+ offset: scanResult.index + offset
+ };
+ }
+ if (startContainer.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanStartMakerInTextNode(startContainer, startOffset);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ }
+ let nextNode = startContainer;
+ while ((nextNode = getNextLeafNode(nextNode))) {
+ if (nextNode.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanStartMakerInTextNode(nextNode, 0);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ continue;
+ }
+ }
+ return null;
+ })(startNode, 0);
+ if (startMarker === null) {
+ return null;
+ }
+ let endMarker = (function scanNextEndMarker(startContainer, startOffset) {
+ function scanEndMarkerInTextNode(textNode, offset) {
+ let scanResult = scanMarkerInTextNode(textNode, offset);
+ if (scanResult === null) {
+ return null;
+ }
+ if (scanResult[0] === "{" || scanResult[0] === "[") {
+ throw "A start marker is found before an end marker";
+ }
+ return {
+ marker: scanResult[0],
+ container: textNode,
+ offset: scanResult.index + offset
+ };
+ }
+ if (startContainer.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanEndMarkerInTextNode(startContainer, startOffset);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ }
+ let nextNode = startContainer;
+ while ((nextNode = getNextLeafNode(nextNode))) {
+ if (nextNode.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanEndMarkerInTextNode(nextNode, 0);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ continue;
+ }
+ }
+ return null;
+ })(startMarker.container, startMarker.offset + 1);
+ if (endMarker === null) {
+ throw "Found an open marker, but not found corresponding close marker";
+ }
+ function indexOfContainer(container, child) {
+ let offset = 0;
+ for (let node = container.firstChild; node; node = node.nextSibling) {
+ if (node == child) {
+ return offset;
+ }
+ offset++;
+ }
+ throw "child must be a child node of container";
+ }
+ (function deleteFoundMarkers() {
+ function removeNode(node) {
+ let container = node.parentElement;
+ let offset = indexOfContainer(container, node);
+ node.remove();
+ return { container, offset };
+ }
+ if (startMarker.container == endMarker.container) {
+ // If the text node becomes empty, remove it and set collapsed range
+ // to the position where there is the text node.
+ if (startMarker.container.length === 2) {
+ if (!/[\[\{][\]\}]/.test(startMarker.container.data)) {
+ throw `Unexpected text node (data: "${startMarker.container.data}")`;
+ }
+ let { container, offset } = removeNode(startMarker.container);
+ startMarker.container = endMarker.container = container;
+ startMarker.offset = endMarker.offset = offset;
+ startMarker.marker = endMarker.marker = "";
+ return;
+ }
+ startMarker.container.data = `${startMarker.container.data.substring(
+ 0,
+ startMarker.offset
+ )}${startMarker.container.data.substring(
+ startMarker.offset + 1,
+ endMarker.offset
+ )}${startMarker.container.data.substring(endMarker.offset + 1)}`;
+ if (startMarker.offset >= startMarker.container.length) {
+ startMarker.offset = endMarker.offset = startMarker.container.length;
+ return;
+ }
+ endMarker.offset--; // remove the start marker's length
+ if (endMarker.offset > endMarker.container.length) {
+ endMarker.offset = endMarker.container.length;
+ }
+ return;
+ }
+ if (startMarker.container.length === 1) {
+ let { container, offset } = removeNode(startMarker.container);
+ startMarker.container = container;
+ startMarker.offset = offset;
+ startMarker.marker = "";
+ } else {
+ startMarker.container.data = `${startMarker.container.data.substring(
+ 0,
+ startMarker.offset
+ )}${startMarker.container.data.substring(startMarker.offset + 1)}`;
+ }
+ if (endMarker.container.length === 1) {
+ let { container, offset } = removeNode(endMarker.container);
+ endMarker.container = container;
+ endMarker.offset = offset;
+ endMarker.marker = "";
+ } else {
+ endMarker.container.data = `${endMarker.container.data.substring(
+ 0,
+ endMarker.offset
+ )}${endMarker.container.data.substring(endMarker.offset + 1)}`;
+ }
+ })();
+ (function handleNodeSelectMarker() {
+ if (startMarker.marker === "{") {
+ if (startMarker.offset === 0) {
+ // The range start with the text node.
+ let container = startMarker.container.parentElement;
+ startMarker.offset = indexOfContainer(
+ container,
+ startMarker.container
+ );
+ startMarker.container = container;
+ } else if (startMarker.offset === startMarker.container.data.length) {
+ // The range start after the text node.
+ let container = startMarker.container.parentElement;
+ startMarker.offset =
+ indexOfContainer(container, startMarker.container) + 1;
+ startMarker.container = container;
+ } else {
+ throw 'Start marker "{" is allowed start or end of a text node';
+ }
+ }
+ if (endMarker.marker === "}") {
+ if (endMarker.offset === 0) {
+ // The range ends before the text node.
+ let container = endMarker.container.parentElement;
+ endMarker.offset = indexOfContainer(container, endMarker.container);
+ endMarker.container = container;
+ } else if (endMarker.offset === endMarker.container.data.length) {
+ // The range ends with the text node.
+ let container = endMarker.container.parentElement;
+ endMarker.offset =
+ indexOfContainer(container, endMarker.container) + 1;
+ endMarker.container = container;
+ } else {
+ throw 'End marker "}" is allowed start or end of a text node';
+ }
+ }
+ })();
+ let range = document.createRange();
+ range.setStart(startMarker.container, startMarker.offset);
+ range.setEnd(endMarker.container, endMarker.offset);
+ return range;
+ }
+
+ let ranges = [];
+ for (
+ let range = getNextRangeAndDeleteMarker(gEditor.firstChild);
+ range;
+ range = getNextRangeAndDeleteMarker(range.endContainer)
+ ) {
+ ranges.push(range);
+ }
+
+ gSelection.removeAllRanges();
+ for (let range of ranges) {
+ gSelection.addRange(range);
+ }
+
+ if (gSelection.rangeCount != ranges.length) {
+ throw `Failed to set selection to the given ranges whose length is ${ranges.length}, but only ${gSelection.rangeCount} ranges are added`;
+ }
+}
diff --git a/testing/web-platform/tests/input-events/input-events-typing.html b/testing/web-platform/tests/input-events/input-events-typing.html
new file mode 100644
index 0000000000..a894beea9b
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-typing.html
@@ -0,0 +1,285 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Input Event typing tests</title>
+<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>
+<div id="rich" contenteditable></div>
+<textarea id="plain"></textarea>
+<script>
+let inputEventsLog = [];
+const rich = document.getElementById('rich');
+const plain = document.getElementById('plain');
+
+function log(event) {
+ const clone = new event.constructor(event.type, event);
+ clone.state = rich.innerHTML;
+ inputEventsLog.push(clone);
+}
+
+function resetRich() {
+ inputEventsLog = [];
+ rich.innerHTML = '';
+}
+
+rich.addEventListener('beforeinput', log);
+rich.addEventListener('input', log);
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ const message = 'Hello';
+ await test_driver.send_keys(rich, message);
+ // 10 events (5 beforeinput + 5 input events)
+ assert_equals(inputEventsLog.length, 10);
+ for (let i = 0; i < inputEventsLog.length; i += 2) {
+ const beforeInputEvent = inputEventsLog[i];
+ const inputEvent = inputEventsLog[i + 1];
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'insertText');
+ assert_equals(inputEvent.inputType, 'insertText');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+ assert_equals(inputEvent.data, message[i / 2]);
+ assert_equals(beforeInputEvent.state + message[i / 2], inputEvent.state);
+ }
+}, 'It triggers beforeinput and input events on text typing');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ await test_driver.send_keys(rich, "\uE006"); // Return
+
+ assert_equals(inputEventsLog.length, 2);
+ const beforeInputEvent = inputEventsLog[0];
+ const inputEvent = inputEventsLog[1];
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'insertParagraph');
+ assert_equals(inputEvent.inputType, 'insertParagraph');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+}, 'It triggers beforeinput and input events on typing RETURN');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ await new test_driver.Actions()
+ .keyDown('\uE008') // Shift
+ .keyDown('\uE006') // Return
+ .keyUp('\uE006')
+ .keyUp('\uE008')
+ .send();
+
+ assert_equals(inputEventsLog.length, 2);
+ const [beforeInputEvent, inputEvent] = inputEventsLog;
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'insertLineBreak');
+ assert_equals(inputEvent.inputType, 'insertLineBreak');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+}, 'It triggers beforeinput and input events on typing Shift+RETURN');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>';
+ const caret = document.querySelector('#caret');
+ await test_driver.click(caret);
+ await test_driver.send_keys(caret, "\uE017"); // Delete
+
+ assert_equals(inputEventsLog.length, 2);
+ const [beforeInputEvent, inputEvent] = inputEventsLog;
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'deleteContentForward');
+ assert_equals(inputEvent.inputType, 'deleteContentForward');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+}, 'It triggers beforeinput and input events on typing DELETE with pre-existing content');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ await test_driver.send_keys(rich, "\uE017"); // Delete
+ assert_equals(inputEventsLog.length, 2);
+ const [beforeInputEvent, inputEvent] = inputEventsLog;
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'deleteContentForward');
+ assert_equals(inputEvent.inputType, 'deleteContentForward');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+}, 'It triggers beforeinput and input events on typing DELETE with no pre-existing content');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>';
+
+ await test_driver.click(document.querySelector('#caret'));
+ await test_driver.send_keys(rich, "\uE003"); // Back Space
+
+ assert_equals(inputEventsLog.length, 2);
+ const [beforeInputEvent, inputEvent] = inputEventsLog;
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'deleteContentBackward');
+ assert_equals(inputEvent.inputType, 'deleteContentBackward');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+}, 'It triggers beforeinput and input events on typing BACK_SPACE with pre-existing content');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ await test_driver.send_keys(rich, "\uE003"); // Back Space
+
+ assert_equals(inputEventsLog.length, 2);
+ const [beforeInputEvent, inputEvent] = inputEventsLog;
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, 'deleteContentBackward');
+ assert_equals(inputEvent.inputType, 'deleteContentBackward');
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+}, 'It triggers beforeinput and input events on typing BACK_SPACE with no pre-existing content');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ await test_driver.send_keys(rich, "hello");
+
+ // Decide whether to use Key.COMMAND (mac) or Key.CONTROL (everything else)
+ const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009';
+
+ // Undo
+ await new test_driver.Actions()
+ .keyDown(modifierKey)
+ .keyDown('z')
+ .keyUp('z')
+ .keyUp(modifierKey)
+ .send();
+ // Redo
+ await new test_driver.Actions()
+ .keyDown(modifierKey)
+ .keyDown('\uE008') // Shift
+ .keyDown('z')
+ .keyUp('z')
+ .keyUp('\uE008')
+ .keyUp(modifierKey)
+ .send();
+
+ // Ignore the initial typing of 'hello'
+ const historyInputEventsLog = inputEventsLog.slice(10);
+
+ assert_equals(historyInputEventsLog.length, 4);
+ const inputTypes = ['historyUndo', 'historyRedo'];
+ for (let i = 0; i < historyInputEventsLog.length; i += 2) {
+ // We are increaisng i by 2 as there should always be matching beforeinput and input events.
+ const beforeInputEvent = historyInputEventsLog[i];
+ const inputEvent = historyInputEventsLog[i + 1];
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, inputTypes[i / 2]);
+ assert_equals(inputEvent.inputType, inputTypes[i / 2]);
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+ }
+}, 'It triggers beforeinput and input events on typing Undo and Redo key combinations with an existing history');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ rich.focus();
+ // Decide whether to use Key.COMMAND (mac) or Key.CONTROL (everything else)
+ const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009';
+
+ // Undo
+ await new test_driver.Actions()
+ .keyDown(modifierKey)
+ .keyDown('z')
+ .keyUp('z')
+ .keyUp(modifierKey)
+ .send();
+ // Redo
+ await new test_driver.Actions()
+ .keyDown(modifierKey)
+ .keyDown('\uE008') // Shift
+ .keyDown('z')
+ .keyUp('z')
+ .keyUp('\uE008')
+ .keyUp(modifierKey)
+ .send();
+
+ assert_equals(inputEventsLog.length, 4);
+ const inputTypes = ['historyUndo', 'historyRedo'];
+ for (let i = 0; i < inputEventsLog.length; i += 2) {
+ const beforeInputEvent = inputEventsLog[i];
+ const inputEvent = inputEventsLog[i + 1];
+ assert_equals(beforeInputEvent.type, 'beforeinput');
+ assert_equals(inputEvent.type, 'input');
+ assert_equals(beforeInputEvent.inputType, inputTypes[i / 2]);
+ assert_equals(inputEvent.inputType, inputTypes[i / 2]);
+ assert_equals(beforeInputEvent.data, inputEvent.data);
+ }
+}, 'It triggers beforeinput and input events on typing Undo and Redo key combinations without an existing history');
+
+promise_test(async function() {
+ this.add_cleanup(resetRich);
+ const expectedResult = [
+ // Pressing 'a'.
+ 'plain-keydown-a',
+ 'plain-keypress-a',
+ 'plain-beforeinput-a-null',
+ 'plain-input-a-null',
+ 'plain-keyup-a',
+ // Pressing Shift-'b'.
+ 'plain-keydown-B',
+ 'plain-keypress-B',
+ 'plain-beforeinput-B-null',
+ 'plain-input-B-null',
+ 'plain-keyup-B',
+ // Pressing 'c'.
+ 'rich-keydown-c',
+ 'rich-keypress-c',
+ 'rich-beforeinput-c-null',
+ 'rich-input-c-null',
+ 'rich-keyup-c',
+ // Pressing Shift-'d'.
+ 'rich-keydown-D',
+ 'rich-keypress-D',
+ 'rich-beforeinput-D-null',
+ 'rich-input-D-null',
+ 'rich-keyup-D',
+ ];
+ const result = [];
+
+ for (const eventType of ['beforeinput', 'input', 'keydown', 'keypress', 'keyup']) {
+ const listener = event => {
+ if (event.key === 'Shift') return;
+ const eventInfo = [event.target.id, event.type, event.data || event.key];
+ if (event instanceof InputEvent) eventInfo.push(String(event.dataTransfer));
+ result.push(eventInfo.join('-'));
+ }
+ rich.addEventListener(eventType, listener);
+ plain.addEventListener(eventType, listener);
+ }
+
+ plain.focus();
+ await new test_driver.Actions()
+ .keyDown('a')
+ .keyUp('a')
+ .keyDown('\uE008') // Shift
+ .keyDown('b')
+ .keyUp('b')
+ .keyUp('\uE008')
+ .send();
+
+ rich.focus();
+ await new test_driver.Actions()
+ .keyDown('c')
+ .keyUp('c')
+ .keyDown('\uE008') // Shift
+ .keyDown('d')
+ .keyUp('d')
+ .keyUp('\uE008')
+ .send();
+
+ assert_equals(expectedResult.length, result.length);
+ expectedResult.forEach((er, index) => assert_equals(er, result[index]));
+}, 'InputEvents have correct data/order when typing on textarea and contenteditable');
+</script>
diff --git a/testing/web-platform/tests/input-events/select-event-drag-remove.html b/testing/web-platform/tests/input-events/select-event-drag-remove.html
new file mode 100644
index 0000000000..f5c2e702e5
--- /dev/null
+++ b/testing/web-platform/tests/input-events/select-event-drag-remove.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Drag select triggers the right event, and doesn't crash if it removes the target while at it</title>
+<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>
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1386418">
+<style>
+ input {
+ border: 0;
+ padding: 0;
+ font: 16px/1 monospace;
+ }
+</style>
+<input type="text" value="Drag select to crash">
+<script>
+async_test(t => {
+ let input = document.querySelector("input");
+ input.addEventListener("select", t.step_func(function() {
+ input.remove();
+ requestAnimationFrame(() => requestAnimationFrame(() => t.done()));
+ }));
+ new test_driver.Actions()
+ .pointerMove(0, 0, { origin: input })
+ .pointerDown()
+ .pointerMove(40, 0, { origin: input })
+ .pointerUp()
+ .send();
+}, "Drag and remove from the select event doesn't crash");
+</script>