diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/input-events | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
16 files changed, 9688 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..347ac61c3e --- /dev/null +++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges-backspace.tentative.html @@ -0,0 +1,2065 @@ +<!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 def</p>", + "<p>abc def</p>", + "<p>abc def</p>", + "<p>abc 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>שלום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>שלום[]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>"'); + +</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 def</p>", + "<p>abc def</p>", + "<p>abc def</p>", + "<p>abc 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שלום</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[]שלום</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 </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 </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(/ /g, " ") + : gEditor.innerHTML, + expectedResult + ); + } else { + assert_equals( + options.ignoreWhiteSpaceDifference + ? gEditor.innerHTML.replace(/ /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> |