delete-space-after-double-click-selection.html (10738B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="timeout" content="long"> 6 <title>Test for Bug 1783641</title> 7 <script src=/resources/testharness.js></script> 8 <script src=/resources/testharnessreport.js></script> 9 <script src="/resources/testdriver.js"></script> 10 <script src="/resources/testdriver-actions.js"></script> 11 <style> 12 .testStyle { 13 font-family: 'Courier New', Courier, monospace; 14 font-size: 12px; 15 padding: 0px; 16 width: 200px; 17 } 18 </style> 19 </head> 20 <body> 21 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1783641">Mozilla Bug 1783641</a><br /> 22 <span class="testStyle" id="placeholder"></span> 23 <input class="testStyle" type="text" /> 24 <div class="testStyle" contenteditable></div> 25 <textarea class="testStyle"></textarea> 26 <script> 27 28 promise_test(async t => { 29 await new Promise(resolve => { window.onload = resolve; }); 30 31 await SpecialPowers.pushPrefEnv({ 32 set: [ 33 ["editor.word_select.delete_space_after_doubleclick_selection", true], 34 ["layout.word_select.eat_space_to_next_word", false] 35 ] 36 }); 37 }, "Test setup"); 38 const placeHolder = document.getElementById("placeholder"); 39 const deleteKey = "\uE017"; 40 41 function waitForRender() { 42 return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); 43 }; 44 45 for (const selector of ["input", "div[contenteditable]", "textarea"]) { 46 const editableElement = document.querySelector(selector); 47 editableElement.focus(); 48 49 /** 50 * Helper functions to set or get the value and the current selection of `editableElement`, 51 * regardless of its actual type. 52 */ 53 const setValue = (aValue) => { 54 editableElement.tagName.toLowerCase() == "div" 55 ? editableElement.innerHTML = aValue 56 : editableElement.value = aValue; 57 } 58 const getValue = () => { 59 return editableElement.tagName.toLowerCase() == "div" 60 ? editableElement.innerHTML 61 : editableElement.value; 62 }; 63 const getSelection = () => { 64 return editableElement.tagName.toLowerCase() == "div" 65 ? document.getSelection().toString() 66 : editableElement.value.substring( 67 editableElement.selectionStart, 68 editableElement.selectionEnd 69 ); 70 }; 71 72 /** 73 * Places a double click in `editableElement` at exactly the end of `aPlaceHolderText` and press delete. 74 * `aPlaceholderText` therefore should contain the same text as the value of the `editableElement` 75 * up to the point where the doubleclick should happen. 76 * 77 * If `aSelectionValue` is defined, the selection created by the double click is compared to `aSelectionValue`. 78 */ 79 const doubleClickAndDelete = async (aPlaceHolderText, aSelectionValue = undefined) => { 80 placeHolder.innerHTML = aPlaceHolderText; 81 editableElement.focus(); 82 await waitForRender(); 83 const absInputPos = editableElement.getBoundingClientRect(); 84 selectionOffset = { 85 x: placeHolder.getBoundingClientRect().width, 86 y: Math.floor(placeHolder.getBoundingClientRect().height / 2) 87 }; 88 await (new test_driver.Actions() 89 // for some reason this still doesn't work: 90 // .pointerMove(Math.floor(selectionOffset.x), Math.floor(selectionOffset.y), { origin: editableElement }) 91 // but this does: 92 .pointerMove( 93 Math.floor(absInputPos.x + selectionOffset.x), 94 Math.floor(absInputPos.y + selectionOffset.y), 95 { origin: "viewport" } 96 ) 97 .pointerDown() 98 .pointerUp() 99 .pointerDown() 100 .pointerUp()) 101 .send() 102 await waitForRender(); 103 if (aSelectionValue !== undefined) { 104 assert_equals(getSelection(), aSelectionValue, "Wrong selection value!"); 105 } 106 return test_driver.send_keys(editableElement, deleteKey); 107 }; 108 if (editableElement.tagName.toLowerCase() == "div") { 109 promise_test(async t => { 110 setValue("<p>abc def<span></span></p>"); 111 await doubleClickAndDelete("abc de", "def"); 112 await waitForRender(); 113 assert_equals( 114 getValue(), 115 "<p>abc</p>", 116 "The <span> at the end of the string must be removed, as well as the whitespace in between words."); 117 }, `${editableElement.tagName}: An empty span at the end of the selection should be considered end of selection!`); 118 } 119 promise_test(async t => { 120 setValue("one two"); 121 await doubleClickAndDelete("on", "one"); 122 await waitForRender(); 123 assert_equals( 124 getValue(), 125 "two", 126 "The whitespace between words must be removed when a word at the beginning is selected and deleted!" 127 ); 128 }, `${editableElement.tagName}: Remove word at the beginning of string should remove the whitespace in between.`); 129 130 promise_test(async t => { 131 setValue("one two"); 132 await doubleClickAndDelete("one tw", "two"); 133 await waitForRender(); 134 assert_equals( 135 getValue(), 136 "one", 137 "The whitespace between words must be removed when a word is selected at the end of the string and deleted!" 138 ); 139 }, `${editableElement.tagName}: Remove word at the end of a string should remove the whitespace in between.`); 140 141 promise_test(async t => { 142 setValue("one two three"); 143 await doubleClickAndDelete("one tw", "two"); 144 await waitForRender(); 145 assert_equals( 146 getValue(), 147 "one three", 148 "One whitespace between words must be removed when a word is selected and deleted!" 149 ); 150 await waitForRender(); 151 if (editableElement.tagName.toLowerCase() == "div") { 152 document.getSelection().setBaseAndExtent( 153 editableElement.firstChild, 154 0, 155 editableElement.firstChild, 156 3 157 ); 158 } 159 else { 160 editableElement.setSelectionRange(0, 3); 161 } 162 await test_driver.send_keys(editableElement, deleteKey); 163 // div[contenteditable] returns ' three' here. 164 assert_equals( 165 getValue().replace(/ /g, " "), 166 " three", 167 "The whitespace must not be removed when selecting a word without doubleclicking it!" 168 ); 169 170 }, `${editableElement.tagName}: Remove word in the middle of a string should remove one whitespace ` + 171 "only if selection is created by double click."); 172 173 promise_test(async t => { 174 setValue("one two three"); 175 await doubleClickAndDelete("one tw", "two"); 176 await waitForRender(); 177 assert_equals( 178 getValue(), 179 "one three", 180 "One whitespace between words must be removed when a word is selected and deleted!" 181 ); 182 }, `${editableElement.tagName}: Only one whitespace character should be removed when there are multiple.`); 183 184 promise_test(async t => { 185 setValue("one two"); 186 await doubleClickAndDelete("one tw", "two"); 187 await waitForRender(); 188 assert_equals( 189 getValue(), 190 "one ", 191 "One whitespace character between words must be removed when a word is selected and deleted!" 192 ); 193 }, `${editableElement.tagName}: Only one whitespace character should be removed when ` + 194 "there are multiple whitespaces and the deleted range is the end of the string."); 195 196 promise_test(async t => { 197 setValue("one two, three"); 198 await doubleClickAndDelete("one tw", "two"); 199 await waitForRender(); 200 assert_equals( 201 getValue(), 202 "one, three", 203 "The whitespace in front of the selected word must be removed when punctuation follows selection!" 204 ); 205 }, `${editableElement.tagName}: Removing a word before punctuation should remove the whitespace.`); 206 207 promise_test(async t => { 208 setValue("one, two"); 209 await doubleClickAndDelete("one, tw", "two"); 210 await waitForRender(); 211 assert_equals( 212 getValue(), 213 "one,", 214 "The whitespace in front of the selected word must be removed!" 215 ); 216 }, `${editableElement.tagName}: Remove a word after punctuation should remove the whitespace.`); 217 218 promise_test(async t => { 219 setValue("one\u00A0two, three"); // adds a 220 await doubleClickAndDelete("one tw", "two"); 221 await waitForRender(); 222 assert_equals( 223 getValue(), 224 "one, three", 225 "The whitespace between words must be removed when a word is selected and deleted!" 226 ); 227 }, `${editableElement.tagName}: Removing a word between a and punctuation should remove the nbsp character.`); 228 229 if (editableElement.tagName.toLowerCase() == "div") { 230 promise_test(async t => { 231 setValue("one two<br>"); 232 await doubleClickAndDelete("one tw", "two"); 233 await waitForRender(); 234 assert_equals( 235 getValue(), 236 "one", 237 "The line break must be preserved!" 238 ); 239 }, `${editableElement.tagName}: Removing a word in front of a line break should preserve the line break.`); 240 } 241 if (editableElement.tagName.toLowerCase() == "textarea") { 242 promise_test(async t => { 243 setValue("one two\n"); 244 await doubleClickAndDelete("one tw", "two"); 245 await waitForRender(); 246 assert_equals( 247 getValue(), 248 "one\n", 249 "The line break must be preserved!" 250 ); 251 }, `${editableElement.tagName}: RRemoving a word in front of a line break should preserve the line break.`); 252 } 253 promise_test(async t => { 254 setValue("one two"); 255 await doubleClickAndDelete("on", "one"); 256 await waitForRender(); 257 assert_equals( 258 getValue(), 259 "two", 260 "The whitespace between words must be removed when a word at the beginning is selected and deleted!" 261 ); 262 document.execCommand("undo", false, null); 263 assert_equals( 264 getValue(), 265 "one two", 266 "Undo action must restore the original state!" 267 ); 268 document.execCommand("redo", false, null); 269 assert_equals( 270 getValue(), 271 "two", 272 "Redo action must remove the word and whitespace again!" 273 ); 274 }, `${editableElement.tagName}: Undo and Redo actions should take the removed whitespace into account.`); 275 } 276 </script> 277 </body> 278 </html>