tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }