summaryrefslogtreecommitdiffstats
path: root/accessible/tests/mochitest/events.js
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/tests/mochitest/events.js')
-rw-r--r--accessible/tests/mochitest/events.js2660
1 files changed, 2660 insertions, 0 deletions
diff --git a/accessible/tests/mochitest/events.js b/accessible/tests/mochitest/events.js
new file mode 100644
index 0000000000..a6c216e01d
--- /dev/null
+++ b/accessible/tests/mochitest/events.js
@@ -0,0 +1,2660 @@
+/* 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,
+ aLogMsg
+ ) {
+ 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, aData) {
+ 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";
+ };
+}