/* import-globals-from common.js */
/* import-globals-from states.js */
/* import-globals-from text.js */

// XXX Bug 1425371 - enable no-redeclare and fix the issues with the tests.
/* eslint-disable no-redeclare */

// //////////////////////////////////////////////////////////////////////////////
// Constants

const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT;
const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT;
const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE;
const EVENT_DOCUMENT_LOAD_COMPLETE =
  nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE;
const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD;
const EVENT_DOCUMENT_LOAD_STOPPED =
  nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED;
const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE;
const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS;
const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE;
const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START;
const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END;
const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START;
const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END;
const EVENT_OBJECT_ATTRIBUTE_CHANGED =
  nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED;
const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER;
const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START;
const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION;
const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD;
const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE;
const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN;
const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW;
const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE;
const EVENT_TEXT_ATTRIBUTE_CHANGED =
  nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED;
const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED;
const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED;
const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED;
const EVENT_TEXT_SELECTION_CHANGED =
  nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED;
const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE;
const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE;
const EVENT_VIRTUALCURSOR_CHANGED =
  nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED;

const kNotFromUserInput = 0;
const kFromUserInput = 1;

// //////////////////////////////////////////////////////////////////////////////
// General

/**
 * Set up this variable to dump events into DOM.
 */
var gA11yEventDumpID = "";

/**
 * Set up this variable to dump event processing into console.
 */
var gA11yEventDumpToConsole = false;

/**
 * Set up this variable to dump event processing into error console.
 */
var gA11yEventDumpToAppConsole = false;

/**
 * Semicolon separated set of logging features.
 */
var gA11yEventDumpFeature = "";

/**
 * Function to detect HTML elements when given a node.
 */
function isHTMLElement(aNode) {
  return (
    aNode.nodeType == aNode.ELEMENT_NODE &&
    aNode.namespaceURI == "http://www.w3.org/1999/xhtml"
  );
}

function isXULElement(aNode) {
  return (
    aNode.nodeType == aNode.ELEMENT_NODE &&
    aNode.namespaceURI ==
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  );
}

/**
 * Executes the function when requested event is handled.
 *
 * @param aEventType  [in] event type
 * @param aTarget     [in] event target
 * @param aFunc       [in] function to call when event is handled
 * @param aContext    [in, optional] object in which context the function is
 *                    called
 * @param aArg1       [in, optional] argument passed into the function
 * @param aArg2       [in, optional] argument passed into the function
 */
function waitForEvent(
  aEventType,
  aTargetOrFunc,
  aFunc,
  aContext,
  aArg1,
  aArg2
) {
  var handler = {
    handleEvent: function handleEvent(aEvent) {
      var target = aTargetOrFunc;
      if (typeof aTargetOrFunc == "function") {
        target = aTargetOrFunc.call();
      }

      if (target) {
        if (target instanceof nsIAccessible && target != aEvent.accessible) {
          return;
        }

        if (Node.isInstance(target) && target != aEvent.DOMNode) {
          return;
        }
      }

      unregisterA11yEventListener(aEventType, this);

      window.setTimeout(function () {
        aFunc.call(aContext, aArg1, aArg2);
      }, 0);
    },
  };

  registerA11yEventListener(aEventType, handler);
}

/**
 * Generate mouse move over image map what creates image map accessible (async).
 * See waitForImageMap() function.
 */
function waveOverImageMap(aImageMapID) {
  var imageMapNode = getNode(aImageMapID);
  synthesizeMouse(
    imageMapNode,
    10,
    10,
    { type: "mousemove" },
    imageMapNode.ownerGlobal
  );
}

/**
 * Call the given function when the tree of the given image map is built.
 */
function waitForImageMap(aImageMapID, aTestFunc) {
  waveOverImageMap(aImageMapID);

  var imageMapAcc = getAccessible(aImageMapID);
  if (imageMapAcc.firstChild) {
    aTestFunc();
    return;
  }

  waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc);
}

/**
 * Register accessibility event listener.
 *
 * @param aEventType     the accessible event type (see nsIAccessibleEvent for
 *                       available constants).
 * @param aEventHandler  event listener object, when accessible event of the
 *                       given type is handled then 'handleEvent' method of
 *                       this object is invoked with nsIAccessibleEvent object
 *                       as the first argument.
 */
function registerA11yEventListener(aEventType, aEventHandler) {
  listenA11yEvents(true);
  addA11yEventListener(aEventType, aEventHandler);
}

/**
 * Unregister accessibility event listener. Must be called for every registered
 * event listener (see registerA11yEventListener() function) when the listener
 * is not needed.
 */
function unregisterA11yEventListener(aEventType, aEventHandler) {
  removeA11yEventListener(aEventType, aEventHandler);
  listenA11yEvents(false);
}

// //////////////////////////////////////////////////////////////////////////////
// Event queue

/**
 * Return value of invoke method of invoker object. Indicates invoker was unable
 * to prepare action.
 */
const INVOKER_ACTION_FAILED = 1;

/**
 * Return value of eventQueue.onFinish. Indicates eventQueue should not finish
 * tests.
 */
const DO_NOT_FINISH_TEST = 1;

/**
 * Creates event queue for the given event type. The queue consists of invoker
 * objects, each of them generates the event of the event type. When queue is
 * started then every invoker object is asked to generate event after timeout.
 * When event is caught then current invoker object is asked to check whether
 * event was handled correctly.
 *
 * Invoker interface is:
 *
 *   var invoker = {
 *     // Generates accessible event or event sequence. If returns
 *     // INVOKER_ACTION_FAILED constant then stop tests.
 *     invoke: function(){},
 *
 *     // [optional] Invoker's check of handled event for correctness.
 *     check: function(aEvent){},
 *
 *     // [optional] Invoker's check before the next invoker is proceeded.
 *     finalCheck: function(aEvent){},
 *
 *     // [optional] Is called when event of any registered type is handled.
 *     debugCheck: function(aEvent){},
 *
 *     // [ignored if 'eventSeq' is defined] DOM node event is generated for
 *     // (used in the case when invoker expects single event).
 *     DOMNode getter: function() {},
 *
 *     // [optional] if true then event sequences are ignored (no failure if
 *     // sequences are empty). Use you need to invoke an action, do some check
 *     // after timeout and proceed a next invoker.
 *     noEventsOnAction getter: function() {},
 *
 *     // Array of checker objects defining expected events on invoker's action.
 *     //
 *     // Checker object interface:
 *     //
 *     // var checker = {
 *     //   * DOM or a11y event type. *
 *     //   type getter: function() {},
 *     //
 *     //   * DOM node or accessible. *
 *     //   target getter: function() {},
 *     //
 *     //   * DOM event phase (false - bubbling). *
 *     //   phase getter: function() {},
 *     //
 *     //   * Callback, called to match handled event. *
 *     //   match : function(aEvent) {},
 *     //
 *     //   * Callback, called when event is handled
 *     //   check: function(aEvent) {},
 *     //
 *     //   * Checker ID *
 *     //   getID: function() {},
 *     //
 *     //   * Event that don't have predefined order relative other events. *
 *     //   async getter: function() {},
 *     //
 *     //   * Event that is not expected. *
 *     //   unexpected getter: function() {},
 *     //
 *     //   * No other event of the same type is not allowed. *
 *     //   unique getter: function() {}
 *     // };
 *     eventSeq getter() {},
 *
 *     // Array of checker objects defining unexpected events on invoker's
 *     // action.
 *     unexpectedEventSeq getter() {},
 *
 *     // The ID of invoker.
 *     getID: function(){} // returns invoker ID
 *   };
 *
 *   // Used to add a possible scenario of expected/unexpected events on
 *   // invoker's action.
 *  defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq)
 *
 *
 * @param  aEventType  [in, optional] the default event type (isn't used if
 *                      invoker defines eventSeq property).
 */
function eventQueue(aEventType) {
  // public

  /**
   * Add invoker object into queue.
   */
  this.push = function eventQueue_push(aEventInvoker) {
    this.mInvokers.push(aEventInvoker);
  };

  /**
   * Start the queue processing.
   */
  this.invoke = function eventQueue_invoke() {
    listenA11yEvents(true);

    // XXX: Intermittent test_events_caretmove.html fails withouth timeout,
    // see bug 474952.
    this.processNextInvokerInTimeout(true);
  };

  /**
   * This function is called when all events in the queue were handled.
   * Override it if you need to be notified of this.
   */
  this.onFinish = function eventQueue_finish() {};

  // private

  /**
   * Process next invoker.
   */
  // eslint-disable-next-line complexity
  this.processNextInvoker = function eventQueue_processNextInvoker() {
    // Some scenario was matched, we wait on next invoker processing.
    if (this.mNextInvokerStatus == kInvokerCanceled) {
      this.setInvokerStatus(
        kInvokerNotScheduled,
        "scenario was matched, wait for next invoker activation"
      );
      return;
    }

    this.setInvokerStatus(
      kInvokerNotScheduled,
      "the next invoker is processed now"
    );

    // Finish processing of the current invoker if any.
    var testFailed = false;

    var invoker = this.getInvoker();
    if (invoker) {
      if ("finalCheck" in invoker) {
        invoker.finalCheck();
      }

      if (this.mScenarios && this.mScenarios.length) {
        var matchIdx = -1;
        for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
          var eventSeq = this.mScenarios[scnIdx];
          if (!this.areExpectedEventsLeft(eventSeq)) {
            for (var idx = 0; idx < eventSeq.length; idx++) {
              var checker = eventSeq[idx];
              if (
                (checker.unexpected && checker.wasCaught) ||
                (!checker.unexpected && checker.wasCaught != 1)
              ) {
                break;
              }
            }

            // Ok, we have matched scenario. Report it was completed ok. In
            // case of empty scenario guess it was matched but if later we
            // find out that non empty scenario was matched then it will be
            // a final match.
            if (idx == eventSeq.length) {
              if (
                matchIdx != -1 &&
                !!eventSeq.length &&
                this.mScenarios[matchIdx].length
              ) {
                ok(
                  false,
                  "We have a matched scenario at index " +
                    matchIdx +
                    " already."
                );
              }

              if (matchIdx == -1 || eventSeq.length) {
                matchIdx = scnIdx;
              }

              // Report everything is ok.
              for (var idx = 0; idx < eventSeq.length; idx++) {
                var checker = eventSeq[idx];

                var typeStr = eventQueue.getEventTypeAsString(checker);
                var msg =
                  "Test with ID = '" + this.getEventID(checker) + "' succeed. ";

                if (checker.unexpected) {
                  ok(true, msg + `There's no unexpected '${typeStr}' event.`);
                } else if (checker.todo) {
                  todo(false, `Todo event '${typeStr}' was caught`);
                } else {
                  ok(true, `${msg} Event '${typeStr}' was handled.`);
                }
              }
            }
          }
        }

        // We don't have completely matched scenario. Report each failure/success
        // for every scenario.
        if (matchIdx == -1) {
          testFailed = true;
          for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
            var eventSeq = this.mScenarios[scnIdx];
            for (var idx = 0; idx < eventSeq.length; idx++) {
              var checker = eventSeq[idx];

              var typeStr = eventQueue.getEventTypeAsString(checker);
              var msg =
                "Scenario #" +
                scnIdx +
                " of test with ID = '" +
                this.getEventID(checker) +
                "' failed. ";

              if (checker.wasCaught > 1) {
                ok(false, msg + "Dupe " + typeStr + " event.");
              }

              if (checker.unexpected) {
                if (checker.wasCaught) {
                  ok(false, msg + "There's unexpected " + typeStr + " event.");
                }
              } else if (!checker.wasCaught) {
                var rf = checker.todo ? todo : ok;
                rf(false, `${msg} '${typeStr} event is missed.`);
              }
            }
          }
        }
      }
    }

    this.clearEventHandler();

    // Check if need to stop the test.
    if (testFailed || this.mIndex == this.mInvokers.length - 1) {
      listenA11yEvents(false);

      var res = this.onFinish();
      if (res != DO_NOT_FINISH_TEST) {
        SimpleTest.executeSoon(SimpleTest.finish);
      }

      return;
    }

    // Start processing of next invoker.
    invoker = this.getNextInvoker();

    // Set up event listeners. Process a next invoker if no events were added.
    if (!this.setEventHandler(invoker)) {
      this.processNextInvoker();
      return;
    }

    if (gLogger.isEnabled()) {
      gLogger.logToConsole("Event queue: \n  invoke: " + invoker.getID());
      gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true);
    }

    var infoText = "Invoke the '" + invoker.getID() + "' test { ";
    var scnCount = this.mScenarios ? this.mScenarios.length : 0;
    for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) {
      infoText += "scenario #" + scnIdx + ": ";
      var eventSeq = this.mScenarios[scnIdx];
      for (var idx = 0; idx < eventSeq.length; idx++) {
        infoText += eventSeq[idx].unexpected
          ? "un"
          : "" +
            "expected '" +
            eventQueue.getEventTypeAsString(eventSeq[idx]) +
            "' event; ";
      }
    }
    infoText += " }";
    info(infoText);

    if (invoker.invoke() == INVOKER_ACTION_FAILED) {
      // Invoker failed to prepare action, fail and finish tests.
      this.processNextInvoker();
      return;
    }

    if (this.hasUnexpectedEventsScenario()) {
      this.processNextInvokerInTimeout(true);
    }
  };

  this.processNextInvokerInTimeout =
    function eventQueue_processNextInvokerInTimeout(aUncondProcess) {
      this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout");

      // No need to wait extra timeout when a) we know we don't need to do that
      // and b) there's no any single unexpected event.
      if (!aUncondProcess && this.areAllEventsExpected()) {
        // We need delay to avoid events coalesce from different invokers.
        var queue = this;
        SimpleTest.executeSoon(function () {
          queue.processNextInvoker();
        });
        return;
      }

      // Check in timeout invoker didn't fire registered events.
      window.setTimeout(
        function (aQueue) {
          aQueue.processNextInvoker();
        },
        300,
        this
      );
    };

  /**
   * Handle events for the current invoker.
   */
  // eslint-disable-next-line complexity
  this.handleEvent = function eventQueue_handleEvent(aEvent) {
    var invoker = this.getInvoker();
    if (!invoker) {
      // skip events before test was started
      return;
    }

    if (!this.mScenarios) {
      // Bad invoker object, error will be reported before processing of next
      // invoker in the queue.
      this.processNextInvoker();
      return;
    }

    if ("debugCheck" in invoker) {
      invoker.debugCheck(aEvent);
    }

    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var eventSeq = this.mScenarios[scnIdx];
      for (var idx = 0; idx < eventSeq.length; idx++) {
        var checker = eventSeq[idx];

        // Search through handled expected events to report error if one of them
        // is handled for a second time.
        if (
          !checker.unexpected &&
          checker.wasCaught > 0 &&
          eventQueue.isSameEvent(checker, aEvent)
        ) {
          checker.wasCaught++;
          continue;
        }

        // Search through unexpected events, any match results in error report
        // after this invoker processing (in case of matched scenario only).
        if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) {
          checker.wasCaught++;
          continue;
        }

        // Report an error if we handled not expected event of unique type
        // (i.e. event types are matched, targets differs).
        if (
          !checker.unexpected &&
          checker.unique &&
          eventQueue.compareEventTypes(checker, aEvent)
        ) {
          var isExpected = false;
          for (var jdx = 0; jdx < eventSeq.length; jdx++) {
            isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent);
            if (isExpected) {
              break;
            }
          }

          if (!isExpected) {
            ok(
              false,
              "Unique type " +
                eventQueue.getEventTypeAsString(checker) +
                " event was handled."
            );
          }
        }
      }
    }

    var hasMatchedCheckers = false;
    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var eventSeq = this.mScenarios[scnIdx];

      // Check if handled event matches expected sync event.
      var nextChecker = this.getNextExpectedEvent(eventSeq);
      if (nextChecker) {
        if (eventQueue.compareEvents(nextChecker, aEvent)) {
          this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx);
          hasMatchedCheckers = true;
          continue;
        }
      }

      // Check if handled event matches any expected async events.
      var haveUnmatchedAsync = false;
      for (idx = 0; idx < eventSeq.length; idx++) {
        if (eventSeq[idx] instanceof orderChecker && haveUnmatchedAsync) {
          break;
        }

        if (!eventSeq[idx].wasCaught) {
          haveUnmatchedAsync = true;
        }

        if (!eventSeq[idx].unexpected && eventSeq[idx].async) {
          if (eventQueue.compareEvents(eventSeq[idx], aEvent)) {
            this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx);
            hasMatchedCheckers = true;
            break;
          }
        }
      }
    }

    if (hasMatchedCheckers) {
      var invoker = this.getInvoker();
      if ("check" in invoker) {
        invoker.check(aEvent);
      }
    }

    for (idx = 0; idx < eventSeq.length; idx++) {
      if (!eventSeq[idx].wasCaught) {
        if (eventSeq[idx] instanceof orderChecker) {
          eventSeq[idx].wasCaught++;
        } else {
          break;
        }
      }
    }

    // If we don't have more events to wait then schedule next invoker.
    if (this.hasMatchedScenario()) {
      if (this.mNextInvokerStatus == kInvokerNotScheduled) {
        this.processNextInvokerInTimeout();
      } else if (this.mNextInvokerStatus == kInvokerCanceled) {
        this.setInvokerStatus(
          kInvokerPending,
          "Full match. Void the cancelation of next invoker processing"
        );
      }
      return;
    }

    // If we have scheduled a next invoker then cancel in case of match.
    if (this.mNextInvokerStatus == kInvokerPending && hasMatchedCheckers) {
      this.setInvokerStatus(
        kInvokerCanceled,
        "Cancel the scheduled invoker in case of match"
      );
    }
  };

  // Helpers
  this.processMatchedChecker = function eventQueue_function(
    aEvent,
    aMatchedChecker,
    aScenarioIdx,
    aEventIdx
  ) {
    aMatchedChecker.wasCaught++;

    if ("check" in aMatchedChecker) {
      aMatchedChecker.check(aEvent);
    }

    eventQueue.logEvent(
      aEvent,
      aMatchedChecker,
      aScenarioIdx,
      aEventIdx,
      this.areExpectedEventsLeft(),
      this.mNextInvokerStatus
    );
  };

  this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent(
    aEventSeq
  ) {
    if (!("idx" in aEventSeq)) {
      aEventSeq.idx = 0;
    }

    while (
      aEventSeq.idx < aEventSeq.length &&
      (aEventSeq[aEventSeq.idx].unexpected ||
        aEventSeq[aEventSeq.idx].todo ||
        aEventSeq[aEventSeq.idx].async ||
        aEventSeq[aEventSeq.idx] instanceof orderChecker ||
        aEventSeq[aEventSeq.idx].wasCaught > 0)
    ) {
      aEventSeq.idx++;
    }

    return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null;
  };

  this.areExpectedEventsLeft = function eventQueue_areExpectedEventsLeft(
    aScenario
  ) {
    function scenarioHasUnhandledExpectedEvent(aEventSeq) {
      // Check if we have unhandled async (can be anywhere in the sequance) or
      // sync expcected events yet.
      for (var idx = 0; idx < aEventSeq.length; idx++) {
        if (
          !aEventSeq[idx].unexpected &&
          !aEventSeq[idx].todo &&
          !aEventSeq[idx].wasCaught &&
          !(aEventSeq[idx] instanceof orderChecker)
        ) {
          return true;
        }
      }

      return false;
    }

    if (aScenario) {
      return scenarioHasUnhandledExpectedEvent(aScenario);
    }

    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var eventSeq = this.mScenarios[scnIdx];
      if (scenarioHasUnhandledExpectedEvent(eventSeq)) {
        return true;
      }
    }
    return false;
  };

  this.areAllEventsExpected = function eventQueue_areAllEventsExpected() {
    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var eventSeq = this.mScenarios[scnIdx];
      for (var idx = 0; idx < eventSeq.length; idx++) {
        if (eventSeq[idx].unexpected || eventSeq[idx].todo) {
          return false;
        }
      }
    }

    return true;
  };

  this.isUnexpectedEventScenario =
    function eventQueue_isUnexpectedEventsScenario(aScenario) {
      for (var idx = 0; idx < aScenario.length; idx++) {
        if (!aScenario[idx].unexpected && !aScenario[idx].todo) {
          break;
        }
      }

      return idx == aScenario.length;
    };

  this.hasUnexpectedEventsScenario =
    function eventQueue_hasUnexpectedEventsScenario() {
      if (this.getInvoker().noEventsOnAction) {
        return true;
      }

      for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
        if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) {
          return true;
        }
      }

      return false;
    };

  this.hasMatchedScenario = function eventQueue_hasMatchedScenario() {
    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var scn = this.mScenarios[scnIdx];
      if (
        !this.isUnexpectedEventScenario(scn) &&
        !this.areExpectedEventsLeft(scn)
      ) {
        return true;
      }
    }
    return false;
  };

  this.getInvoker = function eventQueue_getInvoker() {
    return this.mInvokers[this.mIndex];
  };

  this.getNextInvoker = function eventQueue_getNextInvoker() {
    return this.mInvokers[++this.mIndex];
  };

  this.setEventHandler = function eventQueue_setEventHandler(aInvoker) {
    if (!("scenarios" in aInvoker) || !aInvoker.scenarios.length) {
      var eventSeq = aInvoker.eventSeq;
      var unexpectedEventSeq = aInvoker.unexpectedEventSeq;
      if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) {
        eventSeq = [new invokerChecker(this.mDefEventType, aInvoker.DOMNode)];
      }

      if (eventSeq || unexpectedEventSeq) {
        defineScenario(aInvoker, eventSeq, unexpectedEventSeq);
      }
    }

    if (aInvoker.noEventsOnAction) {
      return true;
    }

    this.mScenarios = aInvoker.scenarios;
    if (!this.mScenarios || !this.mScenarios.length) {
      ok(false, "Broken invoker '" + aInvoker.getID() + "'");
      return false;
    }

    // Register event listeners.
    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var eventSeq = this.mScenarios[scnIdx];

      if (gLogger.isEnabled()) {
        var msg =
          "scenario #" +
          scnIdx +
          ", registered events number: " +
          eventSeq.length;
        gLogger.logToConsole(msg);
        gLogger.logToDOM(msg, true);
      }

      // Do not warn about empty event sequances when more than one scenario
      // was registered.
      if (this.mScenarios.length == 1 && !eventSeq.length) {
        ok(
          false,
          "Broken scenario #" +
            scnIdx +
            " of invoker '" +
            aInvoker.getID() +
            "'. No registered events"
        );
        return false;
      }

      for (var idx = 0; idx < eventSeq.length; idx++) {
        eventSeq[idx].wasCaught = 0;
      }

      for (var idx = 0; idx < eventSeq.length; idx++) {
        if (gLogger.isEnabled()) {
          var msg = "registered";
          if (eventSeq[idx].unexpected) {
            msg += " unexpected";
          }
          if (eventSeq[idx].async) {
            msg += " async";
          }

          msg +=
            ": event type: " +
            eventQueue.getEventTypeAsString(eventSeq[idx]) +
            ", target: " +
            eventQueue.getEventTargetDescr(eventSeq[idx], true);

          gLogger.logToConsole(msg);
          gLogger.logToDOM(msg, true);
        }

        var eventType = eventSeq[idx].type;
        if (typeof eventType == "string") {
          // DOM event
          var target = eventQueue.getEventTarget(eventSeq[idx]);
          if (!target) {
            ok(false, "no target for DOM event!");
            return false;
          }
          var phase = eventQueue.getEventPhase(eventSeq[idx]);
          target.addEventListener(eventType, this, phase);
        } else {
          // A11y event
          addA11yEventListener(eventType, this);
        }
      }
    }

    return true;
  };

  this.clearEventHandler = function eventQueue_clearEventHandler() {
    if (!this.mScenarios) {
      return;
    }

    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
      var eventSeq = this.mScenarios[scnIdx];
      for (var idx = 0; idx < eventSeq.length; idx++) {
        var eventType = eventSeq[idx].type;
        if (typeof eventType == "string") {
          // DOM event
          var target = eventQueue.getEventTarget(eventSeq[idx]);
          var phase = eventQueue.getEventPhase(eventSeq[idx]);
          target.removeEventListener(eventType, this, phase);
        } else {
          // A11y event
          removeA11yEventListener(eventType, this);
        }
      }
    }
    this.mScenarios = null;
  };

  this.getEventID = function eventQueue_getEventID(aChecker) {
    if ("getID" in aChecker) {
      return aChecker.getID();
    }

    var invoker = this.getInvoker();
    return invoker.getID();
  };

  this.setInvokerStatus = function eventQueue_setInvokerStatus(aStatus) {
    this.mNextInvokerStatus = aStatus;

    // Uncomment it to debug invoker processing logic.
    // gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg));
  };

  this.mDefEventType = aEventType;

  this.mInvokers = [];
  this.mIndex = -1;
  this.mScenarios = null;

  this.mNextInvokerStatus = kInvokerNotScheduled;
}

// //////////////////////////////////////////////////////////////////////////////
// eventQueue static members and constants

const kInvokerNotScheduled = 0;
const kInvokerPending = 1;
const kInvokerCanceled = 2;

eventQueue.getEventTypeAsString = function eventQueue_getEventTypeAsString(
  aEventOrChecker
) {
  if (Event.isInstance(aEventOrChecker)) {
    return aEventOrChecker.type;
  }

  if (aEventOrChecker instanceof nsIAccessibleEvent) {
    return eventTypeToString(aEventOrChecker.eventType);
  }

  return typeof aEventOrChecker.type == "string"
    ? aEventOrChecker.type
    : eventTypeToString(aEventOrChecker.type);
};

eventQueue.getEventTargetDescr = function eventQueue_getEventTargetDescr(
  aEventOrChecker,
  aDontForceTarget
) {
  if (Event.isInstance(aEventOrChecker)) {
    return prettyName(aEventOrChecker.originalTarget);
  }

  // XXXbz this block doesn't seem to be reachable...
  if (Event.isInstance(aEventOrChecker)) {
    return prettyName(aEventOrChecker.accessible);
  }

  var descr = aEventOrChecker.targetDescr;
  if (descr) {
    return descr;
  }

  if (aDontForceTarget) {
    return "no target description";
  }

  var target = "target" in aEventOrChecker ? aEventOrChecker.target : null;
  return prettyName(target);
};

eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) {
  return "phase" in aChecker ? aChecker.phase : true;
};

eventQueue.getEventTarget = function eventQueue_getEventTarget(aChecker) {
  if ("eventTarget" in aChecker) {
    switch (aChecker.eventTarget) {
      case "element":
        return aChecker.target;
      case "document":
      default:
        return aChecker.target.ownerDocument;
    }
  }
  return aChecker.target.ownerDocument;
};

eventQueue.compareEventTypes = function eventQueue_compareEventTypes(
  aChecker,
  aEvent
) {
  var eventType = Event.isInstance(aEvent) ? aEvent.type : aEvent.eventType;
  return aChecker.type == eventType;
};

eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) {
  if (!eventQueue.compareEventTypes(aChecker, aEvent)) {
    return false;
  }

  // If checker provides "match" function then allow the checker to decide
  // whether event is matched.
  if ("match" in aChecker) {
    return aChecker.match(aEvent);
  }

  var target1 = aChecker.target;
  if (target1 instanceof nsIAccessible) {
    var target2 = Event.isInstance(aEvent)
      ? getAccessible(aEvent.target)
      : aEvent.accessible;

    return target1 == target2;
  }

  // If original target isn't suitable then extend interface to support target
  // (original target is used in test_elm_media.html).
  var target2 = Event.isInstance(aEvent)
    ? aEvent.originalTarget
    : aEvent.DOMNode;
  return target1 == target2;
};

eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) {
  // We don't have stored info about handled event other than its type and
  // target, thus we should filter text change and state change events since
  // they may occur on the same element because of complex changes.
  return (
    this.compareEvents(aChecker, aEvent) &&
    !(aEvent instanceof nsIAccessibleTextChangeEvent) &&
    !(aEvent instanceof nsIAccessibleStateChangeEvent)
  );
};

eventQueue.invokerStatusToMsg = function eventQueue_invokerStatusToMsg(
  aInvokerStatus,
  aMsg
) {
  var msg = "invoker status: ";
  switch (aInvokerStatus) {
    case kInvokerNotScheduled:
      msg += "not scheduled";
      break;
    case kInvokerPending:
      msg += "pending";
      break;
    case kInvokerCanceled:
      msg += "canceled";
      break;
  }

  if (aMsg) {
    msg += " (" + aMsg + ")";
  }

  return msg;
};

eventQueue.logEvent = function eventQueue_logEvent(
  aOrigEvent,
  aMatchedChecker,
  aScenarioIdx,
  aEventIdx,
  aAreExpectedEventsLeft,
  aInvokerStatus
) {
  // Dump DOM event information. Skip a11y event since it is dumped by
  // gA11yEventObserver.
  if (Event.isInstance(aOrigEvent)) {
    var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent);
    info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent);
    gLogger.logToDOM(info);
  }

  var infoMsg =
    "unhandled expected events: " +
    aAreExpectedEventsLeft +
    ", " +
    eventQueue.invokerStatusToMsg(aInvokerStatus);

  var currType = eventQueue.getEventTypeAsString(aMatchedChecker);
  var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker);
  var consoleMsg =
    "*****\nScenario " +
    aScenarioIdx +
    ", event " +
    aEventIdx +
    " matched: " +
    currType +
    "\n" +
    infoMsg +
    "\n*****";
  gLogger.logToConsole(consoleMsg);

  var emphText = "matched ";
  var msg =
    "EQ event, type: " +
    currType +
    ", target: " +
    currTargetDescr +
    ", " +
    infoMsg;
  gLogger.logToDOM(msg, true, emphText);
};

// //////////////////////////////////////////////////////////////////////////////
// Action sequence

/**
 * Deal with action sequence. Used when you need to execute couple of actions
 * each after other one.
 */
function sequence() {
  /**
   * Append new sequence item.
   *
   * @param  aProcessor  [in] object implementing interface
   *                      {
   *                        // execute item action
   *                        process: function() {},
   *                        // callback, is called when item was processed
   *                        onProcessed: function() {}
   *                      };
   * @param  aEventType  [in] event type of expected event on item action
   * @param  aTarget     [in] event target of expected event on item action
   * @param  aItemID     [in] identifier of item
   */
  this.append = function sequence_append(
    aProcessor,
    aEventType,
    aTarget,
    aItemID
  ) {
    var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID);
    this.items.push(item);
  };

  /**
   * Process next sequence item.
   */
  this.processNext = function sequence_processNext() {
    this.idx++;
    if (this.idx >= this.items.length) {
      ok(false, "End of sequence: nothing to process!");
      SimpleTest.finish();
      return;
    }

    this.items[this.idx].startProcess();
  };

  this.items = [];
  this.idx = -1;
}

// //////////////////////////////////////////////////////////////////////////////
// Event queue invokers

/**
 * Defines a scenario of expected/unexpected events. Each invoker can have
 * one or more scenarios of events. Only one scenario must be completed.
 */
function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) {
  if (!("scenarios" in aInvoker)) {
    aInvoker.scenarios = [];
  }

  // Create unified event sequence concatenating expected and unexpected
  // events.
  if (!aEventSeq) {
    aEventSeq = [];
  }

  for (var idx = 0; idx < aEventSeq.length; idx++) {
    aEventSeq[idx].unexpected |= false;
    aEventSeq[idx].async |= false;
  }

  if (aUnexpectedEventSeq) {
    for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) {
      aUnexpectedEventSeq[idx].unexpected = true;
      aUnexpectedEventSeq[idx].async = false;
    }

    aEventSeq = aEventSeq.concat(aUnexpectedEventSeq);
  }

  aInvoker.scenarios.push(aEventSeq);
}

/**
 * Invokers defined below take a checker object (or array of checker objects).
 * An invoker listens for default event type registered in event queue object
 * until its checker is provided.
 *
 * Note, checker object or array of checker objects is optional.
 */

/**
 * Click invoker.
 */
function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) {
  this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);

  this.invoke = function synthClick_invoke() {
    var targetNode = this.DOMNode;
    if (targetNode.nodeType == targetNode.DOCUMENT_NODE) {
      targetNode = this.DOMNode.body
        ? this.DOMNode.body
        : this.DOMNode.documentElement;
    }

    // Scroll the node into view, otherwise synth click may fail.
    if (isHTMLElement(targetNode)) {
      targetNode.scrollIntoView(true);
    } else if (isXULElement(targetNode)) {
      var targetAcc = getAccessible(targetNode);
      targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE);
    }

    var x = 1,
      y = 1;
    if (aArgs && "where" in aArgs) {
      if (aArgs.where == "right") {
        if (isHTMLElement(targetNode)) {
          x = targetNode.offsetWidth - 1;
        } else if (isXULElement(targetNode)) {
          x = targetNode.getBoundingClientRect().width - 1;
        }
      } else if (aArgs.where == "center") {
        if (isHTMLElement(targetNode)) {
          x = targetNode.offsetWidth / 2;
          y = targetNode.offsetHeight / 2;
        } else if (isXULElement(targetNode)) {
          x = targetNode.getBoundingClientRect().width / 2;
          y = targetNode.getBoundingClientRect().height / 2;
        }
      }
    }
    synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {});
  };

  this.finalCheck = function synthClick_finalCheck() {
    // Scroll top window back.
    window.top.scrollTo(0, 0);
  };

  this.getID = function synthClick_getID() {
    return prettyName(aNodeOrID) + " click";
  };
}

/**
 * Scrolls the node into view.
 */
function scrollIntoView(aNodeOrID, aCheckerOrEventSeq) {
  this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);

  this.invoke = function scrollIntoView_invoke() {
    var targetNode = this.DOMNode;
    if (isHTMLElement(targetNode)) {
      targetNode.scrollIntoView(true);
    } else if (isXULElement(targetNode)) {
      var targetAcc = getAccessible(targetNode);
      targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE);
    }
  };

  this.getID = function scrollIntoView_getID() {
    return prettyName(aNodeOrID) + " scrollIntoView";
  };
}

/**
 * Mouse move invoker.
 */
function synthMouseMove(aID, aCheckerOrEventSeq) {
  this.__proto__ = new synthAction(aID, aCheckerOrEventSeq);

  this.invoke = function synthMouseMove_invoke() {
    synthesizeMouse(this.DOMNode, 5, 5, { type: "mousemove" });
    synthesizeMouse(this.DOMNode, 6, 6, { type: "mousemove" });
  };

  this.getID = function synthMouseMove_getID() {
    return prettyName(aID) + " mouse move";
  };
}

/**
 * General key press invoker.
 */
function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) {
  this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);

  this.invoke = function synthKey_invoke() {
    synthesizeKey(this.mKey, this.mArgs, this.mWindow);
  };

  this.getID = function synthKey_getID() {
    var key = this.mKey;
    switch (this.mKey) {
      case "VK_TAB":
        key = "tab";
        break;
      case "VK_DOWN":
        key = "down";
        break;
      case "VK_UP":
        key = "up";
        break;
      case "VK_LEFT":
        key = "left";
        break;
      case "VK_RIGHT":
        key = "right";
        break;
      case "VK_HOME":
        key = "home";
        break;
      case "VK_END":
        key = "end";
        break;
      case "VK_ESCAPE":
        key = "escape";
        break;
      case "VK_RETURN":
        key = "enter";
        break;
    }
    if (aArgs) {
      if (aArgs.shiftKey) {
        key += " shift";
      }
      if (aArgs.ctrlKey) {
        key += " ctrl";
      }
      if (aArgs.altKey) {
        key += " alt";
      }
    }
    return prettyName(aNodeOrID) + " '" + key + " ' key";
  };

  this.mKey = aKey;
  this.mArgs = aArgs ? aArgs : {};
  this.mWindow = aArgs ? aArgs.window : null;
}

/**
 * Tab key invoker.
 */
function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) {
  this.__proto__ = new synthKey(
    aNodeOrID,
    "VK_TAB",
    { shiftKey: false, window: aWindow },
    aCheckerOrEventSeq
  );
}

/**
 * Shift tab key invoker.
 */
function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) {
  this.__proto__ = new synthKey(
    aNodeOrID,
    "VK_TAB",
    { shiftKey: true },
    aCheckerOrEventSeq
  );
}

/**
 * Escape key invoker.
 */
function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) {
  this.__proto__ = new synthKey(
    aNodeOrID,
    "VK_ESCAPE",
    null,
    aCheckerOrEventSeq
  );
}

/**
 * Down arrow key invoker.
 */
function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) {
  this.__proto__ = new synthKey(
    aNodeOrID,
    "VK_DOWN",
    aArgs,
    aCheckerOrEventSeq
  );
}

/**
 * Up arrow key invoker.
 */
function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) {
  this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, aCheckerOrEventSeq);
}

/**
 * Left arrow key invoker.
 */
function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) {
  this.__proto__ = new synthKey(
    aNodeOrID,
    "VK_LEFT",
    aArgs,
    aCheckerOrEventSeq
  );
}

/**
 * Right arrow key invoker.
 */
function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) {
  this.__proto__ = new synthKey(
    aNodeOrID,
    "VK_RIGHT",
    aArgs,
    aCheckerOrEventSeq
  );
}

/**
 * Home key invoker.
 */
function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) {
  this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq);
}

/**
 * End key invoker.
 */
function synthEndKey(aNodeOrID, aCheckerOrEventSeq) {
  this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq);
}

/**
 * Enter key invoker
 */
function synthEnterKey(aID, aCheckerOrEventSeq) {
  this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq);
}

/**
 * Synth alt + down arrow to open combobox.
 */
function synthOpenComboboxKey(aID, aCheckerOrEventSeq) {
  this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true });

  this.getID = function synthOpenComboboxKey_getID() {
    return "open combobox (alt + down arrow) " + prettyName(aID);
  };
}

/**
 * Focus invoker.
 */
function synthFocus(aNodeOrID, aCheckerOrEventSeq) {
  var checkerOfEventSeq = aCheckerOrEventSeq
    ? aCheckerOrEventSeq
    : new focusChecker(aNodeOrID);
  this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq);

  this.invoke = function synthFocus_invoke() {
    if (this.DOMNode.editor) {
      this.DOMNode.selectionStart = this.DOMNode.selectionEnd =
        this.DOMNode.value.length;
    }
    this.DOMNode.focus();
  };

  this.getID = function synthFocus_getID() {
    return prettyName(aNodeOrID) + " focus";
  };
}

/**
 * Focus invoker. Focus the HTML body of content document of iframe.
 */
function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) {
  var frameDoc = getNode(aNodeOrID).contentDocument;
  var checkerOrEventSeq = aCheckerOrEventSeq
    ? aCheckerOrEventSeq
    : new focusChecker(frameDoc);
  this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq);

  this.invoke = function synthFocus_invoke() {
    this.DOMNode.body.focus();
  };

  this.getID = function synthFocus_getID() {
    return prettyName(aNodeOrID) + " frame document focus";
  };
}

/**
 * Change the current item when the widget doesn't have a focus.
 */
function changeCurrentItem(aID, aItemID) {
  this.eventSeq = [new nofocusChecker()];

  this.invoke = function changeCurrentItem_invoke() {
    var controlNode = getNode(aID);
    var itemNode = getNode(aItemID);

    // HTML
    if (controlNode.localName == "input") {
      if (controlNode.checked) {
        this.reportError();
      }

      controlNode.checked = true;
      return;
    }

    if (controlNode.localName == "select") {
      if (controlNode.selectedIndex == itemNode.index) {
        this.reportError();
      }

      controlNode.selectedIndex = itemNode.index;
      return;
    }

    // XUL
    if (controlNode.localName == "tree") {
      if (controlNode.currentIndex == aItemID) {
        this.reportError();
      }

      controlNode.currentIndex = aItemID;
      return;
    }

    if (controlNode.localName == "menulist") {
      if (controlNode.selectedItem == itemNode) {
        this.reportError();
      }

      controlNode.selectedItem = itemNode;
      return;
    }

    if (controlNode.currentItem == itemNode) {
      ok(
        false,
        "Error in test: proposed current item is already current" +
          prettyName(aID)
      );
    }

    controlNode.currentItem = itemNode;
  };

  this.getID = function changeCurrentItem_getID() {
    return "current item change for " + prettyName(aID);
  };

  this.reportError = function changeCurrentItem_reportError() {
    ok(
      false,
      "Error in test: proposed current item '" +
        aItemID +
        "' is already current"
    );
  };
}

/**
 * Toggle top menu invoker.
 */
function toggleTopMenu(aID, aCheckerOrEventSeq) {
  this.__proto__ = new synthKey(aID, "VK_ALT", null, aCheckerOrEventSeq);

  this.getID = function toggleTopMenu_getID() {
    return "toggle top menu on " + prettyName(aID);
  };
}

/**
 * Context menu invoker.
 */
function synthContextMenu(aID, aCheckerOrEventSeq) {
  this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, {
    button: 0,
    type: "contextmenu",
  });

  this.getID = function synthContextMenu_getID() {
    return "context menu on " + prettyName(aID);
  };
}

/**
 * Open combobox, autocomplete and etc popup, check expandable states.
 */
function openCombobox(aComboboxID) {
  this.eventSeq = [
    new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID),
  ];

  this.invoke = function openCombobox_invoke() {
    getNode(aComboboxID).focus();
    synthesizeKey("VK_DOWN", { altKey: true });
  };

  this.getID = function openCombobox_getID() {
    return "open combobox " + prettyName(aComboboxID);
  };
}

/**
 * Close combobox, autocomplete and etc popup, check expandable states.
 */
function closeCombobox(aComboboxID) {
  this.eventSeq = [
    new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID),
  ];

  this.invoke = function closeCombobox_invoke() {
    synthesizeKey("KEY_Escape");
  };

  this.getID = function closeCombobox_getID() {
    return "close combobox " + prettyName(aComboboxID);
  };
}

/**
 * Select all invoker.
 */
function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) {
  this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);

  this.invoke = function synthSelectAll_invoke() {
    if (ChromeUtils.getClassName(this.DOMNode) === "HTMLInputElement") {
      this.DOMNode.select();
    } else {
      window.getSelection().selectAllChildren(this.DOMNode);
    }
  };

  this.getID = function synthSelectAll_getID() {
    return aNodeOrID + " selectall";
  };
}

/**
 * Move the caret to the end of line.
 */
function moveToLineEnd(aID, aCaretOffset) {
  if (MAC) {
    this.__proto__ = new synthKey(
      aID,
      "VK_RIGHT",
      { metaKey: true },
      new caretMoveChecker(aCaretOffset, true, aID)
    );
  } else {
    this.__proto__ = new synthEndKey(
      aID,
      new caretMoveChecker(aCaretOffset, true, aID)
    );
  }

  this.getID = function moveToLineEnd_getID() {
    return "move to line end in " + prettyName(aID);
  };
}

/**
 * Move the caret to the end of previous line if any.
 */
function moveToPrevLineEnd(aID, aCaretOffset) {
  this.__proto__ = new synthAction(
    aID,
    new caretMoveChecker(aCaretOffset, true, aID)
  );

  this.invoke = function moveToPrevLineEnd_invoke() {
    synthesizeKey("KEY_ArrowUp");

    if (MAC) {
      synthesizeKey("Key_ArrowRight", { metaKey: true });
    } else {
      synthesizeKey("KEY_End");
    }
  };

  this.getID = function moveToPrevLineEnd_getID() {
    return "move to previous line end in " + prettyName(aID);
  };
}

/**
 * Move the caret to begining of the line.
 */
function moveToLineStart(aID, aCaretOffset) {
  if (MAC) {
    this.__proto__ = new synthKey(
      aID,
      "VK_LEFT",
      { metaKey: true },
      new caretMoveChecker(aCaretOffset, true, aID)
    );
  } else {
    this.__proto__ = new synthHomeKey(
      aID,
      new caretMoveChecker(aCaretOffset, true, aID)
    );
  }

  this.getID = function moveToLineEnd_getID() {
    return "move to line start in " + prettyName(aID);
  };
}

/**
 * Move the caret to begining of the text.
 */
function moveToTextStart(aID) {
  if (MAC) {
    this.__proto__ = new synthKey(
      aID,
      "VK_UP",
      { metaKey: true },
      new caretMoveChecker(0, true, aID)
    );
  } else {
    this.__proto__ = new synthKey(
      aID,
      "VK_HOME",
      { ctrlKey: true },
      new caretMoveChecker(0, true, aID)
    );
  }

  this.getID = function moveToTextStart_getID() {
    return "move to text start in " + prettyName(aID);
  };
}

/**
 * Move the caret in text accessible.
 */
function moveCaretToDOMPoint(
  aID,
  aDOMPointNodeID,
  aDOMPointOffset,
  aExpectedOffset,
  aFocusTargetID,
  aCheckFunc
) {
  this.target = getAccessible(aID, [nsIAccessibleText]);
  this.DOMPointNode = getNode(aDOMPointNodeID);
  this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null;
  this.focusNode = this.focus ? this.focus.DOMNode : null;

  this.invoke = function moveCaretToDOMPoint_invoke() {
    if (this.focusNode) {
      this.focusNode.focus();
    }

    var selection = this.DOMPointNode.ownerGlobal.getSelection();
    var selRange = selection.getRangeAt(0);
    selRange.setStart(this.DOMPointNode, aDOMPointOffset);
    selRange.collapse(true);

    selection.removeRange(selRange);
    selection.addRange(selRange);
  };

  this.getID = function moveCaretToDOMPoint_getID() {
    return (
      "Set caret on " +
      prettyName(aID) +
      " at point: " +
      prettyName(aDOMPointNodeID) +
      " node with offset " +
      aDOMPointOffset
    );
  };

  this.finalCheck = function moveCaretToDOMPoint_finalCheck() {
    if (aCheckFunc) {
      aCheckFunc.call();
    }
  };

  this.eventSeq = [new caretMoveChecker(aExpectedOffset, true, this.target)];

  if (this.focus) {
    this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus));
  }
}

/**
 * Set caret offset in text accessible.
 */
function setCaretOffset(aID, aOffset, aFocusTargetID) {
  this.target = getAccessible(aID, [nsIAccessibleText]);
  this.offset = aOffset == -1 ? this.target.characterCount : aOffset;
  this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null;

  this.invoke = function setCaretOffset_invoke() {
    this.target.caretOffset = this.offset;
  };

  this.getID = function setCaretOffset_getID() {
    return "Set caretOffset on " + prettyName(aID) + " at " + this.offset;
  };

  this.eventSeq = [new caretMoveChecker(this.offset, true, this.target)];

  if (this.focus) {
    this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus));
  }
}

// //////////////////////////////////////////////////////////////////////////////
// Event queue checkers

/**
 * Common invoker checker (see eventSeq of eventQueue).
 */
function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) {
  this.type = aEventType;
  this.async = aIsAsync;

  this.__defineGetter__("target", invokerChecker_targetGetter);
  this.__defineSetter__("target", invokerChecker_targetSetter);

  // implementation details
  function invokerChecker_targetGetter() {
    if (typeof this.mTarget == "function") {
      return this.mTarget.call(null, this.mTargetFuncArg);
    }
    if (typeof this.mTarget == "string") {
      return getNode(this.mTarget);
    }

    return this.mTarget;
  }

  function invokerChecker_targetSetter(aValue) {
    this.mTarget = aValue;
    return this.mTarget;
  }

  this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter);

  function invokerChecker_targetDescrGetter() {
    if (typeof this.mTarget == "function") {
      return this.mTarget.name + ", arg: " + this.mTargetFuncArg;
    }

    return prettyName(this.mTarget);
  }

  this.mTarget = aTargetOrFunc;
  this.mTargetFuncArg = aTargetFuncArg;
}

/**
 * event checker that forces preceeding async events to happen before this
 * checker.
 */
function orderChecker() {
  // XXX it doesn't actually work to inherit from invokerChecker, but maybe we
  // should fix that?
  //  this.__proto__ = new invokerChecker(null, null, null, false);
}

/**
 * Generic invoker checker for todo events.
 */
function todo_invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) {
  this.__proto__ = new invokerChecker(
    aEventType,
    aTargetOrFunc,
    aTargetFuncArg,
    true
  );
  this.todo = true;
}

/**
 * Generic invoker checker for unexpected events.
 */
function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) {
  this.__proto__ = new invokerChecker(
    aEventType,
    aTargetOrFunc,
    aTargetFuncArg,
    true
  );

  this.unexpected = true;
}

/**
 * Common invoker checker for async events.
 */
function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) {
  this.__proto__ = new invokerChecker(
    aEventType,
    aTargetOrFunc,
    aTargetFuncArg,
    true
  );
}

function focusChecker(aTargetOrFunc, aTargetFuncArg) {
  this.__proto__ = new invokerChecker(
    EVENT_FOCUS,
    aTargetOrFunc,
    aTargetFuncArg,
    false
  );

  this.unique = true; // focus event must be unique for invoker action

  this.check = function focusChecker_check(aEvent) {
    testStates(aEvent.accessible, STATE_FOCUSED);
  };
}

function nofocusChecker(aID) {
  this.__proto__ = new focusChecker(aID);
  this.unexpected = true;
}

/**
 * Text inserted/removed events checker.
 * @param aFromUser  [in, optional] kNotFromUserInput or kFromUserInput
 */
function textChangeChecker(
  aID,
  aStart,
  aEnd,
  aTextOrFunc,
  aIsInserted,
  aFromUser,
  aAsync
) {
  this.target = getNode(aID);
  this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED;
  this.startOffset = aStart;
  this.endOffset = aEnd;
  this.textOrFunc = aTextOrFunc;
  this.async = aAsync;

  this.match = function stextChangeChecker_match(aEvent) {
    if (
      !(aEvent instanceof nsIAccessibleTextChangeEvent) ||
      aEvent.accessible !== getAccessible(this.target)
    ) {
      return false;
    }

    let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent);
    let modifiedText =
      typeof this.textOrFunc === "function"
        ? this.textOrFunc()
        : this.textOrFunc;
    return modifiedText === tcEvent.modifiedText;
  };

  this.check = function textChangeChecker_check(aEvent) {
    aEvent.QueryInterface(nsIAccessibleTextChangeEvent);

    var modifiedText =
      typeof this.textOrFunc == "function"
        ? this.textOrFunc()
        : this.textOrFunc;
    var modifiedTextLen =
      this.endOffset == -1 ? modifiedText.length : aEnd - aStart;

    is(
      aEvent.start,
      this.startOffset,
      "Wrong start offset for " + prettyName(aID)
    );
    is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID));
    var changeInfo = aIsInserted ? "inserted" : "removed";
    is(
      aEvent.isInserted,
      aIsInserted,
      "Text was " + changeInfo + " for " + prettyName(aID)
    );
    is(
      aEvent.modifiedText,
      modifiedText,
      "Wrong " + changeInfo + " text for " + prettyName(aID)
    );
    if (typeof aFromUser != "undefined") {
      is(
        aEvent.isFromUserInput,
        aFromUser,
        "wrong value of isFromUserInput() for " + prettyName(aID)
      );
    }
  };
}

/**
 * Caret move events checker.
 */
function caretMoveChecker(
  aCaretOffset,
  aIsSelectionCollapsed,
  aTargetOrFunc,
  aTargetFuncArg,
  aIsAsync
) {
  this.__proto__ = new invokerChecker(
    EVENT_TEXT_CARET_MOVED,
    aTargetOrFunc,
    aTargetFuncArg,
    aIsAsync
  );

  this.check = function caretMoveChecker_check(aEvent) {
    let evt = aEvent.QueryInterface(nsIAccessibleCaretMoveEvent);
    is(
      evt.caretOffset,
      aCaretOffset,
      "Wrong caret offset for " + prettyName(aEvent.accessible)
    );
    is(
      evt.isSelectionCollapsed,
      aIsSelectionCollapsed,
      "wrong collapsed value for  " + prettyName(aEvent.accessible)
    );
  };
}

function asyncCaretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) {
  this.__proto__ = new caretMoveChecker(
    aCaretOffset,
    true, // Caret is collapsed
    aTargetOrFunc,
    aTargetFuncArg,
    true
  );
}

/**
 * Text selection change checker.
 */
function textSelectionChecker(
  aID,
  aStartOffset,
  aEndOffset,
  aRangeStartContainer,
  aRangeStartOffset,
  aRangeEndContainer,
  aRangeEndOffset
) {
  this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID);

  this.check = function textSelectionChecker_check(aEvent) {
    if (aStartOffset == aEndOffset) {
      ok(true, "Collapsed selection triggered text selection change event.");
    } else {
      testTextGetSelection(aID, aStartOffset, aEndOffset, 0);

      // Test selection test range
      let selectionRanges = aEvent.QueryInterface(
        nsIAccessibleTextSelectionChangeEvent
      ).selectionRanges;
      let range = selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
      is(
        range.startContainer,
        getAccessible(aRangeStartContainer),
        "correct range start container"
      );
      is(range.startOffset, aRangeStartOffset, "correct range start offset");
      is(range.endOffset, aRangeEndOffset, "correct range end offset");
      is(
        range.endContainer,
        getAccessible(aRangeEndContainer),
        "correct range end container"
      );
    }
  };
}

/**
 * Object attribute changed checker
 */
function objAttrChangedChecker(aID, aAttr) {
  this.__proto__ = new invokerChecker(EVENT_OBJECT_ATTRIBUTE_CHANGED, aID);

  this.check = function objAttrChangedChecker_check(aEvent) {
    var event = null;
    try {
      var event = aEvent.QueryInterface(
        nsIAccessibleObjectAttributeChangedEvent
      );
    } catch (e) {
      ok(false, "Object attribute changed event was expected");
    }

    if (!event) {
      return;
    }

    is(
      event.changedAttribute,
      aAttr,
      "Wrong attribute name of the object attribute changed event."
    );
  };

  this.match = function objAttrChangedChecker_match(aEvent) {
    if (aEvent instanceof nsIAccessibleObjectAttributeChangedEvent) {
      var scEvent = aEvent.QueryInterface(
        nsIAccessibleObjectAttributeChangedEvent
      );
      return (
        aEvent.accessible == getAccessible(this.target) &&
        scEvent.changedAttribute == aAttr
      );
    }
    return false;
  };
}

/**
 * State change checker.
 */
function stateChangeChecker(
  aState,
  aIsExtraState,
  aIsEnabled,
  aTargetOrFunc,
  aTargetFuncArg,
  aIsAsync,
  aSkipCurrentStateCheck
) {
  this.__proto__ = new invokerChecker(
    EVENT_STATE_CHANGE,
    aTargetOrFunc,
    aTargetFuncArg,
    aIsAsync
  );

  this.check = function stateChangeChecker_check(aEvent) {
    var event = null;
    try {
      var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
    } catch (e) {
      ok(false, "State change event was expected");
    }

    if (!event) {
      return;
    }

    is(
      event.isExtraState,
      aIsExtraState,
      "Wrong extra state bit of the statechange event."
    );
    isState(
      event.state,
      aState,
      aIsExtraState,
      "Wrong state of the statechange event."
    );
    is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state");

    if (aSkipCurrentStateCheck) {
      todo(false, "State checking was skipped!");
      return;
    }

    var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0;
    var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0;
    var unxpdState = aIsEnabled ? 0 : aIsExtraState ? 0 : aState;
    var unxpdExtraState = aIsEnabled ? 0 : aIsExtraState ? aState : 0;
    testStates(
      event.accessible,
      state,
      extraState,
      unxpdState,
      unxpdExtraState
    );
  };

  this.match = function stateChangeChecker_match(aEvent) {
    if (aEvent instanceof nsIAccessibleStateChangeEvent) {
      var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
      return (
        aEvent.accessible == getAccessible(this.target) &&
        scEvent.state == aState
      );
    }
    return false;
  };
}

function asyncStateChangeChecker(
  aState,
  aIsExtraState,
  aIsEnabled,
  aTargetOrFunc,
  aTargetFuncArg
) {
  this.__proto__ = new stateChangeChecker(
    aState,
    aIsExtraState,
    aIsEnabled,
    aTargetOrFunc,
    aTargetFuncArg,
    true
  );
}

/**
 * Expanded state change checker.
 */
function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) {
  this.__proto__ = new invokerChecker(
    EVENT_STATE_CHANGE,
    aTargetOrFunc,
    aTargetFuncArg
  );

  this.check = function expandedStateChecker_check(aEvent) {
    var event = null;
    try {
      var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
    } catch (e) {
      ok(false, "State change event was expected");
    }

    if (!event) {
      return;
    }

    is(event.state, STATE_EXPANDED, "Wrong state of the statechange event.");
    is(
      event.isExtraState,
      false,
      "Wrong extra state bit of the statechange event."
    );
    is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state");

    testStates(event.accessible, aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED);
  };
}

// //////////////////////////////////////////////////////////////////////////////
// Event sequances (array of predefined checkers)

/**
 * Event seq for single selection change.
 */
function selChangeSeq(aUnselectedID, aSelectedID) {
  if (!aUnselectedID) {
    return [
      new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
      new invokerChecker(EVENT_SELECTION, aSelectedID),
    ];
  }

  // Return two possible scenarios: depending on widget type when selection is
  // moved the the order of items that get selected and unselected may vary.
  return [
    [
      new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID),
      new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
      new invokerChecker(EVENT_SELECTION, aSelectedID),
    ],
    [
      new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
      new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID),
      new invokerChecker(EVENT_SELECTION, aSelectedID),
    ],
  ];
}

/**
 * Event seq for item removed form the selection.
 */
function selRemoveSeq(aUnselectedID) {
  return [
    new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID),
    new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID),
  ];
}

/**
 * Event seq for item added to the selection.
 */
function selAddSeq(aSelectedID) {
  return [
    new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID),
    new invokerChecker(EVENT_SELECTION_ADD, aSelectedID),
  ];
}

