test_bug1623764.html (8303B)
1 <!DOCTYPE html> 2 <title>Test Windows conventional caret movement behavior</title> 3 <script src="/tests/SimpleTest/SimpleTest.js"></script> 4 <script src="/tests/SimpleTest/EventUtils.js"></script> 5 <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> 6 <style> 7 [contenteditable] { 8 font-size: 14px; 9 font-family: monospace; 10 overflow-wrap: normal; 11 outline: none; 12 width: 35ch; 13 } 14 textarea { 15 font-size: 14px; 16 font-family: monospace; 17 } 18 19 .break-word { 20 overflow-wrap: break-word; 21 } 22 </style> 23 <div id="testDiv" contenteditable></div> 24 <textarea id="testTextarea" cols=35></textarea> 25 26 <script> 27 // Call testEach(cases[i]) on console when debugging 28 /** 29 * @type {TestCase[]} 30 * 31 * @typedef {object} TestCase 32 * @property {string} title 33 * @property {string} text Text on which the test runs. 34 * The newlines and spaces will be auto-converted if needed. 35 * @property {boolean} [reverse] Test in both forward and backward directions 36 * @property {boolean} [backward] Move backward by cmd_wordPrevious 37 * @property {ElementSpecific} [div] Can be omitted when there is a bug 38 * @property {ElementSpecific} textarea 39 * 40 * @typedef {object} ElementSpecific 41 * @property {number} expectedOffset Expected offset after a caret move 42 * @property {number} [expectedNodeIndex] The index of child node with focus after a caret move. 43 * -1 means the parent node. 44 * @property {number} [initialOffset=0] initialOffset offset before a caret move 45 * @property {number} [initialNodeIndex] 46 */ 47 48 const kFirstWordLength = "Supercalifragilisticexpialidocious".length; // 34 49 const kParentNodeIndex = -1; // special value 50 const cases = [ 51 { 52 title: "Eats inline whitespaces after word", 53 text: "Supercalifragilisticexpialidocious foo bar fussball", 54 reverse: true, 55 div: { 56 expectedOffset: kFirstWordLength + 1 57 }, 58 textarea: { 59 expectedOffset: kFirstWordLength + 1 60 } 61 }, 62 { 63 title: "Eats inline whitespaces across a wrapped line after word", 64 text: "Supercalifragilisticexpialidocious foo bar fussball", 65 reverse: true, 66 div: { 67 expectedOffset: kFirstWordLength + 7 68 }, 69 textarea: { 70 expectedOffset: kFirstWordLength + 7 71 } 72 }, 73 { 74 title: "Eats inline whitespaces across multiple wrapped lines after word", 75 text: `Supercalifragilisticexpialidocious foo bar fussball`, 76 reverse: true, 77 div: { 78 expectedOffset: kFirstWordLength + 65 79 }, 80 textarea: { 81 expectedOffset: kFirstWordLength + 65 82 } 83 }, 84 { 85 title: "Eats inline whitespaces after a whole word and stop before a newline", 86 text: "Supercalifragilisticexpialidocious \nfoo bar fussball", 87 reverse: true, 88 div: { 89 expectedOffset: 1, 90 expectedNodeIndex: kParentNodeIndex 91 }, 92 textarea: { 93 expectedOffset: kFirstWordLength + 1 94 } 95 }, 96 { 97 title: "Eats inline whitespaces after a partial word and stop before a newline", 98 text: "Supercalifragilisticexpialidocious \nfoo bar fussball", 99 div: { 100 initialOffset: 5, 101 expectedOffset: 1, 102 expectedNodeIndex: kParentNodeIndex 103 }, 104 textarea: { 105 initialOffset: 5, 106 expectedOffset: kFirstWordLength + 1 107 } 108 }, 109 { 110 title: "Eats inline whitespaces without a word and stop before a newline", 111 text: "Supercalifragilisticexpialidocious \n foo bar fussball", 112 // TODO(krosylight): Currently ClusterIterator internally skips trailing whitespace 113 // div: { 114 // initialOffset: kFirstWordLength, 115 // expectedOffset: 1, 116 // expectedNodeIndex: kParentNodeIndex 117 // }, 118 textarea: { 119 initialOffset: kFirstWordLength, 120 expectedOffset: kFirstWordLength + 1 121 } 122 }, 123 { 124 title: "Jumps to the next line and eats inline whitespaces", 125 text: "Supercalifragilisticexpialidocious \n foo bar fussball", 126 reverse: true, 127 div: { 128 initialOffset: kFirstWordLength + 1, 129 expectedOffset: 6, 130 expectedNodeIndex: 2 131 }, 132 textarea: { 133 initialOffset: kFirstWordLength + 1, 134 expectedOffset: kFirstWordLength + 8 135 } 136 }, 137 { 138 title: "Stops on whitespaces after word", 139 text: "Supercalifragilisticexpialidocious \n foo bar fussball", 140 backward: true, 141 div: { 142 initialOffset: 8, 143 initialNodeIndex: 2, 144 expectedOffset: 6, 145 expectedNodeIndex: 2 146 }, 147 textarea: { 148 initialOffset: 44, // middle of "foo" 149 expectedOffset: 42 150 } 151 }, 152 // TODO(krosylight): Currently no way to tell a word break is from line wrapping 153 // { 154 // title: "Ignore a word break introduced by line wrapping", 155 // text: "Supercalifragilisticexpialidociouspostfix", 156 // className: "narrow break-word", 157 // div: { 158 // expectedOffset: kFirstWordLength + 7 159 // }, 160 // textarea: { 161 // expectedOffset: kFirstWordLength + 7 162 // } 163 // } 164 { 165 title: "Jump only one line at an empty line end", 166 text: "Supercalifragilisticexpialidocious \n\nfoo bar fussball", 167 reverse: true, 168 div: { 169 initialOffset: 2, 170 initialNodeIndex: kParentNodeIndex, 171 expectedOffset: 0, 172 expectedNodeIndex: 3 173 }, 174 textarea: { 175 initialOffset: kFirstWordLength + 2, 176 expectedOffset: kFirstWordLength + 3 177 } 178 }, 179 { 180 title: "Jump only one line at a non-empty line end", 181 text: "Supercalifragilisticexpialidocious \n\nfoo bar fussball", 182 reverse: true, 183 div: { 184 initialOffset: kFirstWordLength + 1, 185 expectedOffset: 2, 186 expectedNodeIndex: -1 187 }, 188 textarea: { 189 initialOffset: kFirstWordLength + 1, 190 expectedOffset: kFirstWordLength + 2 191 } 192 }, 193 ]; 194 195 SimpleTest.waitForExplicitFinish(); 196 SimpleTest.waitForFocus(async () => { 197 await SpecialPowers.pushPrefEnv({ 198 set: [["layout.word_select.eat_space_to_next_word", true]] 199 }); 200 // Test on div first to prevent Bug 1623413 201 for (const testCase of cases) { 202 if (testCase.div) { 203 testOnDiv(testCase); 204 } 205 } 206 for (const testCase of cases) { 207 testOnTextarea(testCase); 208 } 209 SimpleTest.finish(); 210 }); 211 212 /** @param {TestCase} testCase */ 213 function testOnDiv(testCase) { 214 const { 215 title, 216 backward = false, 217 reverse = false, 218 } = testCase; 219 const { 220 initialOffset = 0, 221 expectedOffset, 222 } = testCase.div; 223 if (expectedOffset === undefined) { 224 throw new Error("`expectedOffset` must be defined.") 225 } 226 227 testDiv.innerHTML = testCase.text.replaceAll(/ {2}/g, " ").replaceAll(/\n/g, "<br>"); 228 const initialNode = childNode(testDiv, testCase.div.initialNodeIndex); 229 const expectedNode = childNode(testDiv, testCase.div.expectedNodeIndex); 230 231 const selection = getSelection(); 232 selection.collapse(initialNode, initialOffset); 233 234 moveByWord(backward); 235 236 is(selection.focusNode, expectedNode, `focusNode in "${title}"`); 237 is(selection.focusOffset, expectedOffset, `focusOffset in "${title}"`); 238 239 if (reverse) { 240 selection.collapse(expectedNode, expectedOffset); 241 242 moveByWord(!backward); 243 244 is(selection.focusNode, initialNode, `focusNode with reversed selection in "${title}"`); 245 is(selection.focusOffset, initialOffset, `focusOffset with reversed selection in "${title}"`); 246 } 247 } 248 249 function testOnTextarea(testCase) { 250 const { 251 title, 252 backward = false, 253 reverse = false, 254 } = testCase; 255 const { 256 initialOffset = 0, 257 expectedOffset, 258 } = testCase.textarea; 259 if (expectedOffset === undefined) { 260 throw new Error("`expectedOffset` must be defined.") 261 } 262 263 testTextarea.value = testCase.text; 264 testTextarea.selectionStart = testTextarea.selectionEnd = initialOffset; 265 testTextarea.focus(); 266 267 moveByWord(backward); 268 269 is(testTextarea.selectionStart, expectedOffset, `selectionStart in "${title}"`); 270 271 if (reverse) { 272 testTextarea.selectionStart = testTextarea.selectionEnd = expectedOffset; 273 274 moveByWord(!backward); 275 276 is(testTextarea.selectionStart, initialOffset, `selectionStart with reversed selection in "${title}"`); 277 } 278 } 279 280 function childNode(parent, index = 0) { 281 if (index === kParentNodeIndex) { 282 return parent; 283 } 284 return parent.childNodes[index]; 285 } 286 287 /** @param {boolean} backward */ 288 function moveByWord(backward) { 289 const dir = backward ? "Previous" : "Next"; 290 SpecialPowers.doCommand(window, `cmd_word${dir}`); 291 } 292 </script>