browser_text_selection.js (12442B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* import-globals-from ../../mochitest/text.js */ 8 9 function waitForSelectionChange(selectionAcc, caretAcc) { 10 if (!caretAcc) { 11 caretAcc = selectionAcc; 12 } 13 return waitForEvents( 14 [ 15 [EVENT_TEXT_SELECTION_CHANGED, selectionAcc], 16 // We must swallow the caret events as well to avoid confusion with later, 17 // unrelated caret events. 18 [EVENT_TEXT_CARET_MOVED, caretAcc], 19 ], 20 true 21 ); 22 } 23 24 function changeDomSelection( 25 browser, 26 anchorId, 27 anchorOffset, 28 focusId, 29 focusOffset 30 ) { 31 return invokeContentTask( 32 browser, 33 [anchorId, anchorOffset, focusId, focusOffset], 34 ( 35 contentAnchorId, 36 contentAnchorOffset, 37 contentFocusId, 38 contentFocusOffset 39 ) => { 40 // We want the text node, so we use firstChild. 41 content.window 42 .getSelection() 43 .setBaseAndExtent( 44 content.document.getElementById(contentAnchorId).firstChild, 45 contentAnchorOffset, 46 content.document.getElementById(contentFocusId).firstChild, 47 contentFocusOffset 48 ); 49 } 50 ); 51 } 52 53 function testSelectionRange( 54 browser, 55 root, 56 startContainer, 57 startOffset, 58 endContainer, 59 endOffset 60 ) { 61 let selRange = root.selectionRanges.queryElementAt(0, nsIAccessibleTextRange); 62 testTextRange( 63 selRange, 64 getAccessibleDOMNodeID(root), 65 startContainer, 66 startOffset, 67 endContainer, 68 endOffset 69 ); 70 } 71 72 /** 73 * Test text selection via keyboard. 74 */ 75 addAccessibleTask( 76 ` 77 <textarea id="textarea">ab</textarea> 78 <div id="editable" contenteditable> 79 <p id="p1">a</p> 80 <p id="p2">bc</p> 81 <p id="pWithLink">d<a id="link" href="https://example.com/">e</a><span id="textAfterLink">f</span></p> 82 </div> 83 `, 84 async function (browser, docAcc) { 85 queryInterfaces(docAcc, [nsIAccessibleText]); 86 87 const textarea = findAccessibleChildByID(docAcc, "textarea", [ 88 nsIAccessibleText, 89 ]); 90 info("Focusing textarea"); 91 let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea); 92 textarea.takeFocus(); 93 await caretMoved; 94 testSelectionRange(browser, textarea, textarea, 0, textarea, 0); 95 is(textarea.selectionCount, 0, "textarea selectionCount is 0"); 96 is(docAcc.selectionCount, 0, "document selectionCount is 0"); 97 98 info("Selecting a in textarea"); 99 let selChanged = waitForSelectionChange(textarea); 100 EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }); 101 await selChanged; 102 testSelectionRange(browser, textarea, textarea, 0, textarea, 1); 103 testTextGetSelection(textarea, 0, 1, 0); 104 105 info("Selecting b in textarea"); 106 selChanged = waitForSelectionChange(textarea); 107 EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }); 108 await selChanged; 109 testSelectionRange(browser, textarea, textarea, 0, textarea, 2); 110 testTextGetSelection(textarea, 0, 2, 0); 111 112 info("Unselecting b in textarea"); 113 selChanged = waitForSelectionChange(textarea); 114 EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); 115 await selChanged; 116 testSelectionRange(browser, textarea, textarea, 0, textarea, 1); 117 testTextGetSelection(textarea, 0, 1, 0); 118 119 info("Unselecting a in textarea"); 120 // We don't fire selection changed when the selection collapses. 121 caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea); 122 EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); 123 await caretMoved; 124 testSelectionRange(browser, textarea, textarea, 0, textarea, 0); 125 is(textarea.selectionCount, 0, "textarea selectionCount is 0"); 126 127 const editable = findAccessibleChildByID(docAcc, "editable", [ 128 nsIAccessibleText, 129 ]); 130 const p1 = findAccessibleChildByID(docAcc, "p1", [nsIAccessibleText]); 131 info("Focusing editable, caret to start"); 132 caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p1); 133 await changeDomSelection(browser, "p1", 0, "p1", 0); 134 await caretMoved; 135 testSelectionRange(browser, editable, p1, 0, p1, 0); 136 is(editable.selectionCount, 0, "editable selectionCount is 0"); 137 is(p1.selectionCount, 0, "p1 selectionCount is 0"); 138 is(docAcc.selectionCount, 0, "document selectionCount is 0"); 139 140 info("Selecting a in editable"); 141 selChanged = waitForSelectionChange(p1); 142 await changeDomSelection(browser, "p1", 0, "p1", 1); 143 await selChanged; 144 testSelectionRange(browser, editable, p1, 0, p1, 1); 145 testTextGetSelection(editable, 0, 1, 0); 146 testTextGetSelection(p1, 0, 1, 0); 147 const p2 = findAccessibleChildByID(docAcc, "p2", [nsIAccessibleText]); 148 if (browser.isRemoteBrowser) { 149 is(p2.selectionCount, 0, "p2 selectionCount is 0"); 150 } else { 151 todo( 152 false, 153 "Siblings report wrong selection in non-cache implementation" 154 ); 155 } 156 157 // Selecting across two Accessibles with only a partial selection in the 158 // second. 159 info("Selecting ab in editable"); 160 selChanged = waitForSelectionChange(editable, p2); 161 await changeDomSelection(browser, "p1", 0, "p2", 1); 162 await selChanged; 163 testSelectionRange(browser, editable, p1, 0, p2, 1); 164 testTextGetSelection(editable, 0, 2, 0); 165 testTextGetSelection(p1, 0, 1, 0); 166 testTextGetSelection(p2, 0, 1, 0); 167 168 const pWithLink = findAccessibleChildByID(docAcc, "pWithLink", [ 169 nsIAccessibleText, 170 ]); 171 const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]); 172 // Selecting both text and a link. 173 info("Selecting de in editable"); 174 selChanged = waitForSelectionChange(pWithLink, link); 175 await changeDomSelection(browser, "pWithLink", 0, "link", 1); 176 await selChanged; 177 testSelectionRange(browser, editable, pWithLink, 0, link, 1); 178 testTextGetSelection(editable, 2, 3, 0); 179 testTextGetSelection(pWithLink, 0, 2, 0); 180 testTextGetSelection(link, 0, 1, 0); 181 182 // Selecting a link and text on either side. 183 info("Selecting def in editable"); 184 selChanged = waitForSelectionChange(pWithLink, pWithLink); 185 await changeDomSelection(browser, "pWithLink", 0, "textAfterLink", 1); 186 await selChanged; 187 testSelectionRange(browser, editable, pWithLink, 0, pWithLink, 3); 188 testTextGetSelection(editable, 2, 3, 0); 189 testTextGetSelection(pWithLink, 0, 3, 0); 190 testTextGetSelection(link, 0, 1, 0); 191 192 // Noncontiguous selection. 193 info("Selecting a in editable"); 194 selChanged = waitForSelectionChange(p1); 195 await changeDomSelection(browser, "p1", 0, "p1", 1); 196 await selChanged; 197 info("Adding c to selection in editable"); 198 selChanged = waitForSelectionChange(p2); 199 await invokeContentTask(browser, [], () => { 200 const r = content.document.createRange(); 201 const p2text = content.document.getElementById("p2").firstChild; 202 r.setStart(p2text, 0); 203 r.setEnd(p2text, 1); 204 content.window.getSelection().addRange(r); 205 }); 206 await selChanged; 207 let selRanges = editable.selectionRanges; 208 is(selRanges.length, 2, "2 selection ranges"); 209 testTextRange( 210 selRanges.queryElementAt(0, nsIAccessibleTextRange), 211 "range 0", 212 p1, 213 0, 214 p1, 215 1 216 ); 217 testTextRange( 218 selRanges.queryElementAt(1, nsIAccessibleTextRange), 219 "range 1", 220 p2, 221 0, 222 p2, 223 1 224 ); 225 is(editable.selectionCount, 2, "editable selectionCount is 2"); 226 testTextGetSelection(editable, 0, 1, 0); 227 testTextGetSelection(editable, 1, 2, 1); 228 if (browser.isRemoteBrowser) { 229 is(p1.selectionCount, 1, "p1 selectionCount is 1"); 230 testTextGetSelection(p1, 0, 1, 0); 231 is(p2.selectionCount, 1, "p2 selectionCount is 1"); 232 testTextGetSelection(p2, 0, 1, 0); 233 } else { 234 todo( 235 false, 236 "Siblings report wrong selection in non-cache implementation" 237 ); 238 } 239 }, 240 { 241 chrome: true, 242 topLevel: true, 243 iframe: true, 244 remoteIframe: true, 245 } 246 ); 247 248 /** 249 * Tabbing to an input selects all its text. Test that the cached selection 250 *reflects this. This has to be done separately from the other selection tests 251 * because prior contentEditable selection changes the events that get fired. 252 */ 253 addAccessibleTask( 254 ` 255 <button id="before">Before</button> 256 <input id="input" value="test"> 257 `, 258 async function (browser, docAcc) { 259 // The tab order is different when there's an iframe, so focus a control 260 // before the input to make tab consistent. 261 info("Focusing before"); 262 const before = findAccessibleChildByID(docAcc, "before"); 263 // Focusing a button fires a selection event. We must swallow this to 264 // avoid confusing the later test. 265 let events = waitForOrderedEvents([ 266 [EVENT_FOCUS, before], 267 [EVENT_TEXT_SELECTION_CHANGED, docAcc], 268 ]); 269 before.takeFocus(); 270 await events; 271 272 const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]); 273 info("Tabbing to input"); 274 events = waitForEvents( 275 { 276 expected: [ 277 [EVENT_FOCUS, input], 278 [EVENT_TEXT_SELECTION_CHANGED, input], 279 ], 280 unexpected: [[EVENT_TEXT_SELECTION_CHANGED, docAcc]], 281 }, 282 "input", 283 false, 284 (args, task) => invokeContentTask(browser, args, task) 285 ); 286 EventUtils.synthesizeKey("KEY_Tab"); 287 await events; 288 testSelectionRange(browser, input, input, 0, input, 4); 289 testTextGetSelection(input, 0, 4, 0); 290 }, 291 { 292 chrome: true, 293 topLevel: true, 294 iframe: true, 295 remoteIframe: true, 296 } 297 ); 298 299 /** 300 * Test text selection via API. 301 */ 302 addAccessibleTask( 303 ` 304 <p id="paragraph">hello world</p> 305 <ol> 306 <li id="li">Number one</li> 307 </ol> 308 `, 309 async function (browser, docAcc) { 310 const paragraph = findAccessibleChildByID(docAcc, "paragraph", [ 311 nsIAccessibleText, 312 ]); 313 314 let selChanged = waitForSelectionChange(paragraph); 315 paragraph.setSelectionBounds(0, 2, 4); 316 await selChanged; 317 testTextGetSelection(paragraph, 2, 4, 0); 318 319 selChanged = waitForSelectionChange(paragraph); 320 paragraph.addSelection(6, 10); 321 await selChanged; 322 testTextGetSelection(paragraph, 6, 10, 1); 323 is(paragraph.selectionCount, 2, "paragraph selectionCount is 2"); 324 325 selChanged = waitForSelectionChange(paragraph); 326 paragraph.removeSelection(0); 327 await selChanged; 328 testTextGetSelection(paragraph, 6, 10, 0); 329 is(paragraph.selectionCount, 1, "paragraph selectionCount is 1"); 330 331 const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]); 332 333 selChanged = waitForSelectionChange(li); 334 li.setSelectionBounds(0, 1, 8); 335 await selChanged; 336 testTextGetSelection(li, 3, 8, 0); 337 }, 338 { 339 chrome: true, 340 topLevel: true, 341 iframe: true, 342 remoteIframe: true, 343 } 344 ); 345 346 /** 347 * Test selections which start or end in an empty container. 348 */ 349 addAccessibleTask( 350 `<div id="div" contenteditable>a<p id="p"></p>b</div>`, 351 async function testEmptyContainer(browser, docAcc) { 352 const div = findAccessibleChildByID(docAcc, "div", [nsIAccessibleText]); 353 info('Selecting from the empty<p> to after the text "b"'); 354 let selected = waitForSelectionChange(div); 355 await invokeContentTask(browser, [], () => { 356 const divDom = content.document.getElementById("div"); 357 content 358 .getSelection() 359 .setBaseAndExtent(divDom.childNodes[1], 0, divDom.childNodes[2], 1); 360 }); 361 await selected; 362 const p = findAccessibleChildByID(docAcc, "p"); 363 testSelectionRange(browser, div, p, 0, div, 3); 364 testTextGetSelection(div, 1, 3, 0); 365 info('Selecting from the text "a" to after the empty<p>'); 366 selected = waitForSelectionChange(div, p); 367 await invokeContentTask(browser, [], () => { 368 const divDom = content.document.getElementById("div"); 369 content 370 .getSelection() 371 .setBaseAndExtent(divDom.childNodes[0], 0, divDom.childNodes[1], 0); 372 }); 373 await selected; 374 testSelectionRange(browser, div, div, 0, p, 0); 375 // XXX Bug 1973166: This should perhaps be (0, 2), indicating that the 376 // selection includes the empty paragraph. However, offset 1 isn't valid in 377 // an empty container, which means we use offset 0 in the paragraph. Since 378 // the end is exclusive, that causes us to exclude the paragraph when we 379 // transform to the div. 380 testTextGetSelection(div, 0, 1, 0); 381 }, 382 { chrome: true, toplevel: true } 383 );