Bug 1867925 - Mark some storage-access-api tests as intermittent after wpt-sync....
[gecko.git] / accessible / tests / mochitest / events.js
bloba6c216e01d3b0910db5540e85d17c8882e577f0a
1 /* import-globals-from common.js */
2 /* import-globals-from states.js */
3 /* import-globals-from text.js */
5 // XXX Bug 1425371 - enable no-redeclare and fix the issues with the tests.
6 /* eslint-disable no-redeclare */
8 // //////////////////////////////////////////////////////////////////////////////
9 // Constants
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_HIDE = nsIAccessibleEvent.EVENT_HIDE;
20 const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS;
21 const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE;
22 const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START;
23 const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END;
24 const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START;
25 const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END;
26 const EVENT_OBJECT_ATTRIBUTE_CHANGED =
27   nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED;
28 const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER;
29 const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START;
30 const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION;
31 const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD;
32 const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE;
33 const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN;
34 const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW;
35 const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE;
36 const EVENT_TEXT_ATTRIBUTE_CHANGED =
37   nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED;
38 const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED;
39 const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED;
40 const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED;
41 const EVENT_TEXT_SELECTION_CHANGED =
42   nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED;
43 const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE;
44 const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE;
45 const EVENT_VIRTUALCURSOR_CHANGED =
46   nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED;
48 const kNotFromUserInput = 0;
49 const kFromUserInput = 1;
51 // //////////////////////////////////////////////////////////////////////////////
52 // General
54 /**
55  * Set up this variable to dump events into DOM.
56  */
57 var gA11yEventDumpID = "";
59 /**
60  * Set up this variable to dump event processing into console.
61  */
62 var gA11yEventDumpToConsole = false;
64 /**
65  * Set up this variable to dump event processing into error console.
66  */
67 var gA11yEventDumpToAppConsole = false;
69 /**
70  * Semicolon separated set of logging features.
71  */
72 var gA11yEventDumpFeature = "";
74 /**
75  * Function to detect HTML elements when given a node.
76  */
77 function isHTMLElement(aNode) {
78   return (
79     aNode.nodeType == aNode.ELEMENT_NODE &&
80     aNode.namespaceURI == "http://www.w3.org/1999/xhtml"
81   );
84 function isXULElement(aNode) {
85   return (
86     aNode.nodeType == aNode.ELEMENT_NODE &&
87     aNode.namespaceURI ==
88       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
89   );
92 /**
93  * Executes the function when requested event is handled.
94  *
95  * @param aEventType  [in] event type
96  * @param aTarget     [in] event target
97  * @param aFunc       [in] function to call when event is handled
98  * @param aContext    [in, optional] object in which context the function is
99  *                    called
100  * @param aArg1       [in, optional] argument passed into the function
101  * @param aArg2       [in, optional] argument passed into the function
102  */
103 function waitForEvent(
104   aEventType,
105   aTargetOrFunc,
106   aFunc,
107   aContext,
108   aArg1,
109   aArg2
110 ) {
111   var handler = {
112     handleEvent: function handleEvent(aEvent) {
113       var target = aTargetOrFunc;
114       if (typeof aTargetOrFunc == "function") {
115         target = aTargetOrFunc.call();
116       }
118       if (target) {
119         if (target instanceof nsIAccessible && target != aEvent.accessible) {
120           return;
121         }
123         if (Node.isInstance(target) && target != aEvent.DOMNode) {
124           return;
125         }
126       }
128       unregisterA11yEventListener(aEventType, this);
130       window.setTimeout(function () {
131         aFunc.call(aContext, aArg1, aArg2);
132       }, 0);
133     },
134   };
136   registerA11yEventListener(aEventType, handler);
140  * Generate mouse move over image map what creates image map accessible (async).
141  * See waitForImageMap() function.
142  */
143 function waveOverImageMap(aImageMapID) {
144   var imageMapNode = getNode(aImageMapID);
145   synthesizeMouse(
146     imageMapNode,
147     10,
148     10,
149     { type: "mousemove" },
150     imageMapNode.ownerGlobal
151   );
155  * Call the given function when the tree of the given image map is built.
156  */
157 function waitForImageMap(aImageMapID, aTestFunc) {
158   waveOverImageMap(aImageMapID);
160   var imageMapAcc = getAccessible(aImageMapID);
161   if (imageMapAcc.firstChild) {
162     aTestFunc();
163     return;
164   }
166   waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc);
170  * Register accessibility event listener.
172  * @param aEventType     the accessible event type (see nsIAccessibleEvent for
173  *                       available constants).
174  * @param aEventHandler  event listener object, when accessible event of the
175  *                       given type is handled then 'handleEvent' method of
176  *                       this object is invoked with nsIAccessibleEvent object
177  *                       as the first argument.
178  */
179 function registerA11yEventListener(aEventType, aEventHandler) {
180   listenA11yEvents(true);
181   addA11yEventListener(aEventType, aEventHandler);
185  * Unregister accessibility event listener. Must be called for every registered
186  * event listener (see registerA11yEventListener() function) when the listener
187  * is not needed.
188  */
189 function unregisterA11yEventListener(aEventType, aEventHandler) {
190   removeA11yEventListener(aEventType, aEventHandler);
191   listenA11yEvents(false);
194 // //////////////////////////////////////////////////////////////////////////////
195 // Event queue
198  * Return value of invoke method of invoker object. Indicates invoker was unable
199  * to prepare action.
200  */
201 const INVOKER_ACTION_FAILED = 1;
204  * Return value of eventQueue.onFinish. Indicates eventQueue should not finish
205  * tests.
206  */
207 const DO_NOT_FINISH_TEST = 1;
210  * Creates event queue for the given event type. The queue consists of invoker
211  * objects, each of them generates the event of the event type. When queue is
212  * started then every invoker object is asked to generate event after timeout.
213  * When event is caught then current invoker object is asked to check whether
214  * event was handled correctly.
216  * Invoker interface is:
218  *   var invoker = {
219  *     // Generates accessible event or event sequence. If returns
220  *     // INVOKER_ACTION_FAILED constant then stop tests.
221  *     invoke: function(){},
223  *     // [optional] Invoker's check of handled event for correctness.
224  *     check: function(aEvent){},
226  *     // [optional] Invoker's check before the next invoker is proceeded.
227  *     finalCheck: function(aEvent){},
229  *     // [optional] Is called when event of any registered type is handled.
230  *     debugCheck: function(aEvent){},
232  *     // [ignored if 'eventSeq' is defined] DOM node event is generated for
233  *     // (used in the case when invoker expects single event).
234  *     DOMNode getter: function() {},
236  *     // [optional] if true then event sequences are ignored (no failure if
237  *     // sequences are empty). Use you need to invoke an action, do some check
238  *     // after timeout and proceed a next invoker.
239  *     noEventsOnAction getter: function() {},
241  *     // Array of checker objects defining expected events on invoker's action.
242  *     //
243  *     // Checker object interface:
244  *     //
245  *     // var checker = {
246  *     //   * DOM or a11y event type. *
247  *     //   type getter: function() {},
248  *     //
249  *     //   * DOM node or accessible. *
250  *     //   target getter: function() {},
251  *     //
252  *     //   * DOM event phase (false - bubbling). *
253  *     //   phase getter: function() {},
254  *     //
255  *     //   * Callback, called to match handled event. *
256  *     //   match : function(aEvent) {},
257  *     //
258  *     //   * Callback, called when event is handled
259  *     //   check: function(aEvent) {},
260  *     //
261  *     //   * Checker ID *
262  *     //   getID: function() {},
263  *     //
264  *     //   * Event that don't have predefined order relative other events. *
265  *     //   async getter: function() {},
266  *     //
267  *     //   * Event that is not expected. *
268  *     //   unexpected getter: function() {},
269  *     //
270  *     //   * No other event of the same type is not allowed. *
271  *     //   unique getter: function() {}
272  *     // };
273  *     eventSeq getter() {},
275  *     // Array of checker objects defining unexpected events on invoker's
276  *     // action.
277  *     unexpectedEventSeq getter() {},
279  *     // The ID of invoker.
280  *     getID: function(){} // returns invoker ID
281  *   };
283  *   // Used to add a possible scenario of expected/unexpected events on
284  *   // invoker's action.
285  *  defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq)
288  * @param  aEventType  [in, optional] the default event type (isn't used if
289  *                      invoker defines eventSeq property).
290  */
291 function eventQueue(aEventType) {
292   // public
294   /**
295    * Add invoker object into queue.
296    */
297   this.push = function eventQueue_push(aEventInvoker) {
298     this.mInvokers.push(aEventInvoker);
299   };
301   /**
302    * Start the queue processing.
303    */
304   this.invoke = function eventQueue_invoke() {
305     listenA11yEvents(true);
307     // XXX: Intermittent test_events_caretmove.html fails withouth timeout,
308     // see bug 474952.
309     this.processNextInvokerInTimeout(true);
310   };
312   /**
313    * This function is called when all events in the queue were handled.
314    * Override it if you need to be notified of this.
315    */
316   this.onFinish = function eventQueue_finish() {};
318   // private
320   /**
321    * Process next invoker.
322    */
323   // eslint-disable-next-line complexity
324   this.processNextInvoker = function eventQueue_processNextInvoker() {
325     // Some scenario was matched, we wait on next invoker processing.
326     if (this.mNextInvokerStatus == kInvokerCanceled) {
327       this.setInvokerStatus(
328         kInvokerNotScheduled,
329         "scenario was matched, wait for next invoker activation"
330       );
331       return;
332     }
334     this.setInvokerStatus(
335       kInvokerNotScheduled,
336       "the next invoker is processed now"
337     );
339     // Finish processing of the current invoker if any.
340     var testFailed = false;
342     var invoker = this.getInvoker();
343     if (invoker) {
344       if ("finalCheck" in invoker) {
345         invoker.finalCheck();
346       }
348       if (this.mScenarios && this.mScenarios.length) {
349         var matchIdx = -1;
350         for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
351           var eventSeq = this.mScenarios[scnIdx];
352           if (!this.areExpectedEventsLeft(eventSeq)) {
353             for (var idx = 0; idx < eventSeq.length; idx++) {
354               var checker = eventSeq[idx];
355               if (
356                 (checker.unexpected && checker.wasCaught) ||
357                 (!checker.unexpected && checker.wasCaught != 1)
358               ) {
359                 break;
360               }
361             }
363             // Ok, we have matched scenario. Report it was completed ok. In
364             // case of empty scenario guess it was matched but if later we
365             // find out that non empty scenario was matched then it will be
366             // a final match.
367             if (idx == eventSeq.length) {
368               if (
369                 matchIdx != -1 &&
370                 !!eventSeq.length &&
371                 this.mScenarios[matchIdx].length
372               ) {
373                 ok(
374                   false,
375                   "We have a matched scenario at index " +
376                     matchIdx +
377                     " already."
378                 );
379               }
381               if (matchIdx == -1 || eventSeq.length) {
382                 matchIdx = scnIdx;
383               }
385               // Report everything is ok.
386               for (var idx = 0; idx < eventSeq.length; idx++) {
387                 var checker = eventSeq[idx];
389                 var typeStr = eventQueue.getEventTypeAsString(checker);
390                 var msg =
391                   "Test with ID = '" + this.getEventID(checker) + "' succeed. ";
393                 if (checker.unexpected) {
394                   ok(true, msg + `There's no unexpected '${typeStr}' event.`);
395                 } else if (checker.todo) {
396                   todo(false, `Todo event '${typeStr}' was caught`);
397                 } else {
398                   ok(true, `${msg} Event '${typeStr}' was handled.`);
399                 }
400               }
401             }
402           }
403         }
405         // We don't have completely matched scenario. Report each failure/success
406         // for every scenario.
407         if (matchIdx == -1) {
408           testFailed = true;
409           for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
410             var eventSeq = this.mScenarios[scnIdx];
411             for (var idx = 0; idx < eventSeq.length; idx++) {
412               var checker = eventSeq[idx];
414               var typeStr = eventQueue.getEventTypeAsString(checker);
415               var msg =
416                 "Scenario #" +
417                 scnIdx +
418                 " of test with ID = '" +
419                 this.getEventID(checker) +
420                 "' failed. ";
422               if (checker.wasCaught > 1) {
423                 ok(false, msg + "Dupe " + typeStr + " event.");
424               }
426               if (checker.unexpected) {
427                 if (checker.wasCaught) {
428                   ok(false, msg + "There's unexpected " + typeStr + " event.");
429                 }
430               } else if (!checker.wasCaught) {
431                 var rf = checker.todo ? todo : ok;
432                 rf(false, `${msg} '${typeStr} event is missed.`);
433               }
434             }
435           }
436         }
437       }
438     }
440     this.clearEventHandler();
442     // Check if need to stop the test.
443     if (testFailed || this.mIndex == this.mInvokers.length - 1) {
444       listenA11yEvents(false);
446       var res = this.onFinish();
447       if (res != DO_NOT_FINISH_TEST) {
448         SimpleTest.executeSoon(SimpleTest.finish);
449       }
451       return;
452     }
454     // Start processing of next invoker.
455     invoker = this.getNextInvoker();
457     // Set up event listeners. Process a next invoker if no events were added.
458     if (!this.setEventHandler(invoker)) {
459       this.processNextInvoker();
460       return;
461     }
463     if (gLogger.isEnabled()) {
464       gLogger.logToConsole("Event queue: \n  invoke: " + invoker.getID());
465       gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true);
466     }
468     var infoText = "Invoke the '" + invoker.getID() + "' test { ";
469     var scnCount = this.mScenarios ? this.mScenarios.length : 0;
470     for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) {
471       infoText += "scenario #" + scnIdx + ": ";
472       var eventSeq = this.mScenarios[scnIdx];
473       for (var idx = 0; idx < eventSeq.length; idx++) {
474         infoText += eventSeq[idx].unexpected
475           ? "un"
476           : "" +
477             "expected '" +
478             eventQueue.getEventTypeAsString(eventSeq[idx]) +
479             "' event; ";
480       }
481     }
482     infoText += " }";
483     info(infoText);
485     if (invoker.invoke() == INVOKER_ACTION_FAILED) {
486       // Invoker failed to prepare action, fail and finish tests.
487       this.processNextInvoker();
488       return;
489     }
491     if (this.hasUnexpectedEventsScenario()) {
492       this.processNextInvokerInTimeout(true);
493     }
494   };
496   this.processNextInvokerInTimeout =
497     function eventQueue_processNextInvokerInTimeout(aUncondProcess) {
498       this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout");
500       // No need to wait extra timeout when a) we know we don't need to do that
501       // and b) there's no any single unexpected event.
502       if (!aUncondProcess && this.areAllEventsExpected()) {
503         // We need delay to avoid events coalesce from different invokers.
504         var queue = this;
505         SimpleTest.executeSoon(function () {
506           queue.processNextInvoker();
507         });
508         return;
509       }
511       // Check in timeout invoker didn't fire registered events.
512       window.setTimeout(
513         function (aQueue) {
514           aQueue.processNextInvoker();
515         },
516         300,
517         this
518       );
519     };
521   /**
522    * Handle events for the current invoker.
523    */
524   // eslint-disable-next-line complexity
525   this.handleEvent = function eventQueue_handleEvent(aEvent) {
526     var invoker = this.getInvoker();
527     if (!invoker) {
528       // skip events before test was started
529       return;
530     }
532     if (!this.mScenarios) {
533       // Bad invoker object, error will be reported before processing of next
534       // invoker in the queue.
535       this.processNextInvoker();
536       return;
537     }
539     if ("debugCheck" in invoker) {
540       invoker.debugCheck(aEvent);
541     }
543     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
544       var eventSeq = this.mScenarios[scnIdx];
545       for (var idx = 0; idx < eventSeq.length; idx++) {
546         var checker = eventSeq[idx];
548         // Search through handled expected events to report error if one of them
549         // is handled for a second time.
550         if (
551           !checker.unexpected &&
552           checker.wasCaught > 0 &&
553           eventQueue.isSameEvent(checker, aEvent)
554         ) {
555           checker.wasCaught++;
556           continue;
557         }
559         // Search through unexpected events, any match results in error report
560         // after this invoker processing (in case of matched scenario only).
561         if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) {
562           checker.wasCaught++;
563           continue;
564         }
566         // Report an error if we handled not expected event of unique type
567         // (i.e. event types are matched, targets differs).
568         if (
569           !checker.unexpected &&
570           checker.unique &&
571           eventQueue.compareEventTypes(checker, aEvent)
572         ) {
573           var isExpected = false;
574           for (var jdx = 0; jdx < eventSeq.length; jdx++) {
575             isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent);
576             if (isExpected) {
577               break;
578             }
579           }
581           if (!isExpected) {
582             ok(
583               false,
584               "Unique type " +
585                 eventQueue.getEventTypeAsString(checker) +
586                 " event was handled."
587             );
588           }
589         }
590       }
591     }
593     var hasMatchedCheckers = false;
594     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
595       var eventSeq = this.mScenarios[scnIdx];
597       // Check if handled event matches expected sync event.
598       var nextChecker = this.getNextExpectedEvent(eventSeq);
599       if (nextChecker) {
600         if (eventQueue.compareEvents(nextChecker, aEvent)) {
601           this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx);
602           hasMatchedCheckers = true;
603           continue;
604         }
605       }
607       // Check if handled event matches any expected async events.
608       var haveUnmatchedAsync = false;
609       for (idx = 0; idx < eventSeq.length; idx++) {
610         if (eventSeq[idx] instanceof orderChecker && haveUnmatchedAsync) {
611           break;
612         }
614         if (!eventSeq[idx].wasCaught) {
615           haveUnmatchedAsync = true;
616         }
618         if (!eventSeq[idx].unexpected && eventSeq[idx].async) {
619           if (eventQueue.compareEvents(eventSeq[idx], aEvent)) {
620             this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx);
621             hasMatchedCheckers = true;
622             break;
623           }
624         }
625       }
626     }
628     if (hasMatchedCheckers) {
629       var invoker = this.getInvoker();
630       if ("check" in invoker) {
631         invoker.check(aEvent);
632       }
633     }
635     for (idx = 0; idx < eventSeq.length; idx++) {
636       if (!eventSeq[idx].wasCaught) {
637         if (eventSeq[idx] instanceof orderChecker) {
638           eventSeq[idx].wasCaught++;
639         } else {
640           break;
641         }
642       }
643     }
645     // If we don't have more events to wait then schedule next invoker.
646     if (this.hasMatchedScenario()) {
647       if (this.mNextInvokerStatus == kInvokerNotScheduled) {
648         this.processNextInvokerInTimeout();
649       } else if (this.mNextInvokerStatus == kInvokerCanceled) {
650         this.setInvokerStatus(
651           kInvokerPending,
652           "Full match. Void the cancelation of next invoker processing"
653         );
654       }
655       return;
656     }
658     // If we have scheduled a next invoker then cancel in case of match.
659     if (this.mNextInvokerStatus == kInvokerPending && hasMatchedCheckers) {
660       this.setInvokerStatus(
661         kInvokerCanceled,
662         "Cancel the scheduled invoker in case of match"
663       );
664     }
665   };
667   // Helpers
668   this.processMatchedChecker = function eventQueue_function(
669     aEvent,
670     aMatchedChecker,
671     aScenarioIdx,
672     aEventIdx
673   ) {
674     aMatchedChecker.wasCaught++;
676     if ("check" in aMatchedChecker) {
677       aMatchedChecker.check(aEvent);
678     }
680     eventQueue.logEvent(
681       aEvent,
682       aMatchedChecker,
683       aScenarioIdx,
684       aEventIdx,
685       this.areExpectedEventsLeft(),
686       this.mNextInvokerStatus
687     );
688   };
690   this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent(
691     aEventSeq
692   ) {
693     if (!("idx" in aEventSeq)) {
694       aEventSeq.idx = 0;
695     }
697     while (
698       aEventSeq.idx < aEventSeq.length &&
699       (aEventSeq[aEventSeq.idx].unexpected ||
700         aEventSeq[aEventSeq.idx].todo ||
701         aEventSeq[aEventSeq.idx].async ||
702         aEventSeq[aEventSeq.idx] instanceof orderChecker ||
703         aEventSeq[aEventSeq.idx].wasCaught > 0)
704     ) {
705       aEventSeq.idx++;
706     }
708     return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null;
709   };
711   this.areExpectedEventsLeft = function eventQueue_areExpectedEventsLeft(
712     aScenario
713   ) {
714     function scenarioHasUnhandledExpectedEvent(aEventSeq) {
715       // Check if we have unhandled async (can be anywhere in the sequance) or
716       // sync expcected events yet.
717       for (var idx = 0; idx < aEventSeq.length; idx++) {
718         if (
719           !aEventSeq[idx].unexpected &&
720           !aEventSeq[idx].todo &&
721           !aEventSeq[idx].wasCaught &&
722           !(aEventSeq[idx] instanceof orderChecker)
723         ) {
724           return true;
725         }
726       }
728       return false;
729     }
731     if (aScenario) {
732       return scenarioHasUnhandledExpectedEvent(aScenario);
733     }
735     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
736       var eventSeq = this.mScenarios[scnIdx];
737       if (scenarioHasUnhandledExpectedEvent(eventSeq)) {
738         return true;
739       }
740     }
741     return false;
742   };
744   this.areAllEventsExpected = function eventQueue_areAllEventsExpected() {
745     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
746       var eventSeq = this.mScenarios[scnIdx];
747       for (var idx = 0; idx < eventSeq.length; idx++) {
748         if (eventSeq[idx].unexpected || eventSeq[idx].todo) {
749           return false;
750         }
751       }
752     }
754     return true;
755   };
757   this.isUnexpectedEventScenario =
758     function eventQueue_isUnexpectedEventsScenario(aScenario) {
759       for (var idx = 0; idx < aScenario.length; idx++) {
760         if (!aScenario[idx].unexpected && !aScenario[idx].todo) {
761           break;
762         }
763       }
765       return idx == aScenario.length;
766     };
768   this.hasUnexpectedEventsScenario =
769     function eventQueue_hasUnexpectedEventsScenario() {
770       if (this.getInvoker().noEventsOnAction) {
771         return true;
772       }
774       for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
775         if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) {
776           return true;
777         }
778       }
780       return false;
781     };
783   this.hasMatchedScenario = function eventQueue_hasMatchedScenario() {
784     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
785       var scn = this.mScenarios[scnIdx];
786       if (
787         !this.isUnexpectedEventScenario(scn) &&
788         !this.areExpectedEventsLeft(scn)
789       ) {
790         return true;
791       }
792     }
793     return false;
794   };
796   this.getInvoker = function eventQueue_getInvoker() {
797     return this.mInvokers[this.mIndex];
798   };
800   this.getNextInvoker = function eventQueue_getNextInvoker() {
801     return this.mInvokers[++this.mIndex];
802   };
804   this.setEventHandler = function eventQueue_setEventHandler(aInvoker) {
805     if (!("scenarios" in aInvoker) || !aInvoker.scenarios.length) {
806       var eventSeq = aInvoker.eventSeq;
807       var unexpectedEventSeq = aInvoker.unexpectedEventSeq;
808       if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) {
809         eventSeq = [new invokerChecker(this.mDefEventType, aInvoker.DOMNode)];
810       }
812       if (eventSeq || unexpectedEventSeq) {
813         defineScenario(aInvoker, eventSeq, unexpectedEventSeq);
814       }
815     }
817     if (aInvoker.noEventsOnAction) {
818       return true;
819     }
821     this.mScenarios = aInvoker.scenarios;
822     if (!this.mScenarios || !this.mScenarios.length) {
823       ok(false, "Broken invoker '" + aInvoker.getID() + "'");
824       return false;
825     }
827     // Register event listeners.
828     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
829       var eventSeq = this.mScenarios[scnIdx];
831       if (gLogger.isEnabled()) {
832         var msg =
833           "scenario #" +
834           scnIdx +
835           ", registered events number: " +
836           eventSeq.length;
837         gLogger.logToConsole(msg);
838         gLogger.logToDOM(msg, true);
839       }
841       // Do not warn about empty event sequances when more than one scenario
842       // was registered.
843       if (this.mScenarios.length == 1 && !eventSeq.length) {
844         ok(
845           false,
846           "Broken scenario #" +
847             scnIdx +
848             " of invoker '" +
849             aInvoker.getID() +
850             "'. No registered events"
851         );
852         return false;
853       }
855       for (var idx = 0; idx < eventSeq.length; idx++) {
856         eventSeq[idx].wasCaught = 0;
857       }
859       for (var idx = 0; idx < eventSeq.length; idx++) {
860         if (gLogger.isEnabled()) {
861           var msg = "registered";
862           if (eventSeq[idx].unexpected) {
863             msg += " unexpected";
864           }
865           if (eventSeq[idx].async) {
866             msg += " async";
867           }
869           msg +=
870             ": event type: " +
871             eventQueue.getEventTypeAsString(eventSeq[idx]) +
872             ", target: " +
873             eventQueue.getEventTargetDescr(eventSeq[idx], true);
875           gLogger.logToConsole(msg);
876           gLogger.logToDOM(msg, true);
877         }
879         var eventType = eventSeq[idx].type;
880         if (typeof eventType == "string") {
881           // DOM event
882           var target = eventQueue.getEventTarget(eventSeq[idx]);
883           if (!target) {
884             ok(false, "no target for DOM event!");
885             return false;
886           }
887           var phase = eventQueue.getEventPhase(eventSeq[idx]);
888           target.addEventListener(eventType, this, phase);
889         } else {
890           // A11y event
891           addA11yEventListener(eventType, this);
892         }
893       }
894     }
896     return true;
897   };
899   this.clearEventHandler = function eventQueue_clearEventHandler() {
900     if (!this.mScenarios) {
901       return;
902     }
904     for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
905       var eventSeq = this.mScenarios[scnIdx];
906       for (var idx = 0; idx < eventSeq.length; idx++) {
907         var eventType = eventSeq[idx].type;
908         if (typeof eventType == "string") {
909           // DOM event
910           var target = eventQueue.getEventTarget(eventSeq[idx]);
911           var phase = eventQueue.getEventPhase(eventSeq[idx]);
912           target.removeEventListener(eventType, this, phase);
913         } else {
914           // A11y event
915           removeA11yEventListener(eventType, this);
916         }
917       }
918     }
919     this.mScenarios = null;
920   };
922   this.getEventID = function eventQueue_getEventID(aChecker) {
923     if ("getID" in aChecker) {
924       return aChecker.getID();
925     }
927     var invoker = this.getInvoker();
928     return invoker.getID();
929   };
931   this.setInvokerStatus = function eventQueue_setInvokerStatus(
932     aStatus,
933     aLogMsg
934   ) {
935     this.mNextInvokerStatus = aStatus;
937     // Uncomment it to debug invoker processing logic.
938     // gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg));
939   };
941   this.mDefEventType = aEventType;
943   this.mInvokers = [];
944   this.mIndex = -1;
945   this.mScenarios = null;
947   this.mNextInvokerStatus = kInvokerNotScheduled;
950 // //////////////////////////////////////////////////////////////////////////////
951 // eventQueue static members and constants
953 const kInvokerNotScheduled = 0;
954 const kInvokerPending = 1;
955 const kInvokerCanceled = 2;
957 eventQueue.getEventTypeAsString = function eventQueue_getEventTypeAsString(
958   aEventOrChecker
959 ) {
960   if (Event.isInstance(aEventOrChecker)) {
961     return aEventOrChecker.type;
962   }
964   if (aEventOrChecker instanceof nsIAccessibleEvent) {
965     return eventTypeToString(aEventOrChecker.eventType);
966   }
968   return typeof aEventOrChecker.type == "string"
969     ? aEventOrChecker.type
970     : eventTypeToString(aEventOrChecker.type);
973 eventQueue.getEventTargetDescr = function eventQueue_getEventTargetDescr(
974   aEventOrChecker,
975   aDontForceTarget
976 ) {
977   if (Event.isInstance(aEventOrChecker)) {
978     return prettyName(aEventOrChecker.originalTarget);
979   }
981   // XXXbz this block doesn't seem to be reachable...
982   if (Event.isInstance(aEventOrChecker)) {
983     return prettyName(aEventOrChecker.accessible);
984   }
986   var descr = aEventOrChecker.targetDescr;
987   if (descr) {
988     return descr;
989   }
991   if (aDontForceTarget) {
992     return "no target description";
993   }
995   var target = "target" in aEventOrChecker ? aEventOrChecker.target : null;
996   return prettyName(target);
999 eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) {
1000   return "phase" in aChecker ? aChecker.phase : true;
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;
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;
1024 eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) {
1025   if (!eventQueue.compareEventTypes(aChecker, aEvent)) {
1026     return false;
1027   }
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   }
1035   var target1 = aChecker.target;
1036   if (target1 instanceof nsIAccessible) {
1037     var target2 = Event.isInstance(aEvent)
1038       ? getAccessible(aEvent.target)
1039       : aEvent.accessible;
1041     return target1 == target2;
1042   }
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;
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   );
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   }
1080   if (aMsg) {
1081     msg += " (" + aMsg + ")";
1082   }
1084   return msg;
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   }
1103   var infoMsg =
1104     "unhandled expected events: " +
1105     aAreExpectedEventsLeft +
1106     ", " +
1107     eventQueue.invokerStatusToMsg(aInvokerStatus);
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);
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);
1134 // //////////////////////////////////////////////////////////////////////////////
1135 // Action sequence
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   };
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     }
1177     this.items[this.idx].startProcess();
1178   };
1180   this.items = [];
1181   this.idx = -1;
1184 // //////////////////////////////////////////////////////////////////////////////
1185 // Event queue invokers
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   }
1196   // Create unified event sequence concatenating expected and unexpected
1197   // events.
1198   if (!aEventSeq) {
1199     aEventSeq = [];
1200   }
1202   for (var idx = 0; idx < aEventSeq.length; idx++) {
1203     aEventSeq[idx].unexpected |= false;
1204     aEventSeq[idx].async |= false;
1205   }
1207   if (aUnexpectedEventSeq) {
1208     for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) {
1209       aUnexpectedEventSeq[idx].unexpected = true;
1210       aUnexpectedEventSeq[idx].async = false;
1211     }
1213     aEventSeq = aEventSeq.concat(aUnexpectedEventSeq);
1214   }
1216   aInvoker.scenarios.push(aEventSeq);
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.
1224  * Note, checker object or array of checker objects is optional.
1225  */
1228  * Click invoker.
1229  */
1230 function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) {
1231   this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);
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     }
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     }
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   };
1271   this.finalCheck = function synthClick_finalCheck() {
1272     // Scroll top window back.
1273     window.top.scrollTo(0, 0);
1274   };
1276   this.getID = function synthClick_getID() {
1277     return prettyName(aNodeOrID) + " click";
1278   };
1282  * Scrolls the node into view.
1283  */
1284 function scrollIntoView(aNodeOrID, aCheckerOrEventSeq) {
1285   this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);
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   };
1297   this.getID = function scrollIntoView_getID() {
1298     return prettyName(aNodeOrID) + " scrollIntoView";
1299   };
1303  * Mouse move invoker.
1304  */
1305 function synthMouseMove(aID, aCheckerOrEventSeq) {
1306   this.__proto__ = new synthAction(aID, aCheckerOrEventSeq);
1308   this.invoke = function synthMouseMove_invoke() {
1309     synthesizeMouse(this.DOMNode, 5, 5, { type: "mousemove" });
1310     synthesizeMouse(this.DOMNode, 6, 6, { type: "mousemove" });
1311   };
1313   this.getID = function synthMouseMove_getID() {
1314     return prettyName(aID) + " mouse move";
1315   };
1319  * General key press invoker.
1320  */
1321 function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) {
1322   this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);
1324   this.invoke = function synthKey_invoke() {
1325     synthesizeKey(this.mKey, this.mArgs, this.mWindow);
1326   };
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   };
1373   this.mKey = aKey;
1374   this.mArgs = aArgs ? aArgs : {};
1375   this.mWindow = aArgs ? aArgs.window : null;
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   );
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   );
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   );
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   );
1427  * Up arrow key invoker.
1428  */
1429 function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) {
1430   this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, aCheckerOrEventSeq);
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   );
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   );
1458  * Home key invoker.
1459  */
1460 function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) {
1461   this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq);
1465  * End key invoker.
1466  */
1467 function synthEndKey(aNodeOrID, aCheckerOrEventSeq) {
1468   this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq);
1472  * Enter key invoker
1473  */
1474 function synthEnterKey(aID, aCheckerOrEventSeq) {
1475   this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq);
1479  * Synth alt + down arrow to open combobox.
1480  */
1481 function synthOpenComboboxKey(aID, aCheckerOrEventSeq) {
1482   this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true });
1484   this.getID = function synthOpenComboboxKey_getID() {
1485     return "open combobox (alt + down arrow) " + prettyName(aID);
1486   };
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);
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   };
1506   this.getID = function synthFocus_getID() {
1507     return prettyName(aNodeOrID) + " focus";
1508   };
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);
1521   this.invoke = function synthFocus_invoke() {
1522     this.DOMNode.body.focus();
1523   };
1525   this.getID = function synthFocus_getID() {
1526     return prettyName(aNodeOrID) + " frame document focus";
1527   };
1531  * Change the current item when the widget doesn't have a focus.
1532  */
1533 function changeCurrentItem(aID, aItemID) {
1534   this.eventSeq = [new nofocusChecker()];
1536   this.invoke = function changeCurrentItem_invoke() {
1537     var controlNode = getNode(aID);
1538     var itemNode = getNode(aItemID);
1540     // HTML
1541     if (controlNode.localName == "input") {
1542       if (controlNode.checked) {
1543         this.reportError();
1544       }
1546       controlNode.checked = true;
1547       return;
1548     }
1550     if (controlNode.localName == "select") {
1551       if (controlNode.selectedIndex == itemNode.index) {
1552         this.reportError();
1553       }
1555       controlNode.selectedIndex = itemNode.index;
1556       return;
1557     }
1559     // XUL
1560     if (controlNode.localName == "tree") {
1561       if (controlNode.currentIndex == aItemID) {
1562         this.reportError();
1563       }
1565       controlNode.currentIndex = aItemID;
1566       return;
1567     }
1569     if (controlNode.localName == "menulist") {
1570       if (controlNode.selectedItem == itemNode) {
1571         this.reportError();
1572       }
1574       controlNode.selectedItem = itemNode;
1575       return;
1576     }
1578     if (controlNode.currentItem == itemNode) {
1579       ok(
1580         false,
1581         "Error in test: proposed current item is already current" +
1582           prettyName(aID)
1583       );
1584     }
1586     controlNode.currentItem = itemNode;
1587   };
1589   this.getID = function changeCurrentItem_getID() {
1590     return "current item change for " + prettyName(aID);
1591   };
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   };
1604  * Toggle top menu invoker.
1605  */
1606 function toggleTopMenu(aID, aCheckerOrEventSeq) {
1607   this.__proto__ = new synthKey(aID, "VK_ALT", null, aCheckerOrEventSeq);
1609   this.getID = function toggleTopMenu_getID() {
1610     return "toggle top menu on " + prettyName(aID);
1611   };
1615  * Context menu invoker.
1616  */
1617 function synthContextMenu(aID, aCheckerOrEventSeq) {
1618   this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, {
1619     button: 0,
1620     type: "contextmenu",
1621   });
1623   this.getID = function synthContextMenu_getID() {
1624     return "context menu on " + prettyName(aID);
1625   };
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   ];
1636   this.invoke = function openCombobox_invoke() {
1637     getNode(aComboboxID).focus();
1638     synthesizeKey("VK_DOWN", { altKey: true });
1639   };
1641   this.getID = function openCombobox_getID() {
1642     return "open combobox " + prettyName(aComboboxID);
1643   };
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   ];
1654   this.invoke = function closeCombobox_invoke() {
1655     synthesizeKey("KEY_Escape");
1656   };
1658   this.getID = function closeCombobox_getID() {
1659     return "close combobox " + prettyName(aComboboxID);
1660   };
1664  * Select all invoker.
1665  */
1666 function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) {
1667   this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);
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   };
1677   this.getID = function synthSelectAll_getID() {
1678     return aNodeOrID + " selectall";
1679   };
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   }
1700   this.getID = function moveToLineEnd_getID() {
1701     return "move to line end in " + prettyName(aID);
1702   };
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   );
1714   this.invoke = function moveToPrevLineEnd_invoke() {
1715     synthesizeKey("KEY_ArrowUp");
1717     if (MAC) {
1718       synthesizeKey("Key_ArrowRight", { metaKey: true });
1719     } else {
1720       synthesizeKey("KEY_End");
1721     }
1722   };
1724   this.getID = function moveToPrevLineEnd_getID() {
1725     return "move to previous line end in " + prettyName(aID);
1726   };
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   }
1747   this.getID = function moveToLineEnd_getID() {
1748     return "move to line start in " + prettyName(aID);
1749   };
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   }
1772   this.getID = function moveToTextStart_getID() {
1773     return "move to text start in " + prettyName(aID);
1774   };
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;
1793   this.invoke = function moveCaretToDOMPoint_invoke() {
1794     if (this.focusNode) {
1795       this.focusNode.focus();
1796     }
1798     var selection = this.DOMPointNode.ownerGlobal.getSelection();
1799     var selRange = selection.getRangeAt(0);
1800     selRange.setStart(this.DOMPointNode, aDOMPointOffset);
1801     selRange.collapse(true);
1803     selection.removeRange(selRange);
1804     selection.addRange(selRange);
1805   };
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   };
1818   this.finalCheck = function moveCaretToDOMPoint_finalCheck() {
1819     if (aCheckFunc) {
1820       aCheckFunc.call();
1821     }
1822   };
1824   this.eventSeq = [new caretMoveChecker(aExpectedOffset, true, this.target)];
1826   if (this.focus) {
1827     this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus));
1828   }
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;
1839   this.invoke = function setCaretOffset_invoke() {
1840     this.target.caretOffset = this.offset;
1841   };
1843   this.getID = function setCaretOffset_getID() {
1844     return "Set caretOffset on " + prettyName(aID) + " at " + this.offset;
1845   };
1847   this.eventSeq = [new caretMoveChecker(this.offset, true, this.target)];
1849   if (this.focus) {
1850     this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus));
1851   }
1854 // //////////////////////////////////////////////////////////////////////////////
1855 // Event queue checkers
1858  * Common invoker checker (see eventSeq of eventQueue).
1859  */
1860 function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) {
1861   this.type = aEventType;
1862   this.async = aIsAsync;
1864   this.__defineGetter__("target", invokerChecker_targetGetter);
1865   this.__defineSetter__("target", invokerChecker_targetSetter);
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     }
1876     return this.mTarget;
1877   }
1879   function invokerChecker_targetSetter(aValue) {
1880     this.mTarget = aValue;
1881     return this.mTarget;
1882   }
1884   this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter);
1886   function invokerChecker_targetDescrGetter() {
1887     if (typeof this.mTarget == "function") {
1888       return this.mTarget.name + ", arg: " + this.mTargetFuncArg;
1889     }
1891     return prettyName(this.mTarget);
1892   }
1894   this.mTarget = aTargetOrFunc;
1895   this.mTargetFuncArg = aTargetFuncArg;
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);
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;
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   );
1932   this.unexpected = true;
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   );
1947 function focusChecker(aTargetOrFunc, aTargetFuncArg) {
1948   this.__proto__ = new invokerChecker(
1949     EVENT_FOCUS,
1950     aTargetOrFunc,
1951     aTargetFuncArg,
1952     false
1953   );
1955   this.unique = true; // focus event must be unique for invoker action
1957   this.check = function focusChecker_check(aEvent) {
1958     testStates(aEvent.accessible, STATE_FOCUSED);
1959   };
1962 function nofocusChecker(aID) {
1963   this.__proto__ = new focusChecker(aID);
1964   this.unexpected = true;
1968  * Text inserted/removed events checker.
1969  * @param aFromUser  [in, optional] kNotFromUserInput or kFromUserInput
1970  */
1971 function textChangeChecker(
1972   aID,
1973   aStart,
1974   aEnd,
1975   aTextOrFunc,
1976   aIsInserted,
1977   aFromUser,
1978   aAsync
1979 ) {
1980   this.target = getNode(aID);
1981   this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED;
1982   this.startOffset = aStart;
1983   this.endOffset = aEnd;
1984   this.textOrFunc = aTextOrFunc;
1985   this.async = aAsync;
1987   this.match = function stextChangeChecker_match(aEvent) {
1988     if (
1989       !(aEvent instanceof nsIAccessibleTextChangeEvent) ||
1990       aEvent.accessible !== getAccessible(this.target)
1991     ) {
1992       return false;
1993     }
1995     let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent);
1996     let modifiedText =
1997       typeof this.textOrFunc === "function"
1998         ? this.textOrFunc()
1999         : this.textOrFunc;
2000     return modifiedText === tcEvent.modifiedText;
2001   };
2003   this.check = function textChangeChecker_check(aEvent) {
2004     aEvent.QueryInterface(nsIAccessibleTextChangeEvent);
2006     var modifiedText =
2007       typeof this.textOrFunc == "function"
2008         ? this.textOrFunc()
2009         : this.textOrFunc;
2010     var modifiedTextLen =
2011       this.endOffset == -1 ? modifiedText.length : aEnd - aStart;
2013     is(
2014       aEvent.start,
2015       this.startOffset,
2016       "Wrong start offset for " + prettyName(aID)
2017     );
2018     is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID));
2019     var changeInfo = aIsInserted ? "inserted" : "removed";
2020     is(
2021       aEvent.isInserted,
2022       aIsInserted,
2023       "Text was " + changeInfo + " for " + prettyName(aID)
2024     );
2025     is(
2026       aEvent.modifiedText,
2027       modifiedText,
2028       "Wrong " + changeInfo + " text for " + prettyName(aID)
2029     );
2030     if (typeof aFromUser != "undefined") {
2031       is(
2032         aEvent.isFromUserInput,
2033         aFromUser,
2034         "wrong value of isFromUserInput() for " + prettyName(aID)
2035       );
2036     }
2037   };
2041  * Caret move events checker.
2042  */
2043 function caretMoveChecker(
2044   aCaretOffset,
2045   aIsSelectionCollapsed,
2046   aTargetOrFunc,
2047   aTargetFuncArg,
2048   aIsAsync
2049 ) {
2050   this.__proto__ = new invokerChecker(
2051     EVENT_TEXT_CARET_MOVED,
2052     aTargetOrFunc,
2053     aTargetFuncArg,
2054     aIsAsync
2055   );
2057   this.check = function caretMoveChecker_check(aEvent) {
2058     let evt = aEvent.QueryInterface(nsIAccessibleCaretMoveEvent);
2059     is(
2060       evt.caretOffset,
2061       aCaretOffset,
2062       "Wrong caret offset for " + prettyName(aEvent.accessible)
2063     );
2064     is(
2065       evt.isSelectionCollapsed,
2066       aIsSelectionCollapsed,
2067       "wrong collapsed value for  " + prettyName(aEvent.accessible)
2068     );
2069   };
2072 function asyncCaretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) {
2073   this.__proto__ = new caretMoveChecker(
2074     aCaretOffset,
2075     true, // Caret is collapsed
2076     aTargetOrFunc,
2077     aTargetFuncArg,
2078     true
2079   );
2083  * Text selection change checker.
2084  */
2085 function textSelectionChecker(
2086   aID,
2087   aStartOffset,
2088   aEndOffset,
2089   aRangeStartContainer,
2090   aRangeStartOffset,
2091   aRangeEndContainer,
2092   aRangeEndOffset
2093 ) {
2094   this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID);
2096   this.check = function textSelectionChecker_check(aEvent) {
2097     if (aStartOffset == aEndOffset) {
2098       ok(true, "Collapsed selection triggered text selection change event.");
2099     } else {
2100       testTextGetSelection(aID, aStartOffset, aEndOffset, 0);
2102       // Test selection test range
2103       let selectionRanges = aEvent.QueryInterface(
2104         nsIAccessibleTextSelectionChangeEvent
2105       ).selectionRanges;
2106       let range = selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
2107       is(
2108         range.startContainer,
2109         getAccessible(aRangeStartContainer),
2110         "correct range start container"
2111       );
2112       is(range.startOffset, aRangeStartOffset, "correct range start offset");
2113       is(range.endOffset, aRangeEndOffset, "correct range end offset");
2114       is(
2115         range.endContainer,
2116         getAccessible(aRangeEndContainer),
2117         "correct range end container"
2118       );
2119     }
2120   };
2124  * Object attribute changed checker
2125  */
2126 function objAttrChangedChecker(aID, aAttr) {
2127   this.__proto__ = new invokerChecker(EVENT_OBJECT_ATTRIBUTE_CHANGED, aID);
2129   this.check = function objAttrChangedChecker_check(aEvent) {
2130     var event = null;
2131     try {
2132       var event = aEvent.QueryInterface(
2133         nsIAccessibleObjectAttributeChangedEvent
2134       );
2135     } catch (e) {
2136       ok(false, "Object attribute changed event was expected");
2137     }
2139     if (!event) {
2140       return;
2141     }
2143     is(
2144       event.changedAttribute,
2145       aAttr,
2146       "Wrong attribute name of the object attribute changed event."
2147     );
2148   };
2150   this.match = function objAttrChangedChecker_match(aEvent) {
2151     if (aEvent instanceof nsIAccessibleObjectAttributeChangedEvent) {
2152       var scEvent = aEvent.QueryInterface(
2153         nsIAccessibleObjectAttributeChangedEvent
2154       );
2155       return (
2156         aEvent.accessible == getAccessible(this.target) &&
2157         scEvent.changedAttribute == aAttr
2158       );
2159     }
2160     return false;
2161   };
2165  * State change checker.
2166  */
2167 function stateChangeChecker(
2168   aState,
2169   aIsExtraState,
2170   aIsEnabled,
2171   aTargetOrFunc,
2172   aTargetFuncArg,
2173   aIsAsync,
2174   aSkipCurrentStateCheck
2175 ) {
2176   this.__proto__ = new invokerChecker(
2177     EVENT_STATE_CHANGE,
2178     aTargetOrFunc,
2179     aTargetFuncArg,
2180     aIsAsync
2181   );
2183   this.check = function stateChangeChecker_check(aEvent) {
2184     var event = null;
2185     try {
2186       var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
2187     } catch (e) {
2188       ok(false, "State change event was expected");
2189     }
2191     if (!event) {
2192       return;
2193     }
2195     is(
2196       event.isExtraState,
2197       aIsExtraState,
2198       "Wrong extra state bit of the statechange event."
2199     );
2200     isState(
2201       event.state,
2202       aState,
2203       aIsExtraState,
2204       "Wrong state of the statechange event."
2205     );
2206     is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state");
2208     if (aSkipCurrentStateCheck) {
2209       todo(false, "State checking was skipped!");
2210       return;
2211     }
2213     var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0;
2214     var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0;
2215     var unxpdState = aIsEnabled ? 0 : aIsExtraState ? 0 : aState;
2216     var unxpdExtraState = aIsEnabled ? 0 : aIsExtraState ? aState : 0;
2217     testStates(
2218       event.accessible,
2219       state,
2220       extraState,
2221       unxpdState,
2222       unxpdExtraState
2223     );
2224   };
2226   this.match = function stateChangeChecker_match(aEvent) {
2227     if (aEvent instanceof nsIAccessibleStateChangeEvent) {
2228       var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
2229       return (
2230         aEvent.accessible == getAccessible(this.target) &&
2231         scEvent.state == aState
2232       );
2233     }
2234     return false;
2235   };
2238 function asyncStateChangeChecker(
2239   aState,
2240   aIsExtraState,
2241   aIsEnabled,
2242   aTargetOrFunc,
2243   aTargetFuncArg
2244 ) {
2245   this.__proto__ = new stateChangeChecker(
2246     aState,
2247     aIsExtraState,
2248     aIsEnabled,
2249     aTargetOrFunc,
2250     aTargetFuncArg,
2251     true
2252   );
2256  * Expanded state change checker.
2257  */
2258 function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) {
2259   this.__proto__ = new invokerChecker(
2260     EVENT_STATE_CHANGE,
2261     aTargetOrFunc,
2262     aTargetFuncArg
2263   );
2265   this.check = function expandedStateChecker_check(aEvent) {
2266     var event = null;
2267     try {
2268       var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
2269     } catch (e) {
2270       ok(false, "State change event was expected");
2271     }
2273     if (!event) {
2274       return;
2275     }
2277     is(event.state, STATE_EXPANDED, "Wrong state of the statechange event.");
2278     is(
2279       event.isExtraState,
2280       false,
2281       "Wrong extra state bit of the statechange event."
2282     );
2283     is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state");
2285     testStates(event.accessible, aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED);
2286   };
2289 // //////////////////////////////////////////////////////////////////////////////
2290 // Event sequances (array of predefined checkers)
2293  * Event seq for single selection change.
2294  */
2295 function selChangeSeq(aUnselectedID, aSelectedID) {
2296   if (!aUnselectedID) {
2297     return [
2298       new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
2299       new invokerChecker(EVENT_SELECTION, aSelectedID),
2300     ];
2301   }
2303   // Return two possible scenarios: depending on widget type when selection is
2304   // moved the the order of items that get selected and unselected may vary.
2305   return [
2306     [
2307       new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID),
2308       new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
2309       new invokerChecker(EVENT_SELECTION, aSelectedID),
2310     ],
2311     [
2312       new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
2313       new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID),
2314       new invokerChecker(EVENT_SELECTION, aSelectedID),
2315     ],
2316   ];
2320  * Event seq for item removed form the selection.
2321  */
2322 function selRemoveSeq(aUnselectedID) {
2323   return [
2324     new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID),
2325     new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID),
2326   ];
2330  * Event seq for item added to the selection.
2331  */
2332 function selAddSeq(aSelectedID) {
2333   return [
2334     new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
2335     new invokerChecker(EVENT_SELECTION_ADD, aSelectedID),
2336   ];
2339 // //////////////////////////////////////////////////////////////////////////////
2340 // Private implementation details.
2341 // //////////////////////////////////////////////////////////////////////////////
2343 // //////////////////////////////////////////////////////////////////////////////
2344 // General
2346 var gA11yEventListeners = {};
2347 var gA11yEventApplicantsCount = 0;
2349 var gA11yEventObserver = {
2350   // eslint-disable-next-line complexity
2351   observe: function observe(aSubject, aTopic, aData) {
2352     if (aTopic != "accessible-event") {
2353       return;
2354     }
2356     var event;
2357     try {
2358       event = aSubject.QueryInterface(nsIAccessibleEvent);
2359     } catch (ex) {
2360       // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered.
2361       // Remove the leftover observer, otherwise it "leaks" to all the following tests.
2362       Services.obs.removeObserver(this, "accessible-event");
2363       // Forward the exception, with added explanation.
2364       throw new Error(
2365         "[accessible/events.js, gA11yEventObserver.observe] This is expected " +
2366           `if a previous test has been aborted... Initial exception was: [ ${ex} ]`
2367       );
2368     }
2369     var listenersArray = gA11yEventListeners[event.eventType];
2371     var eventFromDumpArea = false;
2372     if (gLogger.isEnabled()) {
2373       // debug stuff
2374       eventFromDumpArea = true;
2376       var target = event.DOMNode;
2377       var dumpElm = gA11yEventDumpID
2378         ? document.getElementById(gA11yEventDumpID)
2379         : null;
2381       if (dumpElm) {
2382         var parent = target;
2383         while (parent && parent != dumpElm) {
2384           parent = parent.parentNode;
2385         }
2386       }
2388       if (!dumpElm || parent != dumpElm) {
2389         var type = eventTypeToString(event.eventType);
2390         var info = "Event type: " + type;
2392         if (event instanceof nsIAccessibleStateChangeEvent) {
2393           var stateStr = statesToString(
2394             event.isExtraState ? 0 : event.state,
2395             event.isExtraState ? event.state : 0
2396           );
2397           info += ", state: " + stateStr + ", is enabled: " + event.isEnabled;
2398         } else if (event instanceof nsIAccessibleTextChangeEvent) {
2399           info +=
2400             ", start: " +
2401             event.start +
2402             ", length: " +
2403             event.length +
2404             ", " +
2405             (event.isInserted ? "inserted" : "removed") +
2406             " text: " +
2407             event.modifiedText;
2408         }
2410         info += ". Target: " + prettyName(event.accessible);
2412         if (listenersArray) {
2413           info += ". Listeners count: " + listenersArray.length;
2414         }
2416         if (gLogger.hasFeature("parentchain:" + type)) {
2417           info += "\nParent chain:\n";
2418           var acc = event.accessible;
2419           while (acc) {
2420             info += "  " + prettyName(acc) + "\n";
2421             acc = acc.parent;
2422           }
2423         }
2425         eventFromDumpArea = false;
2426         gLogger.log(info);
2427       }
2428     }
2430     // Do not notify listeners if event is result of event log changes.
2431     if (!listenersArray || eventFromDumpArea) {
2432       return;
2433     }
2435     for (var index = 0; index < listenersArray.length; index++) {
2436       listenersArray[index].handleEvent(event);
2437     }
2438   },
2441 function listenA11yEvents(aStartToListen) {
2442   if (aStartToListen) {
2443     // Add observer when adding the first applicant only.
2444     if (!gA11yEventApplicantsCount++) {
2445       Services.obs.addObserver(gA11yEventObserver, "accessible-event");
2446     }
2447   } else {
2448     // Remove observer when there are no more applicants only.
2449     // '< 0' case should not happen, but just in case: removeObserver() will throw.
2450     // eslint-disable-next-line no-lonely-if
2451     if (--gA11yEventApplicantsCount <= 0) {
2452       Services.obs.removeObserver(gA11yEventObserver, "accessible-event");
2453     }
2454   }
2457 function addA11yEventListener(aEventType, aEventHandler) {
2458   if (!(aEventType in gA11yEventListeners)) {
2459     gA11yEventListeners[aEventType] = [];
2460   }
2462   var listenersArray = gA11yEventListeners[aEventType];
2463   var index = listenersArray.indexOf(aEventHandler);
2464   if (index == -1) {
2465     listenersArray.push(aEventHandler);
2466   }
2469 function removeA11yEventListener(aEventType, aEventHandler) {
2470   var listenersArray = gA11yEventListeners[aEventType];
2471   if (!listenersArray) {
2472     return false;
2473   }
2475   var index = listenersArray.indexOf(aEventHandler);
2476   if (index == -1) {
2477     return false;
2478   }
2480   listenersArray.splice(index, 1);
2482   if (!listenersArray.length) {
2483     gA11yEventListeners[aEventType] = null;
2484     delete gA11yEventListeners[aEventType];
2485   }
2487   return true;
2491  * Used to dump debug information.
2492  */
2493 var gLogger = {
2494   /**
2495    * Return true if dump is enabled.
2496    */
2497   isEnabled: function debugOutput_isEnabled() {
2498     return (
2499       gA11yEventDumpID || gA11yEventDumpToConsole || gA11yEventDumpToAppConsole
2500     );
2501   },
2503   /**
2504    * Dump information into DOM and console if applicable.
2505    */
2506   log: function logger_log(aMsg) {
2507     this.logToConsole(aMsg);
2508     this.logToAppConsole(aMsg);
2509     this.logToDOM(aMsg);
2510   },
2512   /**
2513    * Log message to DOM.
2514    *
2515    * @param aMsg          [in] the primary message
2516    * @param aHasIndent    [in, optional] if specified the message has an indent
2517    * @param aPreEmphText  [in, optional] the text is colored and appended prior
2518    *                        primary message
2519    */
2520   logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) {
2521     if (gA11yEventDumpID == "") {
2522       return;
2523     }
2525     var dumpElm = document.getElementById(gA11yEventDumpID);
2526     if (!dumpElm) {
2527       ok(
2528         false,
2529         "No dump element '" + gA11yEventDumpID + "' within the document!"
2530       );
2531       return;
2532     }
2534     var containerTagName =
2535       ChromeUtils.getClassName(document) == "HTMLDocument"
2536         ? "div"
2537         : "description";
2539     var container = document.createElement(containerTagName);
2540     if (aHasIndent) {
2541       container.setAttribute("style", "padding-left: 10px;");
2542     }
2544     if (aPreEmphText) {
2545       var inlineTagName =
2546         ChromeUtils.getClassName(document) == "HTMLDocument"
2547           ? "span"
2548           : "description";
2549       var emphElm = document.createElement(inlineTagName);
2550       emphElm.setAttribute("style", "color: blue;");
2551       emphElm.textContent = aPreEmphText;
2553       container.appendChild(emphElm);
2554     }
2556     var textNode = document.createTextNode(aMsg);
2557     container.appendChild(textNode);
2559     dumpElm.appendChild(container);
2560   },
2562   /**
2563    * Log message to console.
2564    */
2565   logToConsole: function logger_logToConsole(aMsg) {
2566     if (gA11yEventDumpToConsole) {
2567       dump("\n" + aMsg + "\n");
2568     }
2569   },
2571   /**
2572    * Log message to error console.
2573    */
2574   logToAppConsole: function logger_logToAppConsole(aMsg) {
2575     if (gA11yEventDumpToAppConsole) {
2576       Services.console.logStringMessage("events: " + aMsg);
2577     }
2578   },
2580   /**
2581    * Return true if logging feature is enabled.
2582    */
2583   hasFeature: function logger_hasFeature(aFeature) {
2584     var startIdx = gA11yEventDumpFeature.indexOf(aFeature);
2585     if (startIdx == -1) {
2586       return false;
2587     }
2589     var endIdx = startIdx + aFeature.length;
2590     return (
2591       endIdx == gA11yEventDumpFeature.length ||
2592       gA11yEventDumpFeature[endIdx] == ";"
2593     );
2594   },
2597 // //////////////////////////////////////////////////////////////////////////////
2598 // Sequence
2601  * Base class of sequence item.
2602  */
2603 function sequenceItem(aProcessor, aEventType, aTarget, aItemID) {
2604   // private
2606   this.startProcess = function sequenceItem_startProcess() {
2607     this.queue.invoke();
2608   };
2610   this.queue = new eventQueue();
2611   this.queue.onFinish = function () {
2612     aProcessor.onProcessed();
2613     return DO_NOT_FINISH_TEST;
2614   };
2616   var invoker = {
2617     invoke: function invoker_invoke() {
2618       return aProcessor.process();
2619     },
2620     getID: function invoker_getID() {
2621       return aItemID;
2622     },
2623     eventSeq: [new invokerChecker(aEventType, aTarget)],
2624   };
2626   this.queue.push(invoker);
2629 // //////////////////////////////////////////////////////////////////////////////
2630 // Event queue invokers
2633  * Invoker base class for prepare an action.
2634  */
2635 function synthAction(aNodeOrID, aEventsObj) {
2636   this.DOMNode = getNode(aNodeOrID);
2638   if (aEventsObj) {
2639     var scenarios = null;
2640     if (aEventsObj instanceof Array) {
2641       if (aEventsObj[0] instanceof Array) {
2642         scenarios = aEventsObj;
2643       }
2644       // scenarios
2645       else {
2646         scenarios = [aEventsObj];
2647       } // event sequance
2648     } else {
2649       scenarios = [[aEventsObj]]; // a single checker object
2650     }
2652     for (var i = 0; i < scenarios.length; i++) {
2653       defineScenario(this, scenarios[i]);
2654     }
2655   }
2657   this.getID = function synthAction_getID() {
2658     return prettyName(aNodeOrID) + " action";
2659   };