events.js (71973B)
1 /* import-globals-from common.js */ 2 /* import-globals-from states.js */ 3 /* import-globals-from text.js */ 4 5 // XXX Bug 1425371 - enable no-redeclare and fix the issues with the tests. 6 /* eslint-disable no-redeclare */ 7 8 // ////////////////////////////////////////////////////////////////////////////// 9 // Constants 10 11 const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; 12 const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT; 13 const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; 14 const EVENT_DOCUMENT_LOAD_COMPLETE = 15 nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; 16 const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; 17 const EVENT_DOCUMENT_LOAD_STOPPED = 18 nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED; 19 const EVENT_ERRORMESSAGE_CHANGED = 20 nsIAccessibleEvent.EVENT_ERRORMESSAGE_CHANGED; 21 const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; 22 const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; 23 const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; 24 const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; 25 const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; 26 const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; 27 const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; 28 const EVENT_OBJECT_ATTRIBUTE_CHANGED = 29 nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; 30 const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; 31 const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; 32 const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; 33 const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; 34 const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; 35 const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; 36 const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; 37 const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; 38 const EVENT_TEXT_ATTRIBUTE_CHANGED = 39 nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; 40 const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; 41 const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; 42 const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; 43 const EVENT_TEXT_SELECTION_CHANGED = 44 nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; 45 const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; 46 const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE; 47 const EVENT_VIRTUALCURSOR_CHANGED = 48 nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; 49 50 const kNotFromUserInput = 0; 51 const kFromUserInput = 1; 52 53 // ////////////////////////////////////////////////////////////////////////////// 54 // General 55 56 /** 57 * Set up this variable to dump events into DOM. 58 */ 59 var gA11yEventDumpID = ""; 60 61 /** 62 * Set up this variable to dump event processing into console. 63 */ 64 var gA11yEventDumpToConsole = false; 65 66 /** 67 * Set up this variable to dump event processing into error console. 68 */ 69 var gA11yEventDumpToAppConsole = false; 70 71 /** 72 * Semicolon separated set of logging features. 73 */ 74 var gA11yEventDumpFeature = ""; 75 76 /** 77 * Function to detect HTML elements when given a node. 78 */ 79 function isHTMLElement(aNode) { 80 return ( 81 aNode.nodeType == aNode.ELEMENT_NODE && 82 aNode.namespaceURI == "http://www.w3.org/1999/xhtml" 83 ); 84 } 85 86 function isXULElement(aNode) { 87 return ( 88 aNode.nodeType == aNode.ELEMENT_NODE && 89 aNode.namespaceURI == 90 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" 91 ); 92 } 93 94 /** 95 * Executes the function when requested event is handled. 96 * 97 * @param aEventType [in] event type 98 * @param aTarget [in] event target 99 * @param aFunc [in] function to call when event is handled 100 * @param aContext [in, optional] object in which context the function is 101 * called 102 * @param aArg1 [in, optional] argument passed into the function 103 * @param aArg2 [in, optional] argument passed into the function 104 */ 105 function waitForEvent( 106 aEventType, 107 aTargetOrFunc, 108 aFunc, 109 aContext, 110 aArg1, 111 aArg2 112 ) { 113 var handler = { 114 handleEvent: function handleEvent(aEvent) { 115 var target = aTargetOrFunc; 116 if (typeof aTargetOrFunc == "function") { 117 target = aTargetOrFunc.call(); 118 } 119 120 if (target) { 121 if (target instanceof nsIAccessible && target != aEvent.accessible) { 122 return; 123 } 124 125 if (Node.isInstance(target) && target != aEvent.DOMNode) { 126 return; 127 } 128 } 129 130 unregisterA11yEventListener(aEventType, this); 131 132 window.setTimeout(function () { 133 aFunc.call(aContext, aArg1, aArg2); 134 }, 0); 135 }, 136 }; 137 138 registerA11yEventListener(aEventType, handler); 139 } 140 141 /** 142 * Generate mouse move over image map what creates image map accessible (async). 143 * See waitForImageMap() function. 144 */ 145 function waveOverImageMap(aImageMapID) { 146 var imageMapNode = getNode(aImageMapID); 147 synthesizeMouse( 148 imageMapNode, 149 10, 150 10, 151 { type: "mousemove" }, 152 imageMapNode.ownerGlobal 153 ); 154 } 155 156 /** 157 * Call the given function when the tree of the given image map is built. 158 */ 159 function waitForImageMap(aImageMapID, aTestFunc) { 160 waveOverImageMap(aImageMapID); 161 162 var imageMapAcc = getAccessible(aImageMapID); 163 if (imageMapAcc.firstChild) { 164 aTestFunc(); 165 return; 166 } 167 168 waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc); 169 } 170 171 /** 172 * Register accessibility event listener. 173 * 174 * @param aEventType the accessible event type (see nsIAccessibleEvent for 175 * available constants). 176 * @param aEventHandler event listener object, when accessible event of the 177 * given type is handled then 'handleEvent' method of 178 * this object is invoked with nsIAccessibleEvent object 179 * as the first argument. 180 */ 181 function registerA11yEventListener(aEventType, aEventHandler) { 182 listenA11yEvents(true); 183 addA11yEventListener(aEventType, aEventHandler); 184 } 185 186 /** 187 * Unregister accessibility event listener. Must be called for every registered 188 * event listener (see registerA11yEventListener() function) when the listener 189 * is not needed. 190 */ 191 function unregisterA11yEventListener(aEventType, aEventHandler) { 192 removeA11yEventListener(aEventType, aEventHandler); 193 listenA11yEvents(false); 194 } 195 196 // ////////////////////////////////////////////////////////////////////////////// 197 // Event queue 198 199 /** 200 * Return value of invoke method of invoker object. Indicates invoker was unable 201 * to prepare action. 202 */ 203 const INVOKER_ACTION_FAILED = 1; 204 205 /** 206 * Return value of eventQueue.onFinish. Indicates eventQueue should not finish 207 * tests. 208 */ 209 const DO_NOT_FINISH_TEST = 1; 210 211 /** 212 * Creates event queue for the given event type. The queue consists of invoker 213 * objects, each of them generates the event of the event type. When queue is 214 * started then every invoker object is asked to generate event after timeout. 215 * When event is caught then current invoker object is asked to check whether 216 * event was handled correctly. 217 * 218 * Invoker interface is: 219 * 220 * ```js 221 * var invoker = { 222 * // Generates accessible event or event sequence. If returns 223 * // INVOKER_ACTION_FAILED constant then stop tests. 224 * invoke: function(){}, 225 * 226 * // [optional] Invoker's check of handled event for correctness. 227 * check: function(aEvent){}, 228 * 229 * // [optional] Invoker's check before the next invoker is proceeded. 230 * finalCheck: function(aEvent){}, 231 * 232 * // [optional] Is called when event of any registered type is handled. 233 * debugCheck: function(aEvent){}, 234 * 235 * // [ignored if 'eventSeq' is defined] DOM node event is generated for 236 * // (used in the case when invoker expects single event). 237 * DOMNode getter: function() {}, 238 * 239 * // [optional] if true then event sequences are ignored (no failure if 240 * // sequences are empty). Use you need to invoke an action, do some check 241 * // after timeout and proceed a next invoker. 242 * noEventsOnAction getter: function() {}, 243 * 244 * // Array of checker objects defining expected events on invoker's action. 245 * // 246 * // Checker object interface: 247 * // 248 * // var checker = { 249 * // * DOM or a11y event type. * 250 * // type getter: function() {}, 251 * // 252 * // * DOM node or accessible. * 253 * // target getter: function() {}, 254 * // 255 * // * DOM event phase (false - bubbling). * 256 * // phase getter: function() {}, 257 * // 258 * // * Callback, called to match handled event. * 259 * // match : function(aEvent) {}, 260 * // 261 * // * Callback, called when event is handled 262 * // check: function(aEvent) {}, 263 * // 264 * // * Checker ID * 265 * // getID: function() {}, 266 * // 267 * // * Event that don't have predefined order relative other events. * 268 * // async getter: function() {}, 269 * // 270 * // * Event that is not expected. * 271 * // unexpected getter: function() {}, 272 * // 273 * // * No other event of the same type is not allowed. * 274 * // unique getter: function() {} 275 * // }; 276 * eventSeq getter() {}, 277 * 278 * // Array of checker objects defining unexpected events on invoker's 279 * // action. 280 * unexpectedEventSeq getter() {}, 281 * 282 * // The ID of invoker. 283 * getID: function(){} // returns invoker ID 284 * }; 285 * 286 * // Used to add a possible scenario of expected/unexpected events on 287 * // invoker's action. 288 * defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq) 289 * ``` 290 * 291 * @param aEventType [in, optional] the default event type (isn't used if 292 * invoker defines eventSeq property). 293 */ 294 function eventQueue(aEventType) { 295 // public 296 297 /** 298 * Add invoker object into queue. 299 */ 300 this.push = function eventQueue_push(aEventInvoker) { 301 this.mInvokers.push(aEventInvoker); 302 }; 303 304 /** 305 * Start the queue processing. 306 */ 307 this.invoke = function eventQueue_invoke() { 308 listenA11yEvents(true); 309 310 // XXX: Intermittent test_events_caretmove.html fails withouth timeout, 311 // see bug 474952. 312 this.processNextInvokerInTimeout(true); 313 }; 314 315 /** 316 * This function is called when all events in the queue were handled. 317 * Override it if you need to be notified of this. 318 */ 319 this.onFinish = function eventQueue_finish() {}; 320 321 // private 322 323 /** 324 * Process next invoker. 325 */ 326 // eslint-disable-next-line complexity 327 this.processNextInvoker = function eventQueue_processNextInvoker() { 328 // Some scenario was matched, we wait on next invoker processing. 329 if (this.mNextInvokerStatus == kInvokerCanceled) { 330 this.setInvokerStatus( 331 kInvokerNotScheduled, 332 "scenario was matched, wait for next invoker activation" 333 ); 334 return; 335 } 336 337 this.setInvokerStatus( 338 kInvokerNotScheduled, 339 "the next invoker is processed now" 340 ); 341 342 // Finish processing of the current invoker if any. 343 var testFailed = false; 344 345 var invoker = this.getInvoker(); 346 if (invoker) { 347 if ("finalCheck" in invoker) { 348 invoker.finalCheck(); 349 } 350 351 if (this.mScenarios && this.mScenarios.length) { 352 var matchIdx = -1; 353 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 354 var eventSeq = this.mScenarios[scnIdx]; 355 if (!this.areExpectedEventsLeft(eventSeq)) { 356 for (var idx = 0; idx < eventSeq.length; idx++) { 357 var checker = eventSeq[idx]; 358 if ( 359 (checker.unexpected && checker.wasCaught) || 360 (!checker.unexpected && checker.wasCaught != 1) 361 ) { 362 break; 363 } 364 } 365 366 // Ok, we have matched scenario. Report it was completed ok. In 367 // case of empty scenario guess it was matched but if later we 368 // find out that non empty scenario was matched then it will be 369 // a final match. 370 if (idx == eventSeq.length) { 371 if ( 372 matchIdx != -1 && 373 !!eventSeq.length && 374 this.mScenarios[matchIdx].length 375 ) { 376 ok( 377 false, 378 "We have a matched scenario at index " + 379 matchIdx + 380 " already." 381 ); 382 } 383 384 if (matchIdx == -1 || eventSeq.length) { 385 matchIdx = scnIdx; 386 } 387 388 // Report everything is ok. 389 for (var idx = 0; idx < eventSeq.length; idx++) { 390 var checker = eventSeq[idx]; 391 392 var typeStr = eventQueue.getEventTypeAsString(checker); 393 var msg = 394 "Test with ID = '" + this.getEventID(checker) + "' succeed. "; 395 396 if (checker.unexpected) { 397 ok(true, msg + `There's no unexpected '${typeStr}' event.`); 398 } else if (checker.todo) { 399 todo(false, `Todo event '${typeStr}' was caught`); 400 } else { 401 ok(true, `${msg} Event '${typeStr}' was handled.`); 402 } 403 } 404 } 405 } 406 } 407 408 // We don't have completely matched scenario. Report each failure/success 409 // for every scenario. 410 if (matchIdx == -1) { 411 testFailed = true; 412 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 413 var eventSeq = this.mScenarios[scnIdx]; 414 for (var idx = 0; idx < eventSeq.length; idx++) { 415 var checker = eventSeq[idx]; 416 417 var typeStr = eventQueue.getEventTypeAsString(checker); 418 var msg = 419 "Scenario #" + 420 scnIdx + 421 " of test with ID = '" + 422 this.getEventID(checker) + 423 "' failed. "; 424 425 if (checker.wasCaught > 1) { 426 ok(false, msg + "Dupe " + typeStr + " event."); 427 } 428 429 if (checker.unexpected) { 430 if (checker.wasCaught) { 431 ok(false, msg + "There's unexpected " + typeStr + " event."); 432 } 433 } else if (!checker.wasCaught) { 434 var rf = checker.todo ? todo : ok; 435 rf(false, `${msg} '${typeStr} event is missed.`); 436 } 437 } 438 } 439 } 440 } 441 } 442 443 this.clearEventHandler(); 444 445 // Check if need to stop the test. 446 if (testFailed || this.mIndex == this.mInvokers.length - 1) { 447 listenA11yEvents(false); 448 449 var res = this.onFinish(); 450 if (res != DO_NOT_FINISH_TEST) { 451 SimpleTest.executeSoon(SimpleTest.finish); 452 } 453 454 return; 455 } 456 457 // Start processing of next invoker. 458 invoker = this.getNextInvoker(); 459 460 // Set up event listeners. Process a next invoker if no events were added. 461 if (!this.setEventHandler(invoker)) { 462 this.processNextInvoker(); 463 return; 464 } 465 466 if (gLogger.isEnabled()) { 467 gLogger.logToConsole("Event queue: \n invoke: " + invoker.getID()); 468 gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true); 469 } 470 471 var infoText = "Invoke the '" + invoker.getID() + "' test { "; 472 var scnCount = this.mScenarios ? this.mScenarios.length : 0; 473 for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) { 474 infoText += "scenario #" + scnIdx + ": "; 475 var eventSeq = this.mScenarios[scnIdx]; 476 for (var idx = 0; idx < eventSeq.length; idx++) { 477 infoText += eventSeq[idx].unexpected 478 ? "un" 479 : "" + 480 "expected '" + 481 eventQueue.getEventTypeAsString(eventSeq[idx]) + 482 "' event; "; 483 } 484 } 485 infoText += " }"; 486 info(infoText); 487 488 if (invoker.invoke() == INVOKER_ACTION_FAILED) { 489 // Invoker failed to prepare action, fail and finish tests. 490 this.processNextInvoker(); 491 return; 492 } 493 494 if (this.hasUnexpectedEventsScenario()) { 495 this.processNextInvokerInTimeout(true); 496 } 497 }; 498 499 this.processNextInvokerInTimeout = 500 function eventQueue_processNextInvokerInTimeout(aUncondProcess) { 501 this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout"); 502 503 // No need to wait extra timeout when a) we know we don't need to do that 504 // and b) there's no any single unexpected event. 505 if (!aUncondProcess && this.areAllEventsExpected()) { 506 // We need delay to avoid events coalesce from different invokers. 507 var queue = this; 508 SimpleTest.executeSoon(function () { 509 queue.processNextInvoker(); 510 }); 511 return; 512 } 513 514 // Check in timeout invoker didn't fire registered events. 515 window.setTimeout( 516 function (aQueue) { 517 aQueue.processNextInvoker(); 518 }, 519 300, 520 this 521 ); 522 }; 523 524 /** 525 * Handle events for the current invoker. 526 */ 527 // eslint-disable-next-line complexity 528 this.handleEvent = function eventQueue_handleEvent(aEvent) { 529 var invoker = this.getInvoker(); 530 if (!invoker) { 531 // skip events before test was started 532 return; 533 } 534 535 if (!this.mScenarios) { 536 // Bad invoker object, error will be reported before processing of next 537 // invoker in the queue. 538 this.processNextInvoker(); 539 return; 540 } 541 542 if ("debugCheck" in invoker) { 543 invoker.debugCheck(aEvent); 544 } 545 546 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 547 var eventSeq = this.mScenarios[scnIdx]; 548 for (var idx = 0; idx < eventSeq.length; idx++) { 549 var checker = eventSeq[idx]; 550 551 // Search through handled expected events to report error if one of them 552 // is handled for a second time. 553 if ( 554 !checker.unexpected && 555 checker.wasCaught > 0 && 556 eventQueue.isSameEvent(checker, aEvent) 557 ) { 558 checker.wasCaught++; 559 continue; 560 } 561 562 // Search through unexpected events, any match results in error report 563 // after this invoker processing (in case of matched scenario only). 564 if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) { 565 checker.wasCaught++; 566 continue; 567 } 568 569 // Report an error if we handled not expected event of unique type 570 // (i.e. event types are matched, targets differs). 571 if ( 572 !checker.unexpected && 573 checker.unique && 574 eventQueue.compareEventTypes(checker, aEvent) 575 ) { 576 var isExpected = false; 577 for (var jdx = 0; jdx < eventSeq.length; jdx++) { 578 isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent); 579 if (isExpected) { 580 break; 581 } 582 } 583 584 if (!isExpected) { 585 ok( 586 false, 587 "Unique type " + 588 eventQueue.getEventTypeAsString(checker) + 589 " event was handled." 590 ); 591 } 592 } 593 } 594 } 595 596 var hasMatchedCheckers = false; 597 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 598 var eventSeq = this.mScenarios[scnIdx]; 599 600 // Check if handled event matches expected sync event. 601 var nextChecker = this.getNextExpectedEvent(eventSeq); 602 if (nextChecker) { 603 if (eventQueue.compareEvents(nextChecker, aEvent)) { 604 this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); 605 hasMatchedCheckers = true; 606 continue; 607 } 608 } 609 610 // Check if handled event matches any expected async events. 611 var haveUnmatchedAsync = false; 612 for (idx = 0; idx < eventSeq.length; idx++) { 613 if (eventSeq[idx] instanceof orderChecker && haveUnmatchedAsync) { 614 break; 615 } 616 617 if (!eventSeq[idx].wasCaught) { 618 haveUnmatchedAsync = true; 619 } 620 621 if (!eventSeq[idx].unexpected && eventSeq[idx].async) { 622 if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { 623 this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); 624 hasMatchedCheckers = true; 625 break; 626 } 627 } 628 } 629 } 630 631 if (hasMatchedCheckers) { 632 var invoker = this.getInvoker(); 633 if ("check" in invoker) { 634 invoker.check(aEvent); 635 } 636 } 637 638 for (idx = 0; idx < eventSeq.length; idx++) { 639 if (!eventSeq[idx].wasCaught) { 640 if (eventSeq[idx] instanceof orderChecker) { 641 eventSeq[idx].wasCaught++; 642 } else { 643 break; 644 } 645 } 646 } 647 648 // If we don't have more events to wait then schedule next invoker. 649 if (this.hasMatchedScenario()) { 650 if (this.mNextInvokerStatus == kInvokerNotScheduled) { 651 this.processNextInvokerInTimeout(); 652 } else if (this.mNextInvokerStatus == kInvokerCanceled) { 653 this.setInvokerStatus( 654 kInvokerPending, 655 "Full match. Void the cancelation of next invoker processing" 656 ); 657 } 658 return; 659 } 660 661 // If we have scheduled a next invoker then cancel in case of match. 662 if (this.mNextInvokerStatus == kInvokerPending && hasMatchedCheckers) { 663 this.setInvokerStatus( 664 kInvokerCanceled, 665 "Cancel the scheduled invoker in case of match" 666 ); 667 } 668 }; 669 670 // Helpers 671 this.processMatchedChecker = function eventQueue_function( 672 aEvent, 673 aMatchedChecker, 674 aScenarioIdx, 675 aEventIdx 676 ) { 677 aMatchedChecker.wasCaught++; 678 679 if ("check" in aMatchedChecker) { 680 aMatchedChecker.check(aEvent); 681 } 682 683 eventQueue.logEvent( 684 aEvent, 685 aMatchedChecker, 686 aScenarioIdx, 687 aEventIdx, 688 this.areExpectedEventsLeft(), 689 this.mNextInvokerStatus 690 ); 691 }; 692 693 this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent( 694 aEventSeq 695 ) { 696 if (!("idx" in aEventSeq)) { 697 aEventSeq.idx = 0; 698 } 699 700 while ( 701 aEventSeq.idx < aEventSeq.length && 702 (aEventSeq[aEventSeq.idx].unexpected || 703 aEventSeq[aEventSeq.idx].todo || 704 aEventSeq[aEventSeq.idx].async || 705 aEventSeq[aEventSeq.idx] instanceof orderChecker || 706 aEventSeq[aEventSeq.idx].wasCaught > 0) 707 ) { 708 aEventSeq.idx++; 709 } 710 711 return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null; 712 }; 713 714 this.areExpectedEventsLeft = function eventQueue_areExpectedEventsLeft( 715 aScenario 716 ) { 717 function scenarioHasUnhandledExpectedEvent(aEventSeq) { 718 // Check if we have unhandled async (can be anywhere in the sequance) or 719 // sync expcected events yet. 720 for (var idx = 0; idx < aEventSeq.length; idx++) { 721 if ( 722 !aEventSeq[idx].unexpected && 723 !aEventSeq[idx].todo && 724 !aEventSeq[idx].wasCaught && 725 !(aEventSeq[idx] instanceof orderChecker) 726 ) { 727 return true; 728 } 729 } 730 731 return false; 732 } 733 734 if (aScenario) { 735 return scenarioHasUnhandledExpectedEvent(aScenario); 736 } 737 738 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 739 var eventSeq = this.mScenarios[scnIdx]; 740 if (scenarioHasUnhandledExpectedEvent(eventSeq)) { 741 return true; 742 } 743 } 744 return false; 745 }; 746 747 this.areAllEventsExpected = function eventQueue_areAllEventsExpected() { 748 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 749 var eventSeq = this.mScenarios[scnIdx]; 750 for (var idx = 0; idx < eventSeq.length; idx++) { 751 if (eventSeq[idx].unexpected || eventSeq[idx].todo) { 752 return false; 753 } 754 } 755 } 756 757 return true; 758 }; 759 760 this.isUnexpectedEventScenario = 761 function eventQueue_isUnexpectedEventsScenario(aScenario) { 762 for (var idx = 0; idx < aScenario.length; idx++) { 763 if (!aScenario[idx].unexpected && !aScenario[idx].todo) { 764 break; 765 } 766 } 767 768 return idx == aScenario.length; 769 }; 770 771 this.hasUnexpectedEventsScenario = 772 function eventQueue_hasUnexpectedEventsScenario() { 773 if (this.getInvoker().noEventsOnAction) { 774 return true; 775 } 776 777 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 778 if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) { 779 return true; 780 } 781 } 782 783 return false; 784 }; 785 786 this.hasMatchedScenario = function eventQueue_hasMatchedScenario() { 787 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 788 var scn = this.mScenarios[scnIdx]; 789 if ( 790 !this.isUnexpectedEventScenario(scn) && 791 !this.areExpectedEventsLeft(scn) 792 ) { 793 return true; 794 } 795 } 796 return false; 797 }; 798 799 this.getInvoker = function eventQueue_getInvoker() { 800 return this.mInvokers[this.mIndex]; 801 }; 802 803 this.getNextInvoker = function eventQueue_getNextInvoker() { 804 return this.mInvokers[++this.mIndex]; 805 }; 806 807 this.setEventHandler = function eventQueue_setEventHandler(aInvoker) { 808 if (!("scenarios" in aInvoker) || !aInvoker.scenarios.length) { 809 var eventSeq = aInvoker.eventSeq; 810 var unexpectedEventSeq = aInvoker.unexpectedEventSeq; 811 if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) { 812 eventSeq = [new invokerChecker(this.mDefEventType, aInvoker.DOMNode)]; 813 } 814 815 if (eventSeq || unexpectedEventSeq) { 816 defineScenario(aInvoker, eventSeq, unexpectedEventSeq); 817 } 818 } 819 820 if (aInvoker.noEventsOnAction) { 821 return true; 822 } 823 824 this.mScenarios = aInvoker.scenarios; 825 if (!this.mScenarios || !this.mScenarios.length) { 826 ok(false, "Broken invoker '" + aInvoker.getID() + "'"); 827 return false; 828 } 829 830 // Register event listeners. 831 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 832 var eventSeq = this.mScenarios[scnIdx]; 833 834 if (gLogger.isEnabled()) { 835 var msg = 836 "scenario #" + 837 scnIdx + 838 ", registered events number: " + 839 eventSeq.length; 840 gLogger.logToConsole(msg); 841 gLogger.logToDOM(msg, true); 842 } 843 844 // Do not warn about empty event sequances when more than one scenario 845 // was registered. 846 if (this.mScenarios.length == 1 && !eventSeq.length) { 847 ok( 848 false, 849 "Broken scenario #" + 850 scnIdx + 851 " of invoker '" + 852 aInvoker.getID() + 853 "'. No registered events" 854 ); 855 return false; 856 } 857 858 for (var idx = 0; idx < eventSeq.length; idx++) { 859 eventSeq[idx].wasCaught = 0; 860 } 861 862 for (var idx = 0; idx < eventSeq.length; idx++) { 863 if (gLogger.isEnabled()) { 864 var msg = "registered"; 865 if (eventSeq[idx].unexpected) { 866 msg += " unexpected"; 867 } 868 if (eventSeq[idx].async) { 869 msg += " async"; 870 } 871 872 msg += 873 ": event type: " + 874 eventQueue.getEventTypeAsString(eventSeq[idx]) + 875 ", target: " + 876 eventQueue.getEventTargetDescr(eventSeq[idx], true); 877 878 gLogger.logToConsole(msg); 879 gLogger.logToDOM(msg, true); 880 } 881 882 var eventType = eventSeq[idx].type; 883 if (typeof eventType == "string") { 884 // DOM event 885 var target = eventQueue.getEventTarget(eventSeq[idx]); 886 if (!target) { 887 ok(false, "no target for DOM event!"); 888 return false; 889 } 890 var phase = eventQueue.getEventPhase(eventSeq[idx]); 891 target.addEventListener(eventType, this, phase); 892 } else { 893 // A11y event 894 addA11yEventListener(eventType, this); 895 } 896 } 897 } 898 899 return true; 900 }; 901 902 this.clearEventHandler = function eventQueue_clearEventHandler() { 903 if (!this.mScenarios) { 904 return; 905 } 906 907 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { 908 var eventSeq = this.mScenarios[scnIdx]; 909 for (var idx = 0; idx < eventSeq.length; idx++) { 910 var eventType = eventSeq[idx].type; 911 if (typeof eventType == "string") { 912 // DOM event 913 var target = eventQueue.getEventTarget(eventSeq[idx]); 914 var phase = eventQueue.getEventPhase(eventSeq[idx]); 915 target.removeEventListener(eventType, this, phase); 916 } else { 917 // A11y event 918 removeA11yEventListener(eventType, this); 919 } 920 } 921 } 922 this.mScenarios = null; 923 }; 924 925 this.getEventID = function eventQueue_getEventID(aChecker) { 926 if ("getID" in aChecker) { 927 return aChecker.getID(); 928 } 929 930 var invoker = this.getInvoker(); 931 return invoker.getID(); 932 }; 933 934 this.setInvokerStatus = function eventQueue_setInvokerStatus(aStatus) { 935 this.mNextInvokerStatus = aStatus; 936 937 // Uncomment it to debug invoker processing logic. 938 // gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg)); 939 }; 940 941 this.mDefEventType = aEventType; 942 943 this.mInvokers = []; 944 this.mIndex = -1; 945 this.mScenarios = null; 946 947 this.mNextInvokerStatus = kInvokerNotScheduled; 948 } 949 950 // ////////////////////////////////////////////////////////////////////////////// 951 // eventQueue static members and constants 952 953 const kInvokerNotScheduled = 0; 954 const kInvokerPending = 1; 955 const kInvokerCanceled = 2; 956 957 eventQueue.getEventTypeAsString = function eventQueue_getEventTypeAsString( 958 aEventOrChecker 959 ) { 960 if (Event.isInstance(aEventOrChecker)) { 961 return aEventOrChecker.type; 962 } 963 964 if (aEventOrChecker instanceof nsIAccessibleEvent) { 965 return eventTypeToString(aEventOrChecker.eventType); 966 } 967 968 return typeof aEventOrChecker.type == "string" 969 ? aEventOrChecker.type 970 : eventTypeToString(aEventOrChecker.type); 971 }; 972 973 eventQueue.getEventTargetDescr = function eventQueue_getEventTargetDescr( 974 aEventOrChecker, 975 aDontForceTarget 976 ) { 977 if (Event.isInstance(aEventOrChecker)) { 978 return prettyName(aEventOrChecker.originalTarget); 979 } 980 981 // XXXbz this block doesn't seem to be reachable... 982 if (Event.isInstance(aEventOrChecker)) { 983 return prettyName(aEventOrChecker.accessible); 984 } 985 986 var descr = aEventOrChecker.targetDescr; 987 if (descr) { 988 return descr; 989 } 990 991 if (aDontForceTarget) { 992 return "no target description"; 993 } 994 995 var target = "target" in aEventOrChecker ? aEventOrChecker.target : null; 996 return prettyName(target); 997 }; 998 999 eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) { 1000 return "phase" in aChecker ? aChecker.phase : true; 1001 }; 1002 1003 eventQueue.getEventTarget = function eventQueue_getEventTarget(aChecker) { 1004 if ("eventTarget" in aChecker) { 1005 switch (aChecker.eventTarget) { 1006 case "element": 1007 return aChecker.target; 1008 case "document": 1009 default: 1010 return aChecker.target.ownerDocument; 1011 } 1012 } 1013 return aChecker.target.ownerDocument; 1014 }; 1015 1016 eventQueue.compareEventTypes = function eventQueue_compareEventTypes( 1017 aChecker, 1018 aEvent 1019 ) { 1020 var eventType = Event.isInstance(aEvent) ? aEvent.type : aEvent.eventType; 1021 return aChecker.type == eventType; 1022 }; 1023 1024 eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) { 1025 if (!eventQueue.compareEventTypes(aChecker, aEvent)) { 1026 return false; 1027 } 1028 1029 // If checker provides "match" function then allow the checker to decide 1030 // whether event is matched. 1031 if ("match" in aChecker) { 1032 return aChecker.match(aEvent); 1033 } 1034 1035 var target1 = aChecker.target; 1036 if (target1 instanceof nsIAccessible) { 1037 var target2 = Event.isInstance(aEvent) 1038 ? getAccessible(aEvent.target) 1039 : aEvent.accessible; 1040 1041 return target1 == target2; 1042 } 1043 1044 // If original target isn't suitable then extend interface to support target 1045 // (original target is used in test_elm_media.html). 1046 var target2 = Event.isInstance(aEvent) 1047 ? aEvent.originalTarget 1048 : aEvent.DOMNode; 1049 return target1 == target2; 1050 }; 1051 1052 eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) { 1053 // We don't have stored info about handled event other than its type and 1054 // target, thus we should filter text change and state change events since 1055 // they may occur on the same element because of complex changes. 1056 return ( 1057 this.compareEvents(aChecker, aEvent) && 1058 !(aEvent instanceof nsIAccessibleTextChangeEvent) && 1059 !(aEvent instanceof nsIAccessibleStateChangeEvent) 1060 ); 1061 }; 1062 1063 eventQueue.invokerStatusToMsg = function eventQueue_invokerStatusToMsg( 1064 aInvokerStatus, 1065 aMsg 1066 ) { 1067 var msg = "invoker status: "; 1068 switch (aInvokerStatus) { 1069 case kInvokerNotScheduled: 1070 msg += "not scheduled"; 1071 break; 1072 case kInvokerPending: 1073 msg += "pending"; 1074 break; 1075 case kInvokerCanceled: 1076 msg += "canceled"; 1077 break; 1078 } 1079 1080 if (aMsg) { 1081 msg += " (" + aMsg + ")"; 1082 } 1083 1084 return msg; 1085 }; 1086 1087 eventQueue.logEvent = function eventQueue_logEvent( 1088 aOrigEvent, 1089 aMatchedChecker, 1090 aScenarioIdx, 1091 aEventIdx, 1092 aAreExpectedEventsLeft, 1093 aInvokerStatus 1094 ) { 1095 // Dump DOM event information. Skip a11y event since it is dumped by 1096 // gA11yEventObserver. 1097 if (Event.isInstance(aOrigEvent)) { 1098 var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent); 1099 info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent); 1100 gLogger.logToDOM(info); 1101 } 1102 1103 var infoMsg = 1104 "unhandled expected events: " + 1105 aAreExpectedEventsLeft + 1106 ", " + 1107 eventQueue.invokerStatusToMsg(aInvokerStatus); 1108 1109 var currType = eventQueue.getEventTypeAsString(aMatchedChecker); 1110 var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); 1111 var consoleMsg = 1112 "*****\nScenario " + 1113 aScenarioIdx + 1114 ", event " + 1115 aEventIdx + 1116 " matched: " + 1117 currType + 1118 "\n" + 1119 infoMsg + 1120 "\n*****"; 1121 gLogger.logToConsole(consoleMsg); 1122 1123 var emphText = "matched "; 1124 var msg = 1125 "EQ event, type: " + 1126 currType + 1127 ", target: " + 1128 currTargetDescr + 1129 ", " + 1130 infoMsg; 1131 gLogger.logToDOM(msg, true, emphText); 1132 }; 1133 1134 // ////////////////////////////////////////////////////////////////////////////// 1135 // Action sequence 1136 1137 /** 1138 * Deal with action sequence. Used when you need to execute couple of actions 1139 * each after other one. 1140 */ 1141 function sequence() { 1142 /** 1143 * Append new sequence item. 1144 * 1145 * @param aProcessor [in] object implementing interface 1146 * { 1147 * // execute item action 1148 * process: function() {}, 1149 * // callback, is called when item was processed 1150 * onProcessed: function() {} 1151 * }; 1152 * @param aEventType [in] event type of expected event on item action 1153 * @param aTarget [in] event target of expected event on item action 1154 * @param aItemID [in] identifier of item 1155 */ 1156 this.append = function sequence_append( 1157 aProcessor, 1158 aEventType, 1159 aTarget, 1160 aItemID 1161 ) { 1162 var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID); 1163 this.items.push(item); 1164 }; 1165 1166 /** 1167 * Process next sequence item. 1168 */ 1169 this.processNext = function sequence_processNext() { 1170 this.idx++; 1171 if (this.idx >= this.items.length) { 1172 ok(false, "End of sequence: nothing to process!"); 1173 SimpleTest.finish(); 1174 return; 1175 } 1176 1177 this.items[this.idx].startProcess(); 1178 }; 1179 1180 this.items = []; 1181 this.idx = -1; 1182 } 1183 1184 // ////////////////////////////////////////////////////////////////////////////// 1185 // Event queue invokers 1186 1187 /** 1188 * Defines a scenario of expected/unexpected events. Each invoker can have 1189 * one or more scenarios of events. Only one scenario must be completed. 1190 */ 1191 function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) { 1192 if (!("scenarios" in aInvoker)) { 1193 aInvoker.scenarios = []; 1194 } 1195 1196 // Create unified event sequence concatenating expected and unexpected 1197 // events. 1198 if (!aEventSeq) { 1199 aEventSeq = []; 1200 } 1201 1202 for (var idx = 0; idx < aEventSeq.length; idx++) { 1203 aEventSeq[idx].unexpected |= false; 1204 aEventSeq[idx].async |= false; 1205 } 1206 1207 if (aUnexpectedEventSeq) { 1208 for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) { 1209 aUnexpectedEventSeq[idx].unexpected = true; 1210 aUnexpectedEventSeq[idx].async = false; 1211 } 1212 1213 aEventSeq = aEventSeq.concat(aUnexpectedEventSeq); 1214 } 1215 1216 aInvoker.scenarios.push(aEventSeq); 1217 } 1218 1219 /** 1220 * Invokers defined below take a checker object (or array of checker objects). 1221 * An invoker listens for default event type registered in event queue object 1222 * until its checker is provided. 1223 * 1224 * Note, checker object or array of checker objects is optional. 1225 */ 1226 1227 /** 1228 * Click invoker. 1229 */ 1230 function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) { 1231 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); 1232 1233 this.invoke = function synthClick_invoke() { 1234 var targetNode = this.DOMNode; 1235 if (targetNode.nodeType == targetNode.DOCUMENT_NODE) { 1236 targetNode = this.DOMNode.body 1237 ? this.DOMNode.body 1238 : this.DOMNode.documentElement; 1239 } 1240 1241 // Scroll the node into view, otherwise synth click may fail. 1242 if (isHTMLElement(targetNode)) { 1243 targetNode.scrollIntoView(true); 1244 } else if (isXULElement(targetNode)) { 1245 var targetAcc = getAccessible(targetNode); 1246 targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); 1247 } 1248 1249 var x = 1, 1250 y = 1; 1251 if (aArgs && "where" in aArgs) { 1252 if (aArgs.where == "right") { 1253 if (isHTMLElement(targetNode)) { 1254 x = targetNode.offsetWidth - 1; 1255 } else if (isXULElement(targetNode)) { 1256 x = targetNode.getBoundingClientRect().width - 1; 1257 } 1258 } else if (aArgs.where == "center") { 1259 if (isHTMLElement(targetNode)) { 1260 x = targetNode.offsetWidth / 2; 1261 y = targetNode.offsetHeight / 2; 1262 } else if (isXULElement(targetNode)) { 1263 x = targetNode.getBoundingClientRect().width / 2; 1264 y = targetNode.getBoundingClientRect().height / 2; 1265 } 1266 } 1267 } 1268 synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {}); 1269 }; 1270 1271 this.finalCheck = function synthClick_finalCheck() { 1272 // Scroll top window back. 1273 window.top.scrollTo(0, 0); 1274 }; 1275 1276 this.getID = function synthClick_getID() { 1277 return prettyName(aNodeOrID) + " click"; 1278 }; 1279 } 1280 1281 /** 1282 * Scrolls the node into view. 1283 */ 1284 function scrollIntoView(aNodeOrID, aCheckerOrEventSeq) { 1285 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); 1286 1287 this.invoke = function scrollIntoView_invoke() { 1288 var targetNode = this.DOMNode; 1289 if (isHTMLElement(targetNode)) { 1290 targetNode.scrollIntoView(true); 1291 } else if (isXULElement(targetNode)) { 1292 var targetAcc = getAccessible(targetNode); 1293 targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); 1294 } 1295 }; 1296 1297 this.getID = function scrollIntoView_getID() { 1298 return prettyName(aNodeOrID) + " scrollIntoView"; 1299 }; 1300 } 1301 1302 /** 1303 * Mouse move invoker. 1304 */ 1305 function synthMouseMove(aID, aCheckerOrEventSeq) { 1306 this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); 1307 1308 this.invoke = function synthMouseMove_invoke() { 1309 synthesizeMouse(this.DOMNode, 5, 5, { type: "mousemove" }); 1310 synthesizeMouse(this.DOMNode, 6, 6, { type: "mousemove" }); 1311 }; 1312 1313 this.getID = function synthMouseMove_getID() { 1314 return prettyName(aID) + " mouse move"; 1315 }; 1316 } 1317 1318 /** 1319 * General key press invoker. 1320 */ 1321 function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) { 1322 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); 1323 1324 this.invoke = function synthKey_invoke() { 1325 synthesizeKey(this.mKey, this.mArgs, this.mWindow); 1326 }; 1327 1328 this.getID = function synthKey_getID() { 1329 var key = this.mKey; 1330 switch (this.mKey) { 1331 case "VK_TAB": 1332 key = "tab"; 1333 break; 1334 case "VK_DOWN": 1335 key = "down"; 1336 break; 1337 case "VK_UP": 1338 key = "up"; 1339 break; 1340 case "VK_LEFT": 1341 key = "left"; 1342 break; 1343 case "VK_RIGHT": 1344 key = "right"; 1345 break; 1346 case "VK_HOME": 1347 key = "home"; 1348 break; 1349 case "VK_END": 1350 key = "end"; 1351 break; 1352 case "VK_ESCAPE": 1353 key = "escape"; 1354 break; 1355 case "VK_RETURN": 1356 key = "enter"; 1357 break; 1358 } 1359 if (aArgs) { 1360 if (aArgs.shiftKey) { 1361 key += " shift"; 1362 } 1363 if (aArgs.ctrlKey) { 1364 key += " ctrl"; 1365 } 1366 if (aArgs.altKey) { 1367 key += " alt"; 1368 } 1369 } 1370 return prettyName(aNodeOrID) + " '" + key + " ' key"; 1371 }; 1372 1373 this.mKey = aKey; 1374 this.mArgs = aArgs ? aArgs : {}; 1375 this.mWindow = aArgs ? aArgs.window : null; 1376 } 1377 1378 /** 1379 * Tab key invoker. 1380 */ 1381 function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) { 1382 this.__proto__ = new synthKey( 1383 aNodeOrID, 1384 "VK_TAB", 1385 { shiftKey: false, window: aWindow }, 1386 aCheckerOrEventSeq 1387 ); 1388 } 1389 1390 /** 1391 * Shift tab key invoker. 1392 */ 1393 function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) { 1394 this.__proto__ = new synthKey( 1395 aNodeOrID, 1396 "VK_TAB", 1397 { shiftKey: true }, 1398 aCheckerOrEventSeq 1399 ); 1400 } 1401 1402 /** 1403 * Escape key invoker. 1404 */ 1405 function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) { 1406 this.__proto__ = new synthKey( 1407 aNodeOrID, 1408 "VK_ESCAPE", 1409 null, 1410 aCheckerOrEventSeq 1411 ); 1412 } 1413 1414 /** 1415 * Down arrow key invoker. 1416 */ 1417 function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { 1418 this.__proto__ = new synthKey( 1419 aNodeOrID, 1420 "VK_DOWN", 1421 aArgs, 1422 aCheckerOrEventSeq 1423 ); 1424 } 1425 1426 /** 1427 * Up arrow key invoker. 1428 */ 1429 function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { 1430 this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, aCheckerOrEventSeq); 1431 } 1432 1433 /** 1434 * Left arrow key invoker. 1435 */ 1436 function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { 1437 this.__proto__ = new synthKey( 1438 aNodeOrID, 1439 "VK_LEFT", 1440 aArgs, 1441 aCheckerOrEventSeq 1442 ); 1443 } 1444 1445 /** 1446 * Right arrow key invoker. 1447 */ 1448 function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { 1449 this.__proto__ = new synthKey( 1450 aNodeOrID, 1451 "VK_RIGHT", 1452 aArgs, 1453 aCheckerOrEventSeq 1454 ); 1455 } 1456 1457 /** 1458 * Home key invoker. 1459 */ 1460 function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) { 1461 this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq); 1462 } 1463 1464 /** 1465 * End key invoker. 1466 */ 1467 function synthEndKey(aNodeOrID, aCheckerOrEventSeq) { 1468 this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq); 1469 } 1470 1471 /** 1472 * Enter key invoker 1473 */ 1474 function synthEnterKey(aID, aCheckerOrEventSeq) { 1475 this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq); 1476 } 1477 1478 /** 1479 * Synth alt + down arrow to open combobox. 1480 */ 1481 function synthOpenComboboxKey(aID, aCheckerOrEventSeq) { 1482 this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true }); 1483 1484 this.getID = function synthOpenComboboxKey_getID() { 1485 return "open combobox (alt + down arrow) " + prettyName(aID); 1486 }; 1487 } 1488 1489 /** 1490 * Focus invoker. 1491 */ 1492 function synthFocus(aNodeOrID, aCheckerOrEventSeq) { 1493 var checkerOfEventSeq = aCheckerOrEventSeq 1494 ? aCheckerOrEventSeq 1495 : new focusChecker(aNodeOrID); 1496 this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq); 1497 1498 this.invoke = function synthFocus_invoke() { 1499 if (this.DOMNode.editor) { 1500 this.DOMNode.selectionStart = this.DOMNode.selectionEnd = 1501 this.DOMNode.value.length; 1502 } 1503 this.DOMNode.focus(); 1504 }; 1505 1506 this.getID = function synthFocus_getID() { 1507 return prettyName(aNodeOrID) + " focus"; 1508 }; 1509 } 1510 1511 /** 1512 * Focus invoker. Focus the HTML body of content document of iframe. 1513 */ 1514 function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) { 1515 var frameDoc = getNode(aNodeOrID).contentDocument; 1516 var checkerOrEventSeq = aCheckerOrEventSeq 1517 ? aCheckerOrEventSeq 1518 : new focusChecker(frameDoc); 1519 this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq); 1520 1521 this.invoke = function synthFocus_invoke() { 1522 this.DOMNode.body.focus(); 1523 }; 1524 1525 this.getID = function synthFocus_getID() { 1526 return prettyName(aNodeOrID) + " frame document focus"; 1527 }; 1528 } 1529 1530 /** 1531 * Change the current item when the widget doesn't have a focus. 1532 */ 1533 function changeCurrentItem(aID, aItemID) { 1534 this.eventSeq = [new nofocusChecker()]; 1535 1536 this.invoke = function changeCurrentItem_invoke() { 1537 var controlNode = getNode(aID); 1538 var itemNode = getNode(aItemID); 1539 1540 // HTML 1541 if (controlNode.localName == "input") { 1542 if (controlNode.checked) { 1543 this.reportError(); 1544 } 1545 1546 controlNode.checked = true; 1547 return; 1548 } 1549 1550 if (controlNode.localName == "select") { 1551 if (controlNode.selectedIndex == itemNode.index) { 1552 this.reportError(); 1553 } 1554 1555 controlNode.selectedIndex = itemNode.index; 1556 return; 1557 } 1558 1559 // XUL 1560 if (controlNode.localName == "tree") { 1561 if (controlNode.currentIndex == aItemID) { 1562 this.reportError(); 1563 } 1564 1565 controlNode.currentIndex = aItemID; 1566 return; 1567 } 1568 1569 if (controlNode.localName == "menulist") { 1570 if (controlNode.selectedItem == itemNode) { 1571 this.reportError(); 1572 } 1573 1574 controlNode.selectedItem = itemNode; 1575 return; 1576 } 1577 1578 if (controlNode.currentItem == itemNode) { 1579 ok( 1580 false, 1581 "Error in test: proposed current item is already current" + 1582 prettyName(aID) 1583 ); 1584 } 1585 1586 controlNode.currentItem = itemNode; 1587 }; 1588 1589 this.getID = function changeCurrentItem_getID() { 1590 return "current item change for " + prettyName(aID); 1591 }; 1592 1593 this.reportError = function changeCurrentItem_reportError() { 1594 ok( 1595 false, 1596 "Error in test: proposed current item '" + 1597 aItemID + 1598 "' is already current" 1599 ); 1600 }; 1601 } 1602 1603 /** 1604 * Toggle top menu invoker. 1605 */ 1606 function toggleTopMenu(aID, aCheckerOrEventSeq) { 1607 this.__proto__ = new synthKey(aID, "VK_ALT", null, aCheckerOrEventSeq); 1608 1609 this.getID = function toggleTopMenu_getID() { 1610 return "toggle top menu on " + prettyName(aID); 1611 }; 1612 } 1613 1614 /** 1615 * Context menu invoker. 1616 */ 1617 function synthContextMenu(aID, aCheckerOrEventSeq) { 1618 this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, { 1619 button: 0, 1620 type: "contextmenu", 1621 }); 1622 1623 this.getID = function synthContextMenu_getID() { 1624 return "context menu on " + prettyName(aID); 1625 }; 1626 } 1627 1628 /** 1629 * Open combobox, autocomplete and etc popup, check expandable states. 1630 */ 1631 function openCombobox(aComboboxID) { 1632 this.eventSeq = [ 1633 new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID), 1634 ]; 1635 1636 this.invoke = function openCombobox_invoke() { 1637 getNode(aComboboxID).focus(); 1638 synthesizeKey("VK_DOWN", { altKey: true }); 1639 }; 1640 1641 this.getID = function openCombobox_getID() { 1642 return "open combobox " + prettyName(aComboboxID); 1643 }; 1644 } 1645 1646 /** 1647 * Close combobox, autocomplete and etc popup, check expandable states. 1648 */ 1649 function closeCombobox(aComboboxID) { 1650 this.eventSeq = [ 1651 new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID), 1652 ]; 1653 1654 this.invoke = function closeCombobox_invoke() { 1655 synthesizeKey("KEY_Escape"); 1656 }; 1657 1658 this.getID = function closeCombobox_getID() { 1659 return "close combobox " + prettyName(aComboboxID); 1660 }; 1661 } 1662 1663 /** 1664 * Select all invoker. 1665 */ 1666 function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) { 1667 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); 1668 1669 this.invoke = function synthSelectAll_invoke() { 1670 if (ChromeUtils.getClassName(this.DOMNode) === "HTMLInputElement") { 1671 this.DOMNode.select(); 1672 } else { 1673 window.getSelection().selectAllChildren(this.DOMNode); 1674 } 1675 }; 1676 1677 this.getID = function synthSelectAll_getID() { 1678 return aNodeOrID + " selectall"; 1679 }; 1680 } 1681 1682 /** 1683 * Move the caret to the end of line. 1684 */ 1685 function moveToLineEnd(aID, aCaretOffset) { 1686 if (MAC) { 1687 this.__proto__ = new synthKey( 1688 aID, 1689 "VK_RIGHT", 1690 { metaKey: true }, 1691 new caretMoveChecker(aCaretOffset, true, aID) 1692 ); 1693 } else { 1694 this.__proto__ = new synthEndKey( 1695 aID, 1696 new caretMoveChecker(aCaretOffset, true, aID) 1697 ); 1698 } 1699 1700 this.getID = function moveToLineEnd_getID() { 1701 return "move to line end in " + prettyName(aID); 1702 }; 1703 } 1704 1705 /** 1706 * Move the caret to the end of previous line if any. 1707 */ 1708 function moveToPrevLineEnd(aID, aCaretOffset) { 1709 this.__proto__ = new synthAction( 1710 aID, 1711 new caretMoveChecker(aCaretOffset, true, aID) 1712 ); 1713 1714 this.invoke = function moveToPrevLineEnd_invoke() { 1715 synthesizeKey("KEY_ArrowUp"); 1716 1717 if (MAC) { 1718 synthesizeKey("Key_ArrowRight", { metaKey: true }); 1719 } else { 1720 synthesizeKey("KEY_End"); 1721 } 1722 }; 1723 1724 this.getID = function moveToPrevLineEnd_getID() { 1725 return "move to previous line end in " + prettyName(aID); 1726 }; 1727 } 1728 1729 /** 1730 * Move the caret to begining of the line. 1731 */ 1732 function moveToLineStart(aID, aCaretOffset) { 1733 if (MAC) { 1734 this.__proto__ = new synthKey( 1735 aID, 1736 "VK_LEFT", 1737 { metaKey: true }, 1738 new caretMoveChecker(aCaretOffset, true, aID) 1739 ); 1740 } else { 1741 this.__proto__ = new synthHomeKey( 1742 aID, 1743 new caretMoveChecker(aCaretOffset, true, aID) 1744 ); 1745 } 1746 1747 this.getID = function moveToLineEnd_getID() { 1748 return "move to line start in " + prettyName(aID); 1749 }; 1750 } 1751 1752 /** 1753 * Move the caret to begining of the text. 1754 */ 1755 function moveToTextStart(aID) { 1756 if (MAC) { 1757 this.__proto__ = new synthKey( 1758 aID, 1759 "VK_UP", 1760 { metaKey: true }, 1761 new caretMoveChecker(0, true, aID) 1762 ); 1763 } else { 1764 this.__proto__ = new synthKey( 1765 aID, 1766 "VK_HOME", 1767 { ctrlKey: true }, 1768 new caretMoveChecker(0, true, aID) 1769 ); 1770 } 1771 1772 this.getID = function moveToTextStart_getID() { 1773 return "move to text start in " + prettyName(aID); 1774 }; 1775 } 1776 1777 /** 1778 * Move the caret in text accessible. 1779 */ 1780 function moveCaretToDOMPoint( 1781 aID, 1782 aDOMPointNodeID, 1783 aDOMPointOffset, 1784 aExpectedOffset, 1785 aFocusTargetID, 1786 aCheckFunc 1787 ) { 1788 this.target = getAccessible(aID, [nsIAccessibleText]); 1789 this.DOMPointNode = getNode(aDOMPointNodeID); 1790 this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; 1791 this.focusNode = this.focus ? this.focus.DOMNode : null; 1792 1793 this.invoke = function moveCaretToDOMPoint_invoke() { 1794 if (this.focusNode) { 1795 this.focusNode.focus(); 1796 } 1797 1798 var selection = this.DOMPointNode.ownerGlobal.getSelection(); 1799 var selRange = selection.getRangeAt(0); 1800 selRange.setStart(this.DOMPointNode, aDOMPointOffset); 1801 selRange.collapse(true); 1802 1803 selection.removeRange(selRange); 1804 selection.addRange(selRange); 1805 }; 1806 1807 this.getID = function moveCaretToDOMPoint_getID() { 1808 return ( 1809 "Set caret on " + 1810 prettyName(aID) + 1811 " at point: " + 1812 prettyName(aDOMPointNodeID) + 1813 " node with offset " + 1814 aDOMPointOffset 1815 ); 1816 }; 1817 1818 this.finalCheck = function moveCaretToDOMPoint_finalCheck() { 1819 if (aCheckFunc) { 1820 aCheckFunc.call(); 1821 } 1822 }; 1823 1824 this.eventSeq = [new caretMoveChecker(aExpectedOffset, true, this.target)]; 1825 1826 if (this.focus) { 1827 this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); 1828 } 1829 } 1830 1831 /** 1832 * Set caret offset in text accessible. 1833 */ 1834 function setCaretOffset(aID, aOffset, aFocusTargetID) { 1835 this.target = getAccessible(aID, [nsIAccessibleText]); 1836 this.offset = aOffset == -1 ? this.target.characterCount : aOffset; 1837 this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; 1838 1839 this.invoke = function setCaretOffset_invoke() { 1840 this.target.caretOffset = this.offset; 1841 }; 1842 1843 this.getID = function setCaretOffset_getID() { 1844 return "Set caretOffset on " + prettyName(aID) + " at " + this.offset; 1845 }; 1846 1847 this.eventSeq = [new caretMoveChecker(this.offset, true, this.target)]; 1848 1849 if (this.focus) { 1850 this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); 1851 } 1852 } 1853 1854 // ////////////////////////////////////////////////////////////////////////////// 1855 // Event queue checkers 1856 1857 /** 1858 * Common invoker checker (see eventSeq of eventQueue). 1859 */ 1860 function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) { 1861 this.type = aEventType; 1862 this.async = aIsAsync; 1863 1864 this.__defineGetter__("target", invokerChecker_targetGetter); 1865 this.__defineSetter__("target", invokerChecker_targetSetter); 1866 1867 // implementation details 1868 function invokerChecker_targetGetter() { 1869 if (typeof this.mTarget == "function") { 1870 return this.mTarget.call(null, this.mTargetFuncArg); 1871 } 1872 if (typeof this.mTarget == "string") { 1873 return getNode(this.mTarget); 1874 } 1875 1876 return this.mTarget; 1877 } 1878 1879 function invokerChecker_targetSetter(aValue) { 1880 this.mTarget = aValue; 1881 return this.mTarget; 1882 } 1883 1884 this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter); 1885 1886 function invokerChecker_targetDescrGetter() { 1887 if (typeof this.mTarget == "function") { 1888 return this.mTarget.name + ", arg: " + this.mTargetFuncArg; 1889 } 1890 1891 return prettyName(this.mTarget); 1892 } 1893 1894 this.mTarget = aTargetOrFunc; 1895 this.mTargetFuncArg = aTargetFuncArg; 1896 } 1897 1898 /** 1899 * event checker that forces preceeding async events to happen before this 1900 * checker. 1901 */ 1902 function orderChecker() { 1903 // XXX it doesn't actually work to inherit from invokerChecker, but maybe we 1904 // should fix that? 1905 // this.__proto__ = new invokerChecker(null, null, null, false); 1906 } 1907 1908 /** 1909 * Generic invoker checker for todo events. 1910 */ 1911 function todo_invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { 1912 this.__proto__ = new invokerChecker( 1913 aEventType, 1914 aTargetOrFunc, 1915 aTargetFuncArg, 1916 true 1917 ); 1918 this.todo = true; 1919 } 1920 1921 /** 1922 * Generic invoker checker for unexpected events. 1923 */ 1924 function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { 1925 this.__proto__ = new invokerChecker( 1926 aEventType, 1927 aTargetOrFunc, 1928 aTargetFuncArg, 1929 true 1930 ); 1931 1932 this.unexpected = true; 1933 } 1934 1935 /** 1936 * Common invoker checker for async events. 1937 */ 1938 function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { 1939 this.__proto__ = new invokerChecker( 1940 aEventType, 1941 aTargetOrFunc, 1942 aTargetFuncArg, 1943 true 1944 ); 1945 } 1946 1947 function focusChecker(aTargetOrFunc, aTargetFuncArg) { 1948 this.__proto__ = new invokerChecker( 1949 EVENT_FOCUS, 1950 aTargetOrFunc, 1951 aTargetFuncArg, 1952 false 1953 ); 1954 1955 this.unique = true; // focus event must be unique for invoker action 1956 1957 this.check = function focusChecker_check(aEvent) { 1958 testStates(aEvent.accessible, STATE_FOCUSED); 1959 }; 1960 } 1961 1962 function nofocusChecker(aID) { 1963 this.__proto__ = new focusChecker(aID); 1964 this.unexpected = true; 1965 } 1966 1967 /** 1968 * Text inserted/removed events checker. 1969 * 1970 * @param aFromUser [in, optional] kNotFromUserInput or kFromUserInput 1971 */ 1972 function textChangeChecker( 1973 aID, 1974 aStart, 1975 aEnd, 1976 aTextOrFunc, 1977 aIsInserted, 1978 aFromUser, 1979 aAsync 1980 ) { 1981 this.target = getNode(aID); 1982 this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; 1983 this.startOffset = aStart; 1984 this.endOffset = aEnd; 1985 this.textOrFunc = aTextOrFunc; 1986 this.async = aAsync; 1987 1988 this.match = function stextChangeChecker_match(aEvent) { 1989 if ( 1990 !(aEvent instanceof nsIAccessibleTextChangeEvent) || 1991 aEvent.accessible !== getAccessible(this.target) 1992 ) { 1993 return false; 1994 } 1995 1996 let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent); 1997 let modifiedText = 1998 typeof this.textOrFunc === "function" 1999 ? this.textOrFunc() 2000 : this.textOrFunc; 2001 return modifiedText === tcEvent.modifiedText; 2002 }; 2003 2004 this.check = function textChangeChecker_check(aEvent) { 2005 aEvent.QueryInterface(nsIAccessibleTextChangeEvent); 2006 2007 var modifiedText = 2008 typeof this.textOrFunc == "function" 2009 ? this.textOrFunc() 2010 : this.textOrFunc; 2011 var modifiedTextLen = 2012 this.endOffset == -1 ? modifiedText.length : aEnd - aStart; 2013 2014 is( 2015 aEvent.start, 2016 this.startOffset, 2017 "Wrong start offset for " + prettyName(aID) 2018 ); 2019 is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID)); 2020 var changeInfo = aIsInserted ? "inserted" : "removed"; 2021 is( 2022 aEvent.isInserted, 2023 aIsInserted, 2024 "Text was " + changeInfo + " for " + prettyName(aID) 2025 ); 2026 is( 2027 aEvent.modifiedText, 2028 modifiedText, 2029 "Wrong " + changeInfo + " text for " + prettyName(aID) 2030 ); 2031 if (typeof aFromUser != "undefined") { 2032 is( 2033 aEvent.isFromUserInput, 2034 aFromUser, 2035 "wrong value of isFromUserInput() for " + prettyName(aID) 2036 ); 2037 } 2038 }; 2039 } 2040 2041 /** 2042 * Caret move events checker. 2043 */ 2044 function caretMoveChecker( 2045 aCaretOffset, 2046 aIsSelectionCollapsed, 2047 aTargetOrFunc, 2048 aTargetFuncArg, 2049 aIsAsync 2050 ) { 2051 this.__proto__ = new invokerChecker( 2052 EVENT_TEXT_CARET_MOVED, 2053 aTargetOrFunc, 2054 aTargetFuncArg, 2055 aIsAsync 2056 ); 2057 2058 this.check = function caretMoveChecker_check(aEvent) { 2059 let evt = aEvent.QueryInterface(nsIAccessibleCaretMoveEvent); 2060 is( 2061 evt.caretOffset, 2062 aCaretOffset, 2063 "Wrong caret offset for " + prettyName(aEvent.accessible) 2064 ); 2065 is( 2066 evt.isSelectionCollapsed, 2067 aIsSelectionCollapsed, 2068 "wrong collapsed value for " + prettyName(aEvent.accessible) 2069 ); 2070 }; 2071 } 2072 2073 function asyncCaretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) { 2074 this.__proto__ = new caretMoveChecker( 2075 aCaretOffset, 2076 true, // Caret is collapsed 2077 aTargetOrFunc, 2078 aTargetFuncArg, 2079 true 2080 ); 2081 } 2082 2083 /** 2084 * Text selection change checker. 2085 */ 2086 function textSelectionChecker( 2087 aID, 2088 aStartOffset, 2089 aEndOffset, 2090 aRangeStartContainer, 2091 aRangeStartOffset, 2092 aRangeEndContainer, 2093 aRangeEndOffset 2094 ) { 2095 this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID); 2096 2097 this.check = function textSelectionChecker_check(aEvent) { 2098 if (aStartOffset == aEndOffset) { 2099 ok(true, "Collapsed selection triggered text selection change event."); 2100 } else { 2101 testTextGetSelection(aID, aStartOffset, aEndOffset, 0); 2102 2103 // Test selection test range 2104 let selectionRanges = aEvent.QueryInterface( 2105 nsIAccessibleTextSelectionChangeEvent 2106 ).selectionRanges; 2107 let range = selectionRanges.queryElementAt(0, nsIAccessibleTextRange); 2108 is( 2109 range.startContainer, 2110 getAccessible(aRangeStartContainer), 2111 "correct range start container" 2112 ); 2113 is(range.startOffset, aRangeStartOffset, "correct range start offset"); 2114 is(range.endOffset, aRangeEndOffset, "correct range end offset"); 2115 is( 2116 range.endContainer, 2117 getAccessible(aRangeEndContainer), 2118 "correct range end container" 2119 ); 2120 } 2121 }; 2122 } 2123 2124 /** 2125 * Object attribute changed checker 2126 */ 2127 function objAttrChangedChecker(aID, aAttr) { 2128 this.__proto__ = new invokerChecker(EVENT_OBJECT_ATTRIBUTE_CHANGED, aID); 2129 2130 this.check = function objAttrChangedChecker_check(aEvent) { 2131 var event = null; 2132 try { 2133 var event = aEvent.QueryInterface( 2134 nsIAccessibleObjectAttributeChangedEvent 2135 ); 2136 } catch (e) { 2137 ok(false, "Object attribute changed event was expected"); 2138 } 2139 2140 if (!event) { 2141 return; 2142 } 2143 2144 is( 2145 event.changedAttribute, 2146 aAttr, 2147 "Wrong attribute name of the object attribute changed event." 2148 ); 2149 }; 2150 2151 this.match = function objAttrChangedChecker_match(aEvent) { 2152 if (aEvent instanceof nsIAccessibleObjectAttributeChangedEvent) { 2153 var scEvent = aEvent.QueryInterface( 2154 nsIAccessibleObjectAttributeChangedEvent 2155 ); 2156 return ( 2157 aEvent.accessible == getAccessible(this.target) && 2158 scEvent.changedAttribute == aAttr 2159 ); 2160 } 2161 return false; 2162 }; 2163 } 2164 2165 /** 2166 * State change checker. 2167 */ 2168 function stateChangeChecker( 2169 aState, 2170 aIsExtraState, 2171 aIsEnabled, 2172 aTargetOrFunc, 2173 aTargetFuncArg, 2174 aIsAsync, 2175 aSkipCurrentStateCheck 2176 ) { 2177 this.__proto__ = new invokerChecker( 2178 EVENT_STATE_CHANGE, 2179 aTargetOrFunc, 2180 aTargetFuncArg, 2181 aIsAsync 2182 ); 2183 2184 this.check = function stateChangeChecker_check(aEvent) { 2185 var event = null; 2186 try { 2187 var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); 2188 } catch (e) { 2189 ok(false, "State change event was expected"); 2190 } 2191 2192 if (!event) { 2193 return; 2194 } 2195 2196 is( 2197 event.isExtraState, 2198 aIsExtraState, 2199 "Wrong extra state bit of the statechange event." 2200 ); 2201 isState( 2202 event.state, 2203 aState, 2204 aIsExtraState, 2205 "Wrong state of the statechange event." 2206 ); 2207 is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); 2208 2209 if (aSkipCurrentStateCheck) { 2210 todo(false, "State checking was skipped!"); 2211 return; 2212 } 2213 2214 var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0; 2215 var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0; 2216 var unxpdState = aIsEnabled ? 0 : aIsExtraState ? 0 : aState; 2217 var unxpdExtraState = aIsEnabled ? 0 : aIsExtraState ? aState : 0; 2218 testStates( 2219 event.accessible, 2220 state, 2221 extraState, 2222 unxpdState, 2223 unxpdExtraState 2224 ); 2225 }; 2226 2227 this.match = function stateChangeChecker_match(aEvent) { 2228 if (aEvent instanceof nsIAccessibleStateChangeEvent) { 2229 var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); 2230 return ( 2231 aEvent.accessible == getAccessible(this.target) && 2232 scEvent.state == aState 2233 ); 2234 } 2235 return false; 2236 }; 2237 } 2238 2239 function asyncStateChangeChecker( 2240 aState, 2241 aIsExtraState, 2242 aIsEnabled, 2243 aTargetOrFunc, 2244 aTargetFuncArg 2245 ) { 2246 this.__proto__ = new stateChangeChecker( 2247 aState, 2248 aIsExtraState, 2249 aIsEnabled, 2250 aTargetOrFunc, 2251 aTargetFuncArg, 2252 true 2253 ); 2254 } 2255 2256 /** 2257 * Expanded state change checker. 2258 */ 2259 function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) { 2260 this.__proto__ = new invokerChecker( 2261 EVENT_STATE_CHANGE, 2262 aTargetOrFunc, 2263 aTargetFuncArg 2264 ); 2265 2266 this.check = function expandedStateChecker_check(aEvent) { 2267 var event = null; 2268 try { 2269 var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); 2270 } catch (e) { 2271 ok(false, "State change event was expected"); 2272 } 2273 2274 if (!event) { 2275 return; 2276 } 2277 2278 is(event.state, STATE_EXPANDED, "Wrong state of the statechange event."); 2279 is( 2280 event.isExtraState, 2281 false, 2282 "Wrong extra state bit of the statechange event." 2283 ); 2284 is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); 2285 2286 testStates(event.accessible, aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED); 2287 }; 2288 } 2289 2290 // ////////////////////////////////////////////////////////////////////////////// 2291 // Event sequances (array of predefined checkers) 2292 2293 /** 2294 * Event seq for single selection change. 2295 */ 2296 function selChangeSeq(aUnselectedID, aSelectedID) { 2297 if (!aUnselectedID) { 2298 return [ 2299 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), 2300 new invokerChecker(EVENT_SELECTION, aSelectedID), 2301 ]; 2302 } 2303 2304 // Return two possible scenarios: depending on widget type when selection is 2305 // moved the the order of items that get selected and unselected may vary. 2306 return [ 2307 [ 2308 new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), 2309 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), 2310 new invokerChecker(EVENT_SELECTION, aSelectedID), 2311 ], 2312 [ 2313 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), 2314 new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), 2315 new invokerChecker(EVENT_SELECTION, aSelectedID), 2316 ], 2317 ]; 2318 } 2319 2320 /** 2321 * Event seq for item removed form the selection. 2322 */ 2323 function selRemoveSeq(aUnselectedID) { 2324 return [ 2325 new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), 2326 new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID), 2327 ]; 2328 } 2329 2330 /** 2331 * Event seq for item added to the selection. 2332 */ 2333 function selAddSeq(aSelectedID) { 2334 return [ 2335 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), 2336 new invokerChecker(EVENT_SELECTION_ADD, aSelectedID), 2337 ]; 2338 } 2339 2340 // ////////////////////////////////////////////////////////////////////////////// 2341 // Private implementation details. 2342 // ////////////////////////////////////////////////////////////////////////////// 2343 2344 // ////////////////////////////////////////////////////////////////////////////// 2345 // General 2346 2347 var gA11yEventListeners = {}; 2348 var gA11yEventApplicantsCount = 0; 2349 2350 var gA11yEventObserver = { 2351 // eslint-disable-next-line complexity 2352 observe: function observe(aSubject, aTopic) { 2353 if (aTopic != "accessible-event") { 2354 return; 2355 } 2356 2357 var event; 2358 try { 2359 event = aSubject.QueryInterface(nsIAccessibleEvent); 2360 } catch (ex) { 2361 // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered. 2362 // Remove the leftover observer, otherwise it "leaks" to all the following tests. 2363 Services.obs.removeObserver(this, "accessible-event"); 2364 // Forward the exception, with added explanation. 2365 throw new Error( 2366 "[accessible/events.js, gA11yEventObserver.observe] This is expected " + 2367 `if a previous test has been aborted... Initial exception was: [ ${ex} ]` 2368 ); 2369 } 2370 var listenersArray = gA11yEventListeners[event.eventType]; 2371 2372 var eventFromDumpArea = false; 2373 if (gLogger.isEnabled()) { 2374 // debug stuff 2375 eventFromDumpArea = true; 2376 2377 var target = event.DOMNode; 2378 var dumpElm = gA11yEventDumpID 2379 ? document.getElementById(gA11yEventDumpID) 2380 : null; 2381 2382 if (dumpElm) { 2383 var parent = target; 2384 while (parent && parent != dumpElm) { 2385 parent = parent.parentNode; 2386 } 2387 } 2388 2389 if (!dumpElm || parent != dumpElm) { 2390 var type = eventTypeToString(event.eventType); 2391 var info = "Event type: " + type; 2392 2393 if (event instanceof nsIAccessibleStateChangeEvent) { 2394 var stateStr = statesToString( 2395 event.isExtraState ? 0 : event.state, 2396 event.isExtraState ? event.state : 0 2397 ); 2398 info += ", state: " + stateStr + ", is enabled: " + event.isEnabled; 2399 } else if (event instanceof nsIAccessibleTextChangeEvent) { 2400 info += 2401 ", start: " + 2402 event.start + 2403 ", length: " + 2404 event.length + 2405 ", " + 2406 (event.isInserted ? "inserted" : "removed") + 2407 " text: " + 2408 event.modifiedText; 2409 } 2410 2411 info += ". Target: " + prettyName(event.accessible); 2412 2413 if (listenersArray) { 2414 info += ". Listeners count: " + listenersArray.length; 2415 } 2416 2417 if (gLogger.hasFeature("parentchain:" + type)) { 2418 info += "\nParent chain:\n"; 2419 var acc = event.accessible; 2420 while (acc) { 2421 info += " " + prettyName(acc) + "\n"; 2422 acc = acc.parent; 2423 } 2424 } 2425 2426 eventFromDumpArea = false; 2427 gLogger.log(info); 2428 } 2429 } 2430 2431 // Do not notify listeners if event is result of event log changes. 2432 if (!listenersArray || eventFromDumpArea) { 2433 return; 2434 } 2435 2436 for (var index = 0; index < listenersArray.length; index++) { 2437 listenersArray[index].handleEvent(event); 2438 } 2439 }, 2440 }; 2441 2442 function listenA11yEvents(aStartToListen) { 2443 if (aStartToListen) { 2444 // Add observer when adding the first applicant only. 2445 if (!gA11yEventApplicantsCount++) { 2446 Services.obs.addObserver(gA11yEventObserver, "accessible-event"); 2447 } 2448 } else { 2449 // Remove observer when there are no more applicants only. 2450 // '< 0' case should not happen, but just in case: removeObserver() will throw. 2451 // eslint-disable-next-line no-lonely-if 2452 if (--gA11yEventApplicantsCount <= 0) { 2453 Services.obs.removeObserver(gA11yEventObserver, "accessible-event"); 2454 } 2455 } 2456 } 2457 2458 function addA11yEventListener(aEventType, aEventHandler) { 2459 if (!(aEventType in gA11yEventListeners)) { 2460 gA11yEventListeners[aEventType] = []; 2461 } 2462 2463 var listenersArray = gA11yEventListeners[aEventType]; 2464 var index = listenersArray.indexOf(aEventHandler); 2465 if (index == -1) { 2466 listenersArray.push(aEventHandler); 2467 } 2468 } 2469 2470 function removeA11yEventListener(aEventType, aEventHandler) { 2471 var listenersArray = gA11yEventListeners[aEventType]; 2472 if (!listenersArray) { 2473 return false; 2474 } 2475 2476 var index = listenersArray.indexOf(aEventHandler); 2477 if (index == -1) { 2478 return false; 2479 } 2480 2481 listenersArray.splice(index, 1); 2482 2483 if (!listenersArray.length) { 2484 gA11yEventListeners[aEventType] = null; 2485 delete gA11yEventListeners[aEventType]; 2486 } 2487 2488 return true; 2489 } 2490 2491 /** 2492 * Used to dump debug information. 2493 */ 2494 var gLogger = { 2495 /** 2496 * Return true if dump is enabled. 2497 */ 2498 isEnabled: function debugOutput_isEnabled() { 2499 return ( 2500 gA11yEventDumpID || gA11yEventDumpToConsole || gA11yEventDumpToAppConsole 2501 ); 2502 }, 2503 2504 /** 2505 * Dump information into DOM and console if applicable. 2506 */ 2507 log: function logger_log(aMsg) { 2508 this.logToConsole(aMsg); 2509 this.logToAppConsole(aMsg); 2510 this.logToDOM(aMsg); 2511 }, 2512 2513 /** 2514 * Log message to DOM. 2515 * 2516 * @param aMsg [in] the primary message 2517 * @param aHasIndent [in, optional] if specified the message has an indent 2518 * @param aPreEmphText [in, optional] the text is colored and appended prior 2519 * primary message 2520 */ 2521 logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) { 2522 if (gA11yEventDumpID == "") { 2523 return; 2524 } 2525 2526 var dumpElm = document.getElementById(gA11yEventDumpID); 2527 if (!dumpElm) { 2528 ok( 2529 false, 2530 "No dump element '" + gA11yEventDumpID + "' within the document!" 2531 ); 2532 return; 2533 } 2534 2535 var containerTagName = 2536 ChromeUtils.getClassName(document) == "HTMLDocument" 2537 ? "div" 2538 : "description"; 2539 2540 var container = document.createElement(containerTagName); 2541 if (aHasIndent) { 2542 container.setAttribute("style", "padding-left: 10px;"); 2543 } 2544 2545 if (aPreEmphText) { 2546 var inlineTagName = 2547 ChromeUtils.getClassName(document) == "HTMLDocument" 2548 ? "span" 2549 : "description"; 2550 var emphElm = document.createElement(inlineTagName); 2551 emphElm.setAttribute("style", "color: blue;"); 2552 emphElm.textContent = aPreEmphText; 2553 2554 container.appendChild(emphElm); 2555 } 2556 2557 var textNode = document.createTextNode(aMsg); 2558 container.appendChild(textNode); 2559 2560 dumpElm.appendChild(container); 2561 }, 2562 2563 /** 2564 * Log message to console. 2565 */ 2566 logToConsole: function logger_logToConsole(aMsg) { 2567 if (gA11yEventDumpToConsole) { 2568 dump("\n" + aMsg + "\n"); 2569 } 2570 }, 2571 2572 /** 2573 * Log message to error console. 2574 */ 2575 logToAppConsole: function logger_logToAppConsole(aMsg) { 2576 if (gA11yEventDumpToAppConsole) { 2577 Services.console.logStringMessage("events: " + aMsg); 2578 } 2579 }, 2580 2581 /** 2582 * Return true if logging feature is enabled. 2583 */ 2584 hasFeature: function logger_hasFeature(aFeature) { 2585 var startIdx = gA11yEventDumpFeature.indexOf(aFeature); 2586 if (startIdx == -1) { 2587 return false; 2588 } 2589 2590 var endIdx = startIdx + aFeature.length; 2591 return ( 2592 endIdx == gA11yEventDumpFeature.length || 2593 gA11yEventDumpFeature[endIdx] == ";" 2594 ); 2595 }, 2596 }; 2597 2598 // ////////////////////////////////////////////////////////////////////////////// 2599 // Sequence 2600 2601 /** 2602 * Base class of sequence item. 2603 */ 2604 function sequenceItem(aProcessor, aEventType, aTarget, aItemID) { 2605 // private 2606 2607 this.startProcess = function sequenceItem_startProcess() { 2608 this.queue.invoke(); 2609 }; 2610 2611 this.queue = new eventQueue(); 2612 this.queue.onFinish = function () { 2613 aProcessor.onProcessed(); 2614 return DO_NOT_FINISH_TEST; 2615 }; 2616 2617 var invoker = { 2618 invoke: function invoker_invoke() { 2619 return aProcessor.process(); 2620 }, 2621 getID: function invoker_getID() { 2622 return aItemID; 2623 }, 2624 eventSeq: [new invokerChecker(aEventType, aTarget)], 2625 }; 2626 2627 this.queue.push(invoker); 2628 } 2629 2630 // ////////////////////////////////////////////////////////////////////////////// 2631 // Event queue invokers 2632 2633 /** 2634 * Invoker base class for prepare an action. 2635 */ 2636 function synthAction(aNodeOrID, aEventsObj) { 2637 this.DOMNode = getNode(aNodeOrID); 2638 2639 if (aEventsObj) { 2640 var scenarios = null; 2641 if (aEventsObj instanceof Array) { 2642 if (aEventsObj[0] instanceof Array) { 2643 scenarios = aEventsObj; 2644 } 2645 // scenarios 2646 else { 2647 scenarios = [aEventsObj]; 2648 } // event sequance 2649 } else { 2650 scenarios = [[aEventsObj]]; // a single checker object 2651 } 2652 2653 for (var i = 0; i < scenarios.length; i++) { 2654 defineScenario(this, scenarios[i]); 2655 } 2656 } 2657 2658 this.getID = function synthAction_getID() { 2659 return prettyName(aNodeOrID) + " action"; 2660 }; 2661 }