// //////////////////////////////////////////////////////////////////////////////
// Private implementation details.
// //////////////////////////////////////////////////////////////////////////////

// //////////////////////////////////////////////////////////////////////////////
// General

var gA11yEventListeners = {};
var gA11yEventApplicantsCount = 0;

var gA11yEventObserver = {
  // eslint-disable-next-line complexity
  observe: function observe(aSubject, aTopic) {
    if (aTopic != "accessible-event") {
      return;
    }

    var event;
    try {
      event = aSubject.QueryInterface(nsIAccessibleEvent);
    } catch (ex) {
      // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered.
      // Remove the leftover observer, otherwise it "leaks" to all the following tests.
      Services.obs.removeObserver(this, "accessible-event");
      // Forward the exception, with added explanation.
      throw new Error(
        "[accessible/events.js, gA11yEventObserver.observe] This is expected " +
          `if a previous test has been aborted... Initial exception was: [ ${ex} ]`
      );
    }
    var listenersArray = gA11yEventListeners[event.eventType];

    var eventFromDumpArea = false;
    if (gLogger.isEnabled()) {
      // debug stuff
      eventFromDumpArea = true;

      var target = event.DOMNode;
      var dumpElm = gA11yEventDumpID
        ? document.getElementById(gA11yEventDumpID)
        : null;

      if (dumpElm) {
        var parent = target;
        while (parent && parent != dumpElm) {
          parent = parent.parentNode;
        }
      }

      if (!dumpElm || parent != dumpElm) {
        var type = eventTypeToString(event.eventType);
        var info = "Event type: " + type;

        if (event instanceof nsIAccessibleStateChangeEvent) {
          var stateStr = statesToString(
            event.isExtraState ? 0 : event.state,
            event.isExtraState ? event.state : 0
          );
          info += ", state: " + stateStr + ", is enabled: " + event.isEnabled;
        } else if (event instanceof nsIAccessibleTextChangeEvent) {
          info +=
            ", start: " +
            event.start +
            ", length: " +
            event.length +
            ", " +
            (event.isInserted ? "inserted" : "removed") +
            " text: " +
            event.modifiedText;
        }

        info += ". Target: " + prettyName(event.accessible);

        if (listenersArray) {
          info += ". Listeners count: " + listenersArray.length;
        }

        if (gLogger.hasFeature("parentchain:" + type)) {
          info += "\nParent chain:\n";
          var acc = event.accessible;
          while (acc) {
            info += "  " + prettyName(acc) + "\n";
            acc = acc.parent;
          }
        }

        eventFromDumpArea = false;
        gLogger.log(info);
      }
    }

    // Do not notify listeners if event is result of event log changes.
    if (!listenersArray || eventFromDumpArea) {
      return;
    }

    for (var index = 0; index < listenersArray.length; index++) {
      listenersArray[index].handleEvent(event);
    }
  },
};

function listenA11yEvents(aStartToListen) {
  if (aStartToListen) {
    // Add observer when adding the first applicant only.
    if (!gA11yEventApplicantsCount++) {
      Services.obs.addObserver(gA11yEventObserver, "accessible-event");
    }
  } else {
    // Remove observer when there are no more applicants only.
    // '< 0' case should not happen, but just in case: removeObserver() will throw.
    // eslint-disable-next-line no-lonely-if
    if (--gA11yEventApplicantsCount <= 0) {
      Services.obs.removeObserver(gA11yEventObserver, "accessible-event");
    }
  }
}

function addA11yEventListener(aEventType, aEventHandler) {
  if (!(aEventType in gA11yEventListeners)) {
    gA11yEventListeners[aEventType] = [];
  }

  var listenersArray = gA11yEventListeners[aEventType];
  var index = listenersArray.indexOf(aEventHandler);
  if (index == -1) {
    listenersArray.push(aEventHandler);
  }
}

function removeA11yEventListener(aEventType, aEventHandler) {
  var listenersArray = gA11yEventListeners[aEventType];
  if (!listenersArray) {
    return false;
  }

  var index = listenersArray.indexOf(aEventHandler);
  if (index == -1) {
    return false;
  }

  listenersArray.splice(index, 1);

  if (!listenersArray.length) {
    gA11yEventListeners[aEventType] = null;
    delete gA11yEventListeners[aEventType];
  }

  return true;
}

/**
 * Used to dump debug information.
 */
var gLogger = {
  /**
   * Return true if dump is enabled.
   */
  isEnabled: function debugOutput_isEnabled() {
    return (
      gA11yEventDumpID || gA11yEventDumpToConsole || gA11yEventDumpToAppConsole
    );
  },

  /**
   * Dump information into DOM and console if applicable.
   */
  log: function logger_log(aMsg) {
    this.logToConsole(aMsg);
    this.logToAppConsole(aMsg);
    this.logToDOM(aMsg);
  },

  /**
   * Log message to DOM.
   *
   * @param aMsg          [in] the primary message
   * @param aHasIndent    [in, optional] if specified the message has an indent
   * @param aPreEmphText  [in, optional] the text is colored and appended prior
   *                        primary message
   */
  logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) {
    if (gA11yEventDumpID == "") {
      return;
    }

    var dumpElm = document.getElementById(gA11yEventDumpID);
    if (!dumpElm) {
      ok(
        false,
        "No dump element '" + gA11yEventDumpID + "' within the document!"
      );
      return;
    }

    var containerTagName =
      ChromeUtils.getClassName(document) == "HTMLDocument"
        ? "div"
        : "description";

    var container = document.createElement(containerTagName);
    if (aHasIndent) {
      container.setAttribute("style", "padding-left: 10px;");
    }

    if (aPreEmphText) {
      var inlineTagName =
        ChromeUtils.getClassName(document) == "HTMLDocument"
          ? "span"
          : "description";
      var emphElm = document.createElement(inlineTagName);
      emphElm.setAttribute("style", "color: blue;");
      emphElm.textContent = aPreEmphText;

      container.appendChild(emphElm);
    }

    var textNode = document.createTextNode(aMsg);
    container.appendChild(textNode);

    dumpElm.appendChild(container);
  },

  /**
   * Log message to console.
   */
  logToConsole: function logger_logToConsole(aMsg) {
    if (gA11yEventDumpToConsole) {
      dump("\n" + aMsg + "\n");
    }
  },

  /**
   * Log message to error console.
   */
  logToAppConsole: function logger_logToAppConsole(aMsg) {
    if (gA11yEventDumpToAppConsole) {
      Services.console.logStringMessage("events: " + aMsg);
    }
  },

  /**
   * Return true if logging feature is enabled.
   */
  hasFeature: function logger_hasFeature(aFeature) {
    var startIdx = gA11yEventDumpFeature.indexOf(aFeature);
    if (startIdx == -1) {
      return false;
    }

    var endIdx = startIdx + aFeature.length;
    return (
      endIdx == gA11yEventDumpFeature.length ||
      gA11yEventDumpFeature[endIdx] == ";"
    );
  },
};

// //////////////////////////////////////////////////////////////////////////////
// Sequence

/**
 * Base class of sequence item.
 */
function sequenceItem(aProcessor, aEventType, aTarget, aItemID) {
  // private

  this.startProcess = function sequenceItem_startProcess() {
    this.queue.invoke();
  };

  this.queue = new eventQueue();
  this.queue.onFinish = function () {
    aProcessor.onProcessed();
    return DO_NOT_FINISH_TEST;
  };

  var invoker = {
    invoke: function invoker_invoke() {
      return aProcessor.process();
    },
    getID: function invoker_getID() {
      return aItemID;
    },
    eventSeq: [new invokerChecker(aEventType, aTarget)],
  };

  this.queue.push(invoker);
}

// //////////////////////////////////////////////////////////////////////////////
// Event queue invokers

/**
 * Invoker base class for prepare an action.
 */
function synthAction(aNodeOrID, aEventsObj) {
  this.DOMNode = getNode(aNodeOrID);

  if (aEventsObj) {
    var scenarios = null;
    if (aEventsObj instanceof Array) {
      if (aEventsObj[0] instanceof Array) {
        scenarios = aEventsObj;
      }
      // scenarios
      else {
        scenarios = [aEventsObj];
      } // event sequance
    } else {
      scenarios = [[aEventsObj]]; // a single checker object
    }

    for (var i = 0; i < scenarios.length; i++) {
      defineScenario(this, scenarios[i]);
    }
  }

  this.getID = function synthAction_getID() {
    return prettyName(aNodeOrID) + " action";
  };
}
