browser_text_input.js (19376B)
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/role.js */ 8 /* import-globals-from ../../mochitest/states.js */ 9 loadScripts( 10 { name: "role.js", dir: MOCHITESTS_DIR }, 11 { name: "states.js", dir: MOCHITESTS_DIR } 12 ); 13 14 function testValueChangedEventData( 15 macIface, 16 data, 17 expectedId, 18 expectedChangeValue, 19 expectedEditType, 20 expectedWordAtLeft 21 ) { 22 is( 23 data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"), 24 expectedId, 25 "Correct AXTextChangeElement" 26 ); 27 is( 28 data.AXTextStateChangeType, 29 AXTextStateChangeTypeEdit, 30 "Correct AXTextStateChangeType" 31 ); 32 33 let changeValues = data.AXTextChangeValues; 34 is(changeValues.length, 1, "One element in AXTextChangeValues"); 35 is( 36 changeValues[0].AXTextChangeValue, 37 expectedChangeValue, 38 "Correct AXTextChangeValue" 39 ); 40 is( 41 changeValues[0].AXTextEditType, 42 expectedEditType, 43 "Correct AXTextEditType" 44 ); 45 46 let textMarker = changeValues[0].AXTextChangeValueStartMarker; 47 ok(textMarker, "There is a AXTextChangeValueStartMarker"); 48 let range = macIface.getParameterizedAttributeValue( 49 "AXLeftWordTextMarkerRangeForTextMarker", 50 textMarker 51 ); 52 let str = macIface.getParameterizedAttributeValue( 53 "AXStringForTextMarkerRange", 54 range, 55 "correct word before caret" 56 ); 57 is(str, expectedWordAtLeft); 58 } 59 60 // Return true if the first given object a subset of the second 61 function isSubset(subset, superset) { 62 if (typeof subset != "object" || typeof superset != "object") { 63 return superset == subset; 64 } 65 66 for (let [prop, val] of Object.entries(subset)) { 67 if (!isSubset(val, superset[prop])) { 68 return false; 69 } 70 } 71 72 return true; 73 } 74 75 function matchWebArea(expectedId, expectedInfo) { 76 return (iface, data) => { 77 if (!data) { 78 return false; 79 } 80 81 let textChangeElemID = 82 data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"); 83 84 return ( 85 iface.getAttributeValue("AXRole") == "AXWebArea" && 86 textChangeElemID == expectedId && 87 isSubset(expectedInfo, data) 88 ); 89 }; 90 } 91 92 function matchInput(expectedId, expectedInfo) { 93 return (iface, data) => { 94 if (!data) { 95 return false; 96 } 97 98 return ( 99 iface.getAttributeValue("AXDOMIdentifier") == expectedId && 100 isSubset(expectedInfo, data) 101 ); 102 }; 103 } 104 105 async function synthKeyAndTestSelectionChanged( 106 synthKey, 107 synthEvent, 108 expectedId, 109 expectedSelectionString, 110 expectedSelectionInfo 111 ) { 112 let selectionChangedEvents = Promise.all([ 113 waitForMacEventWithInfo( 114 "AXSelectedTextChanged", 115 matchWebArea(expectedId, expectedSelectionInfo) 116 ), 117 waitForMacEventWithInfo( 118 "AXSelectedTextChanged", 119 matchInput(expectedId, expectedSelectionInfo) 120 ), 121 ]); 122 123 EventUtils.synthesizeKey(synthKey, synthEvent); 124 let [webareaEvent, inputEvent] = await selectionChangedEvents; 125 is( 126 inputEvent.data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"), 127 expectedId, 128 "Correct AXTextChangeElement" 129 ); 130 131 let rangeString = inputEvent.macIface.getParameterizedAttributeValue( 132 "AXStringForTextMarkerRange", 133 inputEvent.data.AXSelectedTextMarkerRange 134 ); 135 is( 136 rangeString, 137 expectedSelectionString, 138 `selection has correct value (${expectedSelectionString})` 139 ); 140 141 let rangeBounds = inputEvent.macIface.getParameterizedAttributeValue( 142 "AXBoundsForTextMarkerRange", 143 inputEvent.data.AXSelectedTextMarkerRange 144 ); 145 146 ok( 147 rangeBounds.origin && rangeBounds.size && rangeBounds.size[0], 148 "Selection range has bounds" 149 ); 150 151 is( 152 webareaEvent.macIface.getAttributeValue("AXDOMIdentifier"), 153 "body", 154 "Input event target is top-level WebArea" 155 ); 156 rangeString = webareaEvent.macIface.getParameterizedAttributeValue( 157 "AXStringForTextMarkerRange", 158 inputEvent.data.AXSelectedTextMarkerRange 159 ); 160 is( 161 rangeString, 162 expectedSelectionString, 163 `selection has correct value (${expectedSelectionString}) via top document` 164 ); 165 166 return inputEvent; 167 } 168 169 function testSelectionEventLeftChar(event, expectedChar) { 170 const selStart = event.macIface.getParameterizedAttributeValue( 171 "AXStartTextMarkerForTextMarkerRange", 172 event.data.AXSelectedTextMarkerRange 173 ); 174 const selLeft = event.macIface.getParameterizedAttributeValue( 175 "AXPreviousTextMarkerForTextMarker", 176 selStart 177 ); 178 const leftCharRange = event.macIface.getParameterizedAttributeValue( 179 "AXTextMarkerRangeForUnorderedTextMarkers", 180 [selLeft, selStart] 181 ); 182 const leftCharString = event.macIface.getParameterizedAttributeValue( 183 "AXStringForTextMarkerRange", 184 leftCharRange 185 ); 186 is(leftCharString, expectedChar, "Left character is correct"); 187 } 188 189 function testSelectionEventLine(event, expectedLine) { 190 const selStart = event.macIface.getParameterizedAttributeValue( 191 "AXStartTextMarkerForTextMarkerRange", 192 event.data.AXSelectedTextMarkerRange 193 ); 194 const lineRange = event.macIface.getParameterizedAttributeValue( 195 "AXLineTextMarkerRangeForTextMarker", 196 selStart 197 ); 198 const lineString = event.macIface.getParameterizedAttributeValue( 199 "AXStringForTextMarkerRange", 200 lineRange 201 ); 202 is(lineString, expectedLine, "Line is correct"); 203 } 204 205 async function synthKeyAndTestValueChanged( 206 synthKey, 207 synthEvent, 208 expectedId, 209 expectedTextSelectionId, 210 expectedChangeValue, 211 expectedEditType, 212 expectedWordAtLeft 213 ) { 214 let valueChangedEvents = Promise.all([ 215 waitForMacEvent( 216 "AXSelectedTextChanged", 217 matchWebArea(expectedTextSelectionId, { 218 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 219 }) 220 ), 221 waitForMacEvent( 222 "AXSelectedTextChanged", 223 matchInput(expectedTextSelectionId, { 224 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 225 }) 226 ), 227 waitForMacEventWithInfo( 228 "AXValueChanged", 229 matchWebArea(expectedId, { 230 AXTextStateChangeType: AXTextStateChangeTypeEdit, 231 AXTextChangeValues: [ 232 { 233 AXTextChangeValue: expectedChangeValue, 234 AXTextEditType: expectedEditType, 235 }, 236 ], 237 }) 238 ), 239 waitForMacEventWithInfo( 240 "AXValueChanged", 241 matchInput(expectedId, { 242 AXTextStateChangeType: AXTextStateChangeTypeEdit, 243 AXTextChangeValues: [ 244 { 245 AXTextChangeValue: expectedChangeValue, 246 AXTextEditType: expectedEditType, 247 }, 248 ], 249 }) 250 ), 251 ]); 252 253 EventUtils.synthesizeKey(synthKey, synthEvent); 254 let [, , webareaEvent, inputEvent] = await valueChangedEvents; 255 256 testValueChangedEventData( 257 webareaEvent.macIface, 258 webareaEvent.data, 259 expectedId, 260 expectedChangeValue, 261 expectedEditType, 262 expectedWordAtLeft 263 ); 264 testValueChangedEventData( 265 inputEvent.macIface, 266 inputEvent.data, 267 expectedId, 268 expectedChangeValue, 269 expectedEditType, 270 expectedWordAtLeft 271 ); 272 } 273 274 async function focusIntoInput(accDoc, inputId, innerContainerId) { 275 let selectionId = innerContainerId ? innerContainerId : inputId; 276 let input = getNativeInterface(accDoc, inputId); 277 ok(!input.getAttributeValue("AXFocused"), "input is not focused"); 278 ok(input.isAttributeSettable("AXFocused"), "input is focusable"); 279 let events = Promise.all([ 280 waitForMacEvent( 281 "AXFocusedUIElementChanged", 282 iface => iface.getAttributeValue("AXDOMIdentifier") == inputId 283 ), 284 waitForMacEventWithInfo( 285 "AXSelectedTextChanged", 286 matchWebArea(selectionId, { 287 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 288 }) 289 ), 290 waitForMacEventWithInfo( 291 "AXSelectedTextChanged", 292 matchInput(selectionId, { 293 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 294 }) 295 ), 296 ]); 297 input.setAttributeValue("AXFocused", true); 298 await events; 299 } 300 301 async function focusIntoInputAndType(accDoc, inputId, innerContainerId) { 302 let selectionId = innerContainerId ? innerContainerId : inputId; 303 await focusIntoInput(accDoc, inputId, innerContainerId); 304 305 async function testTextInput( 306 synthKey, 307 expectedChangeValue, 308 expectedWordAtLeft 309 ) { 310 await synthKeyAndTestValueChanged( 311 synthKey, 312 null, 313 inputId, 314 selectionId, 315 expectedChangeValue, 316 AXTextEditTypeTyping, 317 expectedWordAtLeft 318 ); 319 } 320 321 await testTextInput("h", "h", "h"); 322 await testTextInput("e", "e", "he"); 323 await testTextInput("l", "l", "hel"); 324 await testTextInput("l", "l", "hell"); 325 await testTextInput("o", "o", "hello"); 326 await testTextInput(" ", " ", "hello"); 327 // You would expect this to be useless but this is what VO 328 // consumes. I guess it concats the inserted text data to the 329 // word to the left of the marker. 330 await testTextInput("w", "w", " "); 331 await testTextInput("o", "o", "wo"); 332 await testTextInput("r", "r", "wor"); 333 await testTextInput("l", "l", "worl"); 334 await testTextInput("d", "d", "world"); 335 336 async function testTextDelete(expectedChangeValue, expectedWordAtLeft) { 337 await synthKeyAndTestValueChanged( 338 "KEY_Backspace", 339 null, 340 inputId, 341 selectionId, 342 expectedChangeValue, 343 AXTextEditTypeDelete, 344 expectedWordAtLeft 345 ); 346 } 347 348 await testTextDelete("d", "worl"); 349 await testTextDelete("l", "wor"); 350 351 await synthKeyAndTestSelectionChanged( 352 "KEY_ArrowLeft", 353 null, 354 selectionId, 355 "", 356 { 357 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 358 AXTextSelectionDirection: AXTextSelectionDirectionPrevious, 359 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 360 } 361 ); 362 await synthKeyAndTestSelectionChanged( 363 "KEY_ArrowLeft", 364 { shiftKey: true }, 365 selectionId, 366 "o", 367 { 368 AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, 369 AXTextSelectionDirection: AXTextSelectionDirectionPrevious, 370 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 371 } 372 ); 373 await synthKeyAndTestSelectionChanged( 374 "KEY_ArrowLeft", 375 { shiftKey: true }, 376 selectionId, 377 "wo", 378 { 379 AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, 380 AXTextSelectionDirection: AXTextSelectionDirectionPrevious, 381 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 382 } 383 ); 384 await synthKeyAndTestSelectionChanged( 385 "KEY_ArrowLeft", 386 null, 387 selectionId, 388 "", 389 { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove } 390 ); 391 await synthKeyAndTestSelectionChanged( 392 "KEY_ArrowLeft", 393 { shiftKey: true, metaKey: true }, 394 selectionId, 395 "hello ", 396 { 397 AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, 398 AXTextSelectionDirection: AXTextSelectionDirectionBeginning, 399 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 400 } 401 ); 402 await synthKeyAndTestSelectionChanged( 403 "KEY_ArrowLeft", 404 null, 405 selectionId, 406 "", 407 { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove } 408 ); 409 await synthKeyAndTestSelectionChanged( 410 "KEY_ArrowRight", 411 { shiftKey: true, altKey: true }, 412 selectionId, 413 "hello", 414 { 415 AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, 416 AXTextSelectionDirection: AXTextSelectionDirectionNext, 417 AXTextSelectionGranularity: AXTextSelectionGranularityWord, 418 } 419 ); 420 } 421 422 // Test text input 423 addAccessibleTask( 424 `<a href="#">link</a> <input id="input">`, 425 async (browser, accDoc) => { 426 await focusIntoInputAndType(accDoc, "input"); 427 }, 428 { topLevel: true, iframe: true, remoteIframe: true } 429 ); 430 431 // Test content editable 432 addAccessibleTask( 433 `<div id="input" contentEditable="true" tabindex="0" role="textbox" aria-multiline="true"><div id="inner"><br /></div></div>`, 434 async (browser, accDoc) => { 435 const inner = getNativeInterface(accDoc, "inner"); 436 const editableAncestor = inner.getAttributeValue("AXEditableAncestor"); 437 is( 438 editableAncestor.getAttributeValue("AXDOMIdentifier"), 439 "input", 440 "Editable ancestor is input" 441 ); 442 await focusIntoInputAndType(accDoc, "input"); 443 } 444 ); 445 446 // Test input that gets role::EDITCOMBOBOX 447 addAccessibleTask(`<input type="text" id="box">`, async (browser, accDoc) => { 448 const box = getNativeInterface(accDoc, "box"); 449 const editableAncestor = box.getAttributeValue("AXEditableAncestor"); 450 is( 451 editableAncestor.getAttributeValue("AXDOMIdentifier"), 452 "box", 453 "Editable ancestor is box itself" 454 ); 455 await focusIntoInputAndType(accDoc, "box"); 456 }); 457 458 // Test multiline caret control in a text area 459 addAccessibleTask( 460 `<textarea id="input" cols="15">one two three four five six seven eight</textarea>`, 461 async (browser, accDoc) => { 462 await focusIntoInput(accDoc, "input"); 463 464 await synthKeyAndTestSelectionChanged("KEY_ArrowRight", null, "input", "", { 465 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 466 AXTextSelectionDirection: AXTextSelectionDirectionNext, 467 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 468 }); 469 470 await synthKeyAndTestSelectionChanged("KEY_ArrowDown", null, "input", "", { 471 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 472 AXTextSelectionDirection: AXTextSelectionDirectionNext, 473 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 474 }); 475 476 await synthKeyAndTestSelectionChanged( 477 "KEY_ArrowLeft", 478 { metaKey: true }, 479 "input", 480 "", 481 { 482 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 483 AXTextSelectionDirection: AXTextSelectionDirectionBeginning, 484 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 485 } 486 ); 487 488 await synthKeyAndTestSelectionChanged( 489 "KEY_ArrowRight", 490 { metaKey: true }, 491 "input", 492 "", 493 { 494 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 495 AXTextSelectionDirection: AXTextSelectionDirectionEnd, 496 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 497 } 498 ); 499 }, 500 { topLevel: true, iframe: true, remoteIframe: true } 501 ); 502 503 /** 504 * Test that the caret returns the correct marker when it is positioned after 505 * the last character (to facilitate appending text). 506 */ 507 addAccessibleTask( 508 `<input id="input" value="abc">`, 509 async function (browser, docAcc) { 510 await focusIntoInput(docAcc, "input"); 511 512 let event = await synthKeyAndTestSelectionChanged( 513 "KEY_ArrowRight", 514 null, 515 "input", 516 "", 517 { 518 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 519 AXTextSelectionDirection: AXTextSelectionDirectionNext, 520 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 521 } 522 ); 523 testSelectionEventLeftChar(event, "a"); 524 event = await synthKeyAndTestSelectionChanged( 525 "KEY_ArrowRight", 526 null, 527 "input", 528 "", 529 { 530 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 531 AXTextSelectionDirection: AXTextSelectionDirectionNext, 532 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 533 } 534 ); 535 testSelectionEventLeftChar(event, "b"); 536 event = await synthKeyAndTestSelectionChanged( 537 "KEY_ArrowRight", 538 null, 539 "input", 540 "", 541 { 542 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 543 AXTextSelectionDirection: AXTextSelectionDirectionNext, 544 AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, 545 } 546 ); 547 testSelectionEventLeftChar(event, "c"); 548 }, 549 { chrome: true, topLevel: true } 550 ); 551 552 /** 553 * Test that the caret returns the correct line when the caret is at the start 554 * of the line. 555 */ 556 addAccessibleTask( 557 ` 558 <textarea id="hard">ab 559 cd 560 ef 561 562 gh 563 </textarea> 564 <div role="textbox" id="wrapped" contenteditable style="width: 1ch;">a b c</div> 565 `, 566 async function (browser, docAcc) { 567 let hard = getNativeInterface(docAcc, "hard"); 568 await focusIntoInput(docAcc, "hard"); 569 is(hard.getAttributeValue("AXInsertionPointLineNumber"), 0); 570 let event = await synthKeyAndTestSelectionChanged( 571 "KEY_ArrowDown", 572 null, 573 "hard", 574 "", 575 { 576 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 577 AXTextSelectionDirection: AXTextSelectionDirectionNext, 578 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 579 } 580 ); 581 testSelectionEventLine(event, "cd"); 582 is(hard.getAttributeValue("AXInsertionPointLineNumber"), 1); 583 event = await synthKeyAndTestSelectionChanged( 584 "KEY_ArrowDown", 585 null, 586 "hard", 587 "", 588 { 589 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 590 AXTextSelectionDirection: AXTextSelectionDirectionNext, 591 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 592 } 593 ); 594 testSelectionEventLine(event, "ef"); 595 is(hard.getAttributeValue("AXInsertionPointLineNumber"), 2); 596 event = await synthKeyAndTestSelectionChanged( 597 "KEY_ArrowDown", 598 null, 599 "hard", 600 "", 601 { 602 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 603 AXTextSelectionDirection: AXTextSelectionDirectionNext, 604 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 605 } 606 ); 607 testSelectionEventLine(event, ""); 608 is(hard.getAttributeValue("AXInsertionPointLineNumber"), 3); 609 event = await synthKeyAndTestSelectionChanged( 610 "KEY_ArrowDown", 611 null, 612 "hard", 613 "", 614 { 615 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 616 AXTextSelectionDirection: AXTextSelectionDirectionNext, 617 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 618 } 619 ); 620 testSelectionEventLine(event, "gh"); 621 is(hard.getAttributeValue("AXInsertionPointLineNumber"), 4); 622 event = await synthKeyAndTestSelectionChanged( 623 "KEY_ArrowDown", 624 null, 625 "hard", 626 "", 627 { 628 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 629 AXTextSelectionDirection: AXTextSelectionDirectionNext, 630 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 631 } 632 ); 633 testSelectionEventLine(event, ""); 634 is(hard.getAttributeValue("AXInsertionPointLineNumber"), 5); 635 636 let wrapped = getNativeInterface(docAcc, "wrapped"); 637 await focusIntoInput(docAcc, "wrapped"); 638 is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 0); 639 event = await synthKeyAndTestSelectionChanged( 640 "KEY_ArrowDown", 641 null, 642 "wrapped", 643 "", 644 { 645 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 646 AXTextSelectionDirection: AXTextSelectionDirectionNext, 647 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 648 } 649 ); 650 testSelectionEventLine(event, "b "); 651 is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 1); 652 event = await synthKeyAndTestSelectionChanged( 653 "KEY_ArrowDown", 654 null, 655 "wrapped", 656 "", 657 { 658 AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, 659 AXTextSelectionDirection: AXTextSelectionDirectionNext, 660 AXTextSelectionGranularity: AXTextSelectionGranularityLine, 661 } 662 ); 663 testSelectionEventLine(event, "c"); 664 is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 2); 665 }, 666 { chrome: true, topLevel: true } 667 );