summaryrefslogtreecommitdiffstats
path: root/accessible/tests/mochitest/events
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--accessible/tests/mochitest/events.js2660
-rw-r--r--accessible/tests/mochitest/events/a11y.toml128
-rw-r--r--accessible/tests/mochitest/events/docload/a11y.toml21
-rw-r--r--accessible/tests/mochitest/events/docload/docload_wnd.html37
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_aria.html75
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_busy.html83
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_embedded.html85
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_iframe.html99
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_root.html125
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_shutdown.html142
-rw-r--r--accessible/tests/mochitest/events/focus.html10
-rw-r--r--accessible/tests/mochitest/events/scroll.html181
-rw-r--r--accessible/tests/mochitest/events/slow_image.sjs55
-rw-r--r--accessible/tests/mochitest/events/test_announcement.html61
-rw-r--r--accessible/tests/mochitest/events/test_aria_alert.html84
-rw-r--r--accessible/tests/mochitest/events/test_aria_menu.html267
-rw-r--r--accessible/tests/mochitest/events/test_aria_objattr.html68
-rw-r--r--accessible/tests/mochitest/events/test_aria_owns.html122
-rw-r--r--accessible/tests/mochitest/events/test_aria_statechange.html231
-rw-r--r--accessible/tests/mochitest/events/test_attrchange.html107
-rw-r--r--accessible/tests/mochitest/events/test_attrs.html85
-rw-r--r--accessible/tests/mochitest/events/test_bug1322593-2.html77
-rw-r--r--accessible/tests/mochitest/events/test_bug1322593.html74
-rw-r--r--accessible/tests/mochitest/events/test_caretmove.html142
-rw-r--r--accessible/tests/mochitest/events/test_coalescence.html817
-rw-r--r--accessible/tests/mochitest/events/test_contextmenu.html133
-rw-r--r--accessible/tests/mochitest/events/test_descrchange.html142
-rw-r--r--accessible/tests/mochitest/events/test_dragndrop.html106
-rw-r--r--accessible/tests/mochitest/events/test_flush.html74
-rw-r--r--accessible/tests/mochitest/events/test_focus_aria_activedescendant.html327
-rw-r--r--accessible/tests/mochitest/events/test_focus_autocomplete.html83
-rw-r--r--accessible/tests/mochitest/events/test_focus_autocomplete.xhtml507
-rw-r--r--accessible/tests/mochitest/events/test_focus_canvas.html58
-rw-r--r--accessible/tests/mochitest/events/test_focus_contextmenu.xhtml98
-rw-r--r--accessible/tests/mochitest/events/test_focus_controls.html76
-rw-r--r--accessible/tests/mochitest/events/test_focus_doc.html92
-rw-r--r--accessible/tests/mochitest/events/test_focus_general.html176
-rw-r--r--accessible/tests/mochitest/events/test_focus_general.xhtml124
-rw-r--r--accessible/tests/mochitest/events/test_focus_listcontrols.xhtml153
-rw-r--r--accessible/tests/mochitest/events/test_focus_menu.xhtml117
-rw-r--r--accessible/tests/mochitest/events/test_focus_name.html116
-rw-r--r--accessible/tests/mochitest/events/test_focus_removal.html95
-rw-r--r--accessible/tests/mochitest/events/test_focus_selects.html190
-rw-r--r--accessible/tests/mochitest/events/test_focus_tabbox.xhtml102
-rw-r--r--accessible/tests/mochitest/events/test_focus_tree.xhtml117
-rw-r--r--accessible/tests/mochitest/events/test_focusable_statechange.html122
-rw-r--r--accessible/tests/mochitest/events/test_fromUserInput.html112
-rw-r--r--accessible/tests/mochitest/events/test_label.xhtml178
-rw-r--r--accessible/tests/mochitest/events/test_menu.xhtml200
-rw-r--r--accessible/tests/mochitest/events/test_mutation.html580
-rw-r--r--accessible/tests/mochitest/events/test_namechange.html185
-rw-r--r--accessible/tests/mochitest/events/test_namechange.xhtml90
-rw-r--r--accessible/tests/mochitest/events/test_scroll.xhtml107
-rw-r--r--accessible/tests/mochitest/events/test_scroll_caret.xhtml91
-rw-r--r--accessible/tests/mochitest/events/test_selection.html109
-rw-r--r--accessible/tests/mochitest/events/test_selection.xhtml254
-rw-r--r--accessible/tests/mochitest/events/test_selection_aria.html122
-rw-r--r--accessible/tests/mochitest/events/test_statechange.html565
-rw-r--r--accessible/tests/mochitest/events/test_statechange.xhtml117
-rw-r--r--accessible/tests/mochitest/events/test_text.html310
-rw-r--r--accessible/tests/mochitest/events/test_text_alg.html249
-rw-r--r--accessible/tests/mochitest/events/test_textattrchange.html107
-rw-r--r--accessible/tests/mochitest/events/test_textselchange.html82
-rw-r--r--accessible/tests/mochitest/events/test_tree.xhtml358
-rw-r--r--accessible/tests/mochitest/events/test_valuechange.html315
65 files changed, 12975 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";
+ };
+}
diff --git a/accessible/tests/mochitest/events/a11y.toml b/accessible/tests/mochitest/events/a11y.toml
new file mode 100644
index 0000000000..501b59f7b7
--- /dev/null
+++ b/accessible/tests/mochitest/events/a11y.toml
@@ -0,0 +1,128 @@
+[DEFAULT]
+support-files = [
+ "focus.html",
+ "scroll.html",
+ "slow_image.sjs",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/image/test/mochitest/animated-gif-finalframe.gif",
+ "!/image/test/mochitest/animated-gif.gif"]
+
+["test_announcement.html"]
+
+["test_aria_alert.html"]
+
+["test_aria_menu.html"]
+
+["test_aria_objattr.html"]
+
+["test_aria_owns.html"]
+
+["test_aria_statechange.html"]
+
+["test_attrchange.html"]
+
+["test_attrs.html"]
+
+["test_bug1322593-2.html"]
+
+["test_bug1322593.html"]
+
+["test_caretmove.html"]
+
+["test_coalescence.html"]
+
+["test_contextmenu.html"]
+
+["test_descrchange.html"]
+
+["test_dragndrop.html"]
+
+["test_flush.html"]
+
+["test_focus_aria_activedescendant.html"]
+
+["test_focus_autocomplete.html"]
+
+["test_focus_autocomplete.xhtml"]
+# Disabled on Linux and Windows due to frequent failures - bug 695019, bug 890795
+skip-if = [
+ "os == 'win'",
+ "os == 'linux'",
+]
+
+["test_focus_canvas.html"]
+
+["test_focus_contextmenu.xhtml"]
+
+["test_focus_controls.html"]
+
+["test_focus_doc.html"]
+
+["test_focus_general.html"]
+
+["test_focus_general.xhtml"]
+
+["test_focus_listcontrols.xhtml"]
+
+["test_focus_menu.xhtml"]
+
+["test_focus_name.html"]
+
+["test_focus_removal.html"]
+
+["test_focus_selects.html"]
+
+["test_focus_tabbox.xhtml"]
+skip-if = ["true"]
+
+["test_focus_tree.xhtml"]
+
+["test_focusable_statechange.html"]
+
+["test_fromUserInput.html"]
+
+["test_label.xhtml"]
+
+["test_menu.xhtml"]
+
+["test_mutation.html"]
+
+["test_namechange.html"]
+
+["test_namechange.xhtml"]
+
+["test_scroll.xhtml"]
+
+["test_scroll_caret.xhtml"]
+
+["test_selection.html"]
+skip-if = [
+ "os == 'mac'",
+]
+
+["test_selection.xhtml"]
+skip-if = [
+ "os == 'mac'",
+]
+
+["test_selection_aria.html"]
+
+["test_statechange.html"]
+
+["test_statechange.xhtml"]
+
+["test_text.html"]
+
+["test_text_alg.html"]
+
+["test_textattrchange.html"]
+
+["test_textselchange.html"]
+
+["test_tree.xhtml"]
+
+["test_valuechange.html"]
+skip-if = [
+ "os == 'mac'",
+]
diff --git a/accessible/tests/mochitest/events/docload/a11y.toml b/accessible/tests/mochitest/events/docload/a11y.toml
new file mode 100644
index 0000000000..dc71817725
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/a11y.toml
@@ -0,0 +1,21 @@
+[DEFAULT]
+support-files = [
+ "docload_wnd.html",
+ "!/accessible/tests/mochitest/*.js"]
+
+["test_docload_aria.html"]
+
+["test_docload_busy.html"]
+
+["test_docload_embedded.html"]
+
+["test_docload_iframe.html"]
+
+["test_docload_root.html"]
+skip-if = ["os == 'mac'"] # bug 1456997
+
+["test_docload_shutdown.html"]
+skip-if = [
+ "os == 'mac'", # bug 1456997
+ "display == 'wayland'", # bug 1850412
+]
diff --git a/accessible/tests/mochitest/events/docload/docload_wnd.html b/accessible/tests/mochitest/events/docload/docload_wnd.html
new file mode 100644
index 0000000000..93df1e86d4
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/docload_wnd.html
@@ -0,0 +1,37 @@
+<html>
+<head>
+ <title>Accessible events testing for document</title>
+ <script>
+ const STATE_BUSY = Ci.nsIAccessibleStates.STATE_BUSY;
+
+ var gService = null;
+ function waitForDocLoad() {
+ if (!gService) {
+ gService = Cc["@mozilla.org/accessibilityService;1"].
+ getService(Ci.nsIAccessibilityService);
+ }
+
+ var accDoc = gService.getAccessibleFor(document);
+
+ var state = {};
+ accDoc.getState(state, {});
+ if (state.value & STATE_BUSY) {
+ window.setTimeout(waitForDocLoad, 0);
+ return;
+ }
+
+ hideIFrame();
+ }
+
+ function hideIFrame() {
+ var iframe = document.getElementById("iframe");
+ gService.getAccessibleFor(iframe.contentDocument);
+ iframe.style.display = "none";
+ }
+ </script>
+</head>
+
+<body onload="waitForDocLoad();">
+ <iframe id="iframe"></iframe>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/docload/test_docload_aria.html b/accessible/tests/mochitest/events/docload/test_docload_aria.html
new file mode 100644
index 0000000000..c5fc099918
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/test_docload_aria.html
@@ -0,0 +1,75 @@
+<html>
+
+<head>
+ <title>Accessible events testing for ARIA document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../states.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function showARIADialog(aID) {
+ this.dialogNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, this.dialogNode),
+ ];
+
+ this.invoke = function showARIADialog_invoke() {
+ this.dialogNode.style.display = "block";
+ };
+
+ this.getID = function showARIADialog_getID() {
+ return "show ARIA dialog";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new showARIADialog("dialog"));
+ gQueue.push(new showARIADialog("document"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=759833"
+ title="ARIA documents should fire document loading events">
+ Mozilla Bug 759833
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="dialog" id="dialog" style="display: none;">It's a dialog</div>
+ <div role="document" id="document" style="display: none;">It's a document</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/docload/test_docload_busy.html b/accessible/tests/mochitest/events/docload/test_docload_busy.html
new file mode 100644
index 0000000000..37caf306bb
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/test_docload_busy.html
@@ -0,0 +1,83 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../states.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function makeIFrameVisible(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.DOMNode.parentNode),
+ {
+ type: EVENT_STATE_CHANGE,
+ get target() {
+ return getAccessible("iframe").firstChild;
+ },
+ match(aEvent) {
+ // The document shouldn't have busy state (the DOM document was
+ // loaded before its accessible was created). Do this test lately to
+ // make sure the content of document accessible was created
+ // initially, prior to this the document accessible keeps busy
+ // state. The initial creation happens asynchronously after document
+ // creation, there are no events we could use to catch it.
+ let { state, isEnabled } = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
+ return state & STATE_BUSY && !isEnabled;
+ },
+ },
+ ];
+
+ this.invoke = () => (this.DOMNode.style.visibility = "visible");
+
+ this.getID = () =>
+ "The accessible for DOM document loaded before it's shown shouldn't have busy state.";
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ const gQueue = new eventQueue();
+ gQueue.push(new makeIFrameVisible("iframe"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=658185"
+ title="The DOM document loaded before it's shown shouldn't have busy state">
+ Mozilla Bug 658185
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer"><iframe id="iframe" src="about:mozilla" style="visibility: hidden;"></iframe></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/docload/test_docload_embedded.html b/accessible/tests/mochitest/events/docload/test_docload_embedded.html
new file mode 100644
index 0000000000..18873dc904
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/test_docload_embedded.html
@@ -0,0 +1,85 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function changeIframeSrc(aIdentifier, aURL, aTitle) {
+ this.DOMNode = getNode(aIdentifier);
+
+ function getIframeDoc() {
+ return getAccessible(getNode(aIdentifier).contentDocument);
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getAccessible(this.DOMNode)),
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, getIframeDoc),
+ ];
+
+ this.invoke = () => (this.DOMNode.src = aURL);
+
+ this.finalCheck = () =>
+ testAccessibleTree(this.DOMNode, {
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ {
+ role: ROLE_DOCUMENT,
+ name: aTitle,
+ },
+ ],
+ });
+
+ this.getID = () => `change iframe src on ${aURL}`;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ const gQueue = new eventQueue();
+ gQueue.push(new changeIframeSrc("iframe", "about:license", "Licenses"));
+ gQueue.push(new changeIframeSrc("iframe", "about:buildconfig", "Build Configuration"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=420845"
+ title="Fire event_reorder on any embedded frames/iframes whos document has just loaded">
+ Mozilla Bug 420845
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=754165"
+ title="Fire document load events on iframes too">
+ Mozilla Bug 754165
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer"><iframe id="iframe"></iframe></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/docload/test_docload_iframe.html b/accessible/tests/mochitest/events/docload/test_docload_iframe.html
new file mode 100644
index 0000000000..d410ebb7e2
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/test_docload_iframe.html
@@ -0,0 +1,99 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../states.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ const kHide = 1;
+ const kShow = 2;
+ const kRemove = 3;
+
+ function morphIFrame(aIdentifier, aAction) {
+ this.DOMNode = getNode(aIdentifier);
+ this.IFrameContainerDOMNode = this.DOMNode.parentNode;
+
+ this.eventSeq = [
+ new invokerChecker(aAction === kShow ? EVENT_SHOW : EVENT_HIDE, this.DOMNode),
+ new invokerChecker(EVENT_REORDER, this.IFrameContainerDOMNode),
+ ];
+
+ this.invoke = () => {
+ if (aAction === kRemove) {
+ this.IFrameContainerDOMNode.removeChild(this.DOMNode);
+ } else {
+ this.DOMNode.style.display = aAction === kHide ? "none" : "block";
+ }
+ };
+
+ this.finalCheck = () =>
+ testAccessibleTree(this.IFrameContainerDOMNode, {
+ role: ROLE_SECTION,
+ children: (aAction == kHide || aAction == kRemove) ? [ ] :
+ [
+ {
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ { role: ROLE_DOCUMENT },
+ ],
+ },
+ ],
+ });
+
+ this.getID = () => {
+ if (aAction === kRemove) {
+ return "remove iframe";
+ }
+
+ return `change display style of iframe to ${aAction === kHide ? "none" : "block"}`;
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ const gQueue = new eventQueue(EVENT_REORDER);
+ gQueue.push(new morphIFrame("iframe", kHide));
+ gQueue.push(new morphIFrame("iframe", kShow));
+ gQueue.push(new morphIFrame("iframe", kRemove));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=566103"
+ title="Reorganize accessible document handling">
+ Mozilla Bug 566103
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer"><iframe id="iframe"></iframe></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/docload/test_docload_root.html b/accessible/tests/mochitest/events/docload/test_docload_root.html
new file mode 100644
index 0000000000..91ce3a10ee
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/test_docload_root.html
@@ -0,0 +1,125 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ let gDialog;
+ let gDialogDoc;
+ let gRootAcc;
+
+ function openDialogWnd(aURL) {
+ // Get application root accessible.
+ let docAcc = getAccessible(document);
+ while (docAcc) {
+ gRootAcc = docAcc;
+ try {
+ docAcc = docAcc.parent;
+ } catch (e) {
+ ok(false, `Can't get parent for ${prettyName(docAcc)}`);
+ throw e;
+ }
+ }
+
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_REORDER, gRootAcc),
+ // We use a function here to get the target because gDialog isn't set
+ // yet, but it will be when the function is called.
+ new invokerChecker(EVENT_FOCUS, () => gDialog.document)
+ ];
+
+ this.invoke = () => (gDialog = window.browsingContext.topChromeWindow.openDialog(aURL));
+
+ this.finalCheck = () => {
+ const accTree = {
+ role: ROLE_APP_ROOT,
+ children: [
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ ],
+ };
+
+ testAccessibleTree(gRootAcc, accTree);
+
+ gDialogDoc = gDialog.document;
+ ok(isAccessibleInCache(gDialogDoc),
+ `The document accessible for '${aURL}' is not in cache!`);
+ };
+
+ this.getID = () => `open dialog '${aURL}'`;
+ }
+
+ function closeDialogWnd() {
+ this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ];
+
+ this.invoke = () => {
+ gDialog.close();
+ window.focus();
+ };
+
+ this.finalCheck = () => {
+ ok(!isAccessibleInCache(gDialogDoc),
+ `The document accessible for dialog is in cache still!`);
+
+ gDialog = gDialogDoc = gRootAcc = null;
+ };
+
+ this.getID = () => "close dialog";
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ // Front end stuff sometimes likes to stuff things in the hidden window(s)
+ // in which case we should repress all accessibles for those.
+
+ // Try to create an accessible for the hidden window's document.
+ let doc = Services.appShell.hiddenDOMWindow.document;
+ let hiddenDocAcc = gAccService.getAccessibleFor(doc);
+ ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible.");
+
+ const gQueue = new eventQueue();
+ gQueue.push(new openDialogWnd("about:about"));
+ gQueue.push(new closeDialogWnd());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=506206"
+ title="Fire event_reorder application root accessible">
+ Mozilla Bug 506206
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/docload/test_docload_shutdown.html b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html
new file mode 100644
index 0000000000..a111d9e43b
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html
@@ -0,0 +1,142 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ let gDialog;
+ let gDialogDoc;
+ let gRootAcc;
+ let gIframeDoc;
+
+ function openWndShutdownDoc(aURL) {
+ // Get application root accessible.
+ let docAcc = getAccessible(document);
+ while (docAcc) {
+ gRootAcc = docAcc;
+ try {
+ docAcc = docAcc.parent;
+ } catch (e) {
+ ok(false, `Can't get parent for ${prettyName(docAcc)}`);
+ throw e;
+ }
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, gRootAcc),
+ {
+ type: EVENT_HIDE,
+ get target() {
+ gDialogDoc = gDialog.document;
+ const iframe = gDialogDoc.getElementById("iframe");
+ gIframeDoc = iframe.contentDocument;
+ return iframe;
+ },
+ get targetDescr() {
+ return "inner iframe of docload_wnd.html document";
+ },
+ },
+ ];
+
+
+ this.invoke = () => gDialog = window.browsingContext.topChromeWindow.openDialog(aURL);
+
+ this.finalCheck = () => {
+ const accTree = {
+ role: ROLE_APP_ROOT,
+ children: [
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ ],
+ };
+
+ testAccessibleTree(gRootAcc, accTree);
+ // After timeout after event hide for iframe was handled the document
+ // accessible for iframe's document should no longer be in cache.
+ ok(!isAccessibleInCache(gIframeDoc),
+ "The document accessible for iframe is in cache still after iframe hide!");
+ ok(isAccessibleInCache(gDialogDoc),
+ `The document accessible for '${aURL}' is not in cache!`);
+ };
+
+ this.getID = () => `open dialog '${aURL}'`;
+ }
+
+ function closeWndShutdownDoc() {
+ this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ];
+
+ this.invoke = () => {
+ gDialog.close();
+ window.focus();
+ };
+
+ this.finalCheck = () => {
+ ok(!isAccessibleInCache(gDialogDoc),
+ "The document accessible for dialog is in cache still!");
+ // After the window is closed all alive subdocument accessibles should
+ // be shut down.
+ ok(!isAccessibleInCache(gIframeDoc),
+ "The document accessible for iframe is in cache still!");
+
+ gDialog = gDialogDoc = gRootAcc = gIframeDoc = null;
+ };
+
+ this.getID = () => "close dialog";
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ // Front end stuff sometimes likes to stuff things in the hidden window(s)
+ // in which case we should repress all accessibles for those.
+
+ // Try to create an accessible for the hidden window's document.
+ let doc = Services.appShell.hiddenDOMWindow.document;
+ let hiddenDocAcc = gAccService.getAccessibleFor(doc);
+ ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible.");
+
+ const gQueue = new eventQueue();
+ gQueue.push(new openWndShutdownDoc("../../events/docload/docload_wnd.html"));
+ gQueue.push(new closeWndShutdownDoc());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=571459"
+ title="Shutdown document accessible when presshell goes away">
+ Mozilla Bug 571459
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/focus.html b/accessible/tests/mochitest/events/focus.html
new file mode 100644
index 0000000000..ab055df82c
--- /dev/null
+++ b/accessible/tests/mochitest/events/focus.html
@@ -0,0 +1,10 @@
+<html>
+
+<head>
+ <title>editable document</title>
+</head>
+
+<body contentEditable="true">
+ editable document
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/scroll.html b/accessible/tests/mochitest/events/scroll.html
new file mode 100644
index 0000000000..562e0a3825
--- /dev/null
+++ b/accessible/tests/mochitest/events/scroll.html
@@ -0,0 +1,181 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing for anchors</title>
+</head>
+
+<body>
+ <p>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ </p>
+ <a name="link1">link1</a>
+
+ <p style="color: blue">
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ </p>
+
+ <h1 id="heading_1">heading 1</h1>
+ <p style="color: blue">
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ </p>
+</body>
+<html>
diff --git a/accessible/tests/mochitest/events/slow_image.sjs b/accessible/tests/mochitest/events/slow_image.sjs
new file mode 100644
index 0000000000..f322568be6
--- /dev/null
+++ b/accessible/tests/mochitest/events/slow_image.sjs
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+// stolen from file_blocked_script.sjs
+function setGlobalState(data, key) {
+ let x = {
+ data,
+ QueryInterface: ChromeUtils.generateQI([]),
+ };
+ x.wrappedJSObject = x;
+ setObjectState(key, x);
+}
+
+function getGlobalState(key) {
+ var data;
+ getObjectState(key, function (x) {
+ data = x && x.wrappedJSObject.data;
+ });
+ return data;
+}
+
+function handleRequest(request, response) {
+ if (request.queryString == "complete") {
+ // Unblock the previous request.
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "application/json", false);
+ response.write("true"); // the payload doesn't matter.
+
+ let blockedResponse = getGlobalState("a11y-image");
+ if (blockedResponse) {
+ blockedResponse.setStatusLine(request.httpVersion, 200, "OK");
+ blockedResponse.setHeader("Cache-Control", "no-cache", false);
+ blockedResponse.setHeader("Content-Type", "image/png", false);
+ blockedResponse.write(IMG_BYTES);
+ blockedResponse.finish();
+
+ setGlobalState(undefined, "a11y-image");
+ }
+ } else {
+ // Getting the image
+ response.processAsync();
+ // Store the response in the global state
+ setGlobalState(response, "a11y-image");
+ }
+}
diff --git a/accessible/tests/mochitest/events/test_announcement.html b/accessible/tests/mochitest/events/test_announcement.html
new file mode 100644
index 0000000000..eb303e4aa9
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_announcement.html
@@ -0,0 +1,61 @@
+<html>
+
+<head>
+ <title>Announcement event and method testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function doTests() {
+ let acc = getAccessible("display");
+
+ let onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc);
+ acc.announce("please", nsIAccessibleAnnouncementEvent.POLITE);
+ let evt = await onAnnounce;
+ evt.QueryInterface(nsIAccessibleAnnouncementEvent);
+ is(evt.announcement, "please", "announcement matches.");
+ is(evt.priority, nsIAccessibleAnnouncementEvent.POLITE, "priority matches");
+
+ onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc);
+ acc.announce("do it", nsIAccessibleAnnouncementEvent.ASSERTIVE);
+ evt = await onAnnounce;
+ evt.QueryInterface(nsIAccessibleAnnouncementEvent);
+ is(evt.announcement, "do it", "announcement matches.");
+ is(evt.priority, nsIAccessibleAnnouncementEvent.ASSERTIVE,
+ "priority matches");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1525980"
+ title="Introduce announcement event and method">
+ Mozilla Bug 1525980
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_aria_alert.html b/accessible/tests/mochitest/events/test_aria_alert.html
new file mode 100644
index 0000000000..48f4197b50
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_aria_alert.html
@@ -0,0 +1,84 @@
+<html>
+
+<head>
+ <title>ARIA alert event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function showAlert(aID) {
+ this.DOMNode = document.createElement("div");
+
+ this.invoke = function showAlert_invoke() {
+ this.DOMNode.setAttribute("role", "alert");
+ this.DOMNode.setAttribute("id", aID);
+ var text = document.createTextNode("alert");
+ this.DOMNode.appendChild(text);
+ document.body.appendChild(this.DOMNode);
+ };
+
+ this.getID = function showAlert_getID() {
+ return "Show ARIA alert " + aID;
+ };
+ }
+
+ function changeAlert(aID) {
+ this.__defineGetter__("DOMNode", function() { return getNode(aID); });
+
+ this.invoke = function changeAlert_invoke() {
+ this.DOMNode.textContent = "new alert";
+ };
+
+ this.getID = function showAlert_getID() {
+ return "Change ARIA alert " + aID;
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuging
+ // enableLogging("tree,events,verbose");
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue(nsIAccessibleEvent.EVENT_ALERT);
+
+ gQueue.push(new showAlert("alert"));
+ gQueue.push(new changeAlert("alert"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=591199"
+ title="mochitest for bug 334386: fire alert event when ARIA alert is shown or new its children are inserted">
+ Mozilla Bug 591199
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_aria_menu.html b/accessible/tests/mochitest/events/test_aria_menu.html
new file mode 100644
index 0000000000..b240090cb9
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_aria_menu.html
@@ -0,0 +1,267 @@
+<html>
+
+<head>
+ <title>ARIA menu events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ const kViaDisplayStyle = 0;
+ const kViaVisibilityStyle = 1;
+
+ function focusMenu(aMenuBarID, aMenuID, aActiveMenuBarID) {
+ this.eventSeq = [];
+
+ if (aActiveMenuBarID) {
+ this.eventSeq.push(new invokerChecker(EVENT_MENU_END,
+ getNode(aActiveMenuBarID)));
+ }
+
+ this.eventSeq.push(new invokerChecker(EVENT_MENU_START, getNode(aMenuBarID)));
+ this.eventSeq.push(new invokerChecker(EVENT_FOCUS, getNode(aMenuID)));
+
+ this.invoke = function focusMenu_invoke() {
+ getNode(aMenuID).focus();
+ };
+
+ this.getID = function focusMenu_getID() {
+ return "focus menu '" + aMenuID + "'";
+ };
+ }
+
+ function showMenu(aMenuID, aParentMenuID, aHow) {
+ this.menuNode = getNode(aMenuID);
+
+ // Because of aria-owns processing we may have menupopup start fired before
+ // related show event.
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.menuNode),
+ new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)),
+ new invokerChecker(EVENT_MENUPOPUP_START, this.menuNode),
+ ];
+
+ this.invoke = function showMenu_invoke() {
+ if (aHow == kViaDisplayStyle)
+ this.menuNode.style.display = "block";
+ else
+ this.menuNode.style.visibility = "visible";
+ };
+
+ this.getID = function showMenu_getID() {
+ return "Show ARIA menu '" + aMenuID + "' by " +
+ (aHow == kViaDisplayStyle ? "display" : "visibility") +
+ " style tricks";
+ };
+ }
+
+ function closeMenu(aMenuID, aParentMenuID, aHow) {
+ this.menuNode = getNode(aMenuID);
+ this.menu = null;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getMenu, this),
+ new invokerChecker(EVENT_MENUPOPUP_END, getMenu, this),
+ new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)),
+ ];
+
+ this.invoke = function closeMenu_invoke() {
+ // Store menu accessible reference while menu is still open.
+ this.menu = getAccessible(this.menuNode);
+
+ // Hide menu.
+ if (aHow == kViaDisplayStyle)
+ this.menuNode.style.display = "none";
+ else
+ this.menuNode.style.visibility = "hidden";
+ };
+
+ this.getID = function closeMenu_getID() {
+ return "Close ARIA menu " + aMenuID + " by " +
+ (aHow == kViaDisplayStyle ? "display" : "visibility") +
+ " style tricks";
+ };
+
+ function getMenu(aThisObj) {
+ return aThisObj.menu;
+ }
+ }
+
+ function focusInsideMenu(aMenuID, aMenuBarID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode(aMenuID)),
+ ];
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)),
+ ];
+
+ this.invoke = function focusInsideMenu_invoke() {
+ getNode(aMenuID).focus();
+ };
+
+ this.getID = function focusInsideMenu_getID() {
+ return "focus menu '" + aMenuID + "'";
+ };
+ }
+
+ function blurMenu(aMenuBarID) {
+ var eventSeq = [
+ new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)),
+ new invokerChecker(EVENT_FOCUS, getNode("outsidemenu")),
+ ];
+
+ this.__proto__ = new synthClick("outsidemenu", eventSeq);
+
+ this.getID = function blurMenu_getID() {
+ return "blur menu";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuging
+ // enableLogging("tree,events,verbose");
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new focusMenu("menubar2", "menu-help"));
+ gQueue.push(new focusMenu("menubar", "menu-file", "menubar2"));
+ gQueue.push(new showMenu("menupopup-file", "menu-file", kViaDisplayStyle));
+ gQueue.push(new closeMenu("menupopup-file", "menu-file", kViaDisplayStyle));
+ gQueue.push(new showMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle));
+ gQueue.push(new closeMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle));
+ gQueue.push(new focusInsideMenu("menu-edit", "menubar"));
+ gQueue.push(new blurMenu("menubar"));
+
+ gQueue.push(new focusMenu("menubar3", "mb3-mi-outside"));
+ gQueue.push(new showMenu("mb4-menu", document, kViaDisplayStyle));
+ gQueue.push(new focusMenu("menubar4", "mb4-item1"));
+ gQueue.push(new focusMenu("menubar5", "mb5-mi"));
+
+ gQueue.push(new synthFocus("mi6"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=606207"
+ title="Dojo dropdown buttons are broken">
+ Bug 606207
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=614829"
+ title="Menupopup end event isn't fired for ARIA menus">
+ Bug 614829
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189"
+ title="Clean up FireAccessibleFocusEvent">
+ Bug 615189
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Bug 673958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=933322"
+ title="menustart/end events are missing when aria-owns makes a menu hierarchy">
+ Bug 933322
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=934460"
+ title="menustart/end events may be missed when top level menuitem is focused">
+ Bug 934460
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=970005"
+ title="infinite long loop in a11y:FocusManager::ProcessFocusEvent">
+ Bug 970005
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="menubar" role="menubar">
+ <div id="menu-file" role="menuitem" tabindex="0">
+ File
+ <div id="menupopup-file" role="menu" style="display: none;">
+ <div id="menuitem-newtab" role="menuitem" tabindex="0">New Tab</div>
+ <div id="menuitem-newwindow" role="menuitem" tabindex="0">New Window</div>
+ </div>
+ </div>
+ <div id="menu-edit" role="menuitem" tabindex="0">
+ Edit
+ <div id="menupopup-edit" role="menu" style="visibility: hidden;">
+ <div id="menuitem-undo" role="menuitem" tabindex="0">Undo</div>
+ <div id="menuitem-redo" role="menuitem" tabindex="0">Redo</div>
+ </div>
+ </div>
+ </div>
+ <div id="menubar2" role="menubar">
+ <div id="menu-help" role="menuitem" tabindex="0">
+ Help
+ <div id="menupopup-help" role="menu" style="display: none;">
+ <div id="menuitem-about" role="menuitem" tabindex="0">About</div>
+ </div>
+ </div>
+ </div>
+ <div tabindex="0" id="outsidemenu">outsidemenu</div>
+
+ <!-- aria-owns relations -->
+ <div id="menubar3" role="menubar" aria-owns="mb3-mi-outside"></div>
+ <div id="mb3-mi-outside" role="menuitem" tabindex="0">Outside</div>
+
+ <div id="menubar4" role="menubar">
+ <div id="mb4_topitem" role="menuitem" aria-haspopup="true"
+ aria-owns="mb4-menu">Item</div>
+ </div>
+ <div id="mb4-menu" role="menu" style="display:none;">
+ <div role="menuitem" id="mb4-item1" tabindex="0">Item 1.1</div>
+ <div role="menuitem" tabindex="0">Item 1.2</div>
+ </div>
+
+ <!-- focus top-level menu item having haspopup -->
+ <div id="menubar5" role="menubar">
+ <div role="menuitem" aria-haspopup="true" id="mb5-mi" tabindex="0">
+ Item
+ <div role="menu" style="display:none;">
+ <div role="menuitem" tabindex="0">Item 1.1</div>
+ <div role="menuitem" tabindex="0">Item 1.2</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- other aria-owns relations -->
+ <div id="mi6" tabindex="0" role="menuitem">aria-owned item</div>
+ <div aria-owns="mi6">Obla</div>
+
+ <div id="eventdump"></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_aria_objattr.html b/accessible/tests/mochitest/events/test_aria_objattr.html
new file mode 100644
index 0000000000..709089ca02
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_aria_objattr.html
@@ -0,0 +1,68 @@
+<html>
+
+<head>
+ <title>Accessible ARIA object attribute changes</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+ function updateAttribute(aID, aAttr, aValue) {
+ this.node = getNode(aID);
+ this.accessible = getAccessible(this.node);
+
+ this.eventSeq = [
+ new objAttrChangedChecker(aID, aAttr),
+ ];
+
+ this.invoke = function updateAttribute_invoke() {
+ this.node.setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function updateAttribute_getID() {
+ return aAttr + " for " + aID + " " + aValue;
+ };
+ }
+
+ // gA11yEventDumpToConsole = true;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new updateAttribute("sortable", "aria-sort", "ascending"));
+
+ // For experimental ARIA extensions
+ gQueue.push(new updateAttribute("custom", "aria-blah", "true"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="sortable" role="columnheader" aria-sort="none">aria-sort</div>
+
+ <div id="custom" role="custom" aria-blah="false">Fat free cheese</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_aria_owns.html b/accessible/tests/mochitest/events/test_aria_owns.html
new file mode 100644
index 0000000000..3c638ad838
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_aria_owns.html
@@ -0,0 +1,122 @@
+<html>
+
+<head>
+ <title>Aria-owns targets shouldn't be on invalidation list so shouldn't have
+ show/hide events</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests.
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+ // enableLogging("tree,eventTree,verbose");
+
+ /**
+ * Aria-owns target shouldn't have a show event.
+ * Markup:
+ * <div id="t1_fc" aria-owns="t1_owns"></div>
+ * <span id="t1_owns"></div>
+ */
+ function testAriaOwns() {
+ this.parent = getNode("t1");
+ this.fc = document.createElement("div");
+ this.fc.setAttribute("id", "t1_fc");
+ this.owns = document.createElement("span");
+ this.owns.setAttribute("id", "t1_owns");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.fc),
+ new unexpectedInvokerChecker(EVENT_SHOW, this.owns),
+ ];
+
+ this.invoke = function testAriaOwns_invoke() {
+ getNode("t1").appendChild(this.fc);
+ getNode("t1").appendChild(this.owns);
+ getNode("t1_fc").setAttribute("aria-owns", "t1_owns");
+ };
+
+ this.getID = function testAriaOwns_getID() {
+ return "Aria-owns target shouldn't have show event";
+ };
+ }
+
+ /**
+ * Target of both aria-owns and other aria attribute like aria-labelledby
+ * shouldn't have a show event.
+ * Markup:
+ * <div id="t2_fc" aria-owns="t1_owns"></div>
+ * <div id="t2_sc" aria-labelledby="t2_owns"></div>
+ * <span id="t2_owns"></div>
+ */
+ function testAriaOwnsAndLabelledBy() {
+ this.parent = getNode("t2");
+ this.fc = document.createElement("div");
+ this.fc.setAttribute("id", "t2_fc");
+ this.sc = document.createElement("div");
+ this.sc.setAttribute("id", "t2_sc");
+ this.owns = document.createElement("span");
+ this.owns.setAttribute("id", "t2_owns");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.fc),
+ new invokerChecker(EVENT_SHOW, this.sc),
+ new unexpectedInvokerChecker(EVENT_SHOW, this.owns),
+ ];
+
+ this.invoke = function testAriaOwns_invoke() {
+ getNode("t2").appendChild(this.fc);
+ getNode("t2").appendChild(this.sc);
+ getNode("t2").appendChild(this.owns);
+ getNode("t2_fc").setAttribute("aria-owns", "t2_owns");
+ getNode("t2_sc").setAttribute("aria-labelledby", "t2_owns");
+ };
+
+ this.getID = function testAriaOwns_getID() {
+ return "Aria-owns and aria-labelledby target shouldn't have show event";
+ };
+ }
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+ gQueue.push(new testAriaOwns());
+ gQueue.push(new testAriaOwnsAndLabelledBy());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1296420"
+ title="Aria-owns targets shouldn't be on invalidation list so shouldn't
+ have show/hide events">
+ Mozilla Bug 1296420
+ </a><br>
+
+ <div id="testContainer">
+ <div id="t1"></div>
+
+ <div id="t2"></div>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_aria_statechange.html b/accessible/tests/mochitest/events/test_aria_statechange.html
new file mode 100644
index 0000000000..7796d88ec4
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_aria_statechange.html
@@ -0,0 +1,231 @@
+<html>
+
+<head>
+ <title>ARIA state change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debugging
+ // gA11yEventDumpToConsole = true; // debugging
+
+ function expandNode(aID, aIsExpanded) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new expandedStateChecker(aIsExpanded, this.DOMNode),
+ ];
+
+ this.invoke = function expandNode_invoke() {
+ this.DOMNode.setAttribute("aria-expanded",
+ (aIsExpanded ? "true" : "false"));
+ };
+
+ this.getID = function expandNode_getID() {
+ return prettyName(aID) + " aria-expanded changed to '" + aIsExpanded + "'";
+ };
+ }
+
+ function busyify(aID, aIsBusy) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new stateChangeChecker(STATE_BUSY, kOrdinalState, aIsBusy, this.DOMNode),
+ ];
+
+ this.invoke = function busyify_invoke() {
+ this.DOMNode.setAttribute("aria-busy", (aIsBusy ? "true" : "false"));
+ };
+
+ this.getID = function busyify_getID() {
+ return prettyName(aID) + " aria-busy changed to '" + aIsBusy + "'";
+ };
+ }
+
+ function makeCurrent(aID, aIsCurrent, aValue) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new stateChangeChecker(EXT_STATE_CURRENT, true, aIsCurrent, this.DOMNode),
+ ];
+
+ this.invoke = function makeCurrent_invoke() {
+ this.DOMNode.setAttribute("aria-current", aValue);
+ };
+
+ this.getID = function makeCurrent_getID() {
+ return prettyName(aID) + " aria-current changed to " + aValue;
+ };
+ }
+
+ async function testToggleAttribute(aID, aAttribute, aIncludeMixed) {
+ let toggleState = aAttribute == "aria-pressed" ? STATE_PRESSED : STATE_CHECKED;
+
+ // bug 472142. Role changes here if aria-pressed is added,
+ // accessible should be recreated?
+ let stateChange = PromEvents.waitForStateChange(aID, toggleState, true);
+ getNode(aID).setAttribute(aAttribute, "true");
+ await stateChange;
+
+ stateChange = PromEvents.waitForStateChange(aID, toggleState, false);
+ getNode(aID).setAttribute(aAttribute, "false");
+ await stateChange;
+
+ if (aIncludeMixed) {
+ stateChange = PromEvents.waitForStateChange(aID, STATE_MIXED, true);
+ getNode(aID).setAttribute(aAttribute, "mixed");
+ await stateChange;
+
+ stateChange = PromEvents.waitForStateChange(aID, STATE_MIXED, false);
+ getNode(aID).setAttribute(aAttribute, "");
+ await stateChange;
+ }
+
+ stateChange = PromEvents.waitForStateChange(aID, toggleState, true);
+ getNode(aID).setAttribute(aAttribute, "true");
+ await stateChange;
+
+ if (aIncludeMixed) {
+ stateChange = Promise.all([
+ PromEvents.waitForStateChange(aID, STATE_MIXED, true),
+ PromEvents.waitForStateChange(aID, toggleState, false)]);
+ getNode(aID).setAttribute(aAttribute, "mixed");
+ await stateChange;
+
+ stateChange = Promise.all([
+ PromEvents.waitForStateChange(aID, STATE_MIXED, false),
+ PromEvents.waitForStateChange(aID, toggleState, true)]);
+ getNode(aID).setAttribute(aAttribute, "true");
+ await stateChange;
+ }
+
+ // bug 472142. Role changes here too if aria-pressed is removed,
+ // accessible should be recreated?
+ stateChange = PromEvents.waitForStateChange(aID, toggleState, false);
+ getNode(aID).removeAttribute(aAttribute);
+ await stateChange;
+ }
+
+ async function doTests() {
+ gQueue = new eventQueue();
+
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new expandNode("section", true));
+ gQueue.push(new expandNode("section", false));
+ gQueue.push(new expandNode("div", true));
+ gQueue.push(new expandNode("div", false));
+
+ gQueue.push(new busyify("aria_doc", true));
+ gQueue.push(new busyify("aria_doc", false));
+
+ gQueue.push(new makeCurrent("current_page_1", false, "false"));
+ gQueue.push(new makeCurrent("current_page_2", true, "page"));
+ gQueue.push(new makeCurrent("current_page_2", false, "false"));
+ gQueue.push(new makeCurrent("current_page_3", true, "true"));
+ gQueue.push(new makeCurrent("current_page_3", false, ""));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ await testToggleAttribute("pressable", "aria-pressed", true);
+ await testToggleAttribute("pressable_native", "aria-pressed", true);
+ await testToggleAttribute("checkable", "aria-checked", true);
+ await testToggleAttribute("checkableBool", "aria-checked", false);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=551684"
+ title="No statechange event for aria-expanded on native HTML elements, is fired on ARIA widgets">
+ Mozilla Bug 551684
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=648133"
+ title="fire state change event for aria-busy">
+ Mozilla Bug 648133
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=467143"
+ title="mixed state change event is fired for focused accessible only">
+ Mozilla Bug 467143
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958"
+ title="Pressed state is not exposed on a button element with aria-pressed attribute">
+ Mozilla Bug 989958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563"
+ title="Support ARIA 1.1 switch role">
+ Mozilla Bug 1136563
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1355921"
+ title="Elements with a defined, non-false value for aria-current should expose ATK_STATE_ACTIVE">
+ Mozilla Bug 1355921
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- aria-expanded -->
+ <div id="section" role="section" aria-expanded="false">expandable section</div>
+ <div id="div" aria-expanded="false">expandable native div</div>
+
+ <!-- aria-busy -->
+ <div id="aria_doc" role="document" tabindex="0">A document</div>
+
+ <!-- aria-pressed -->
+ <div id="pressable" role="button"></div>
+ <button id="pressable_native"></button>
+
+ <!-- aria-checked -->
+ <div id="checkable" role="checkbox"></div>
+ <div id="checkableBool" role="switch"></div>
+
+ <!-- aria-current -->
+ <div id="current_page_1" role="link" aria-current="page">1</div>
+ <div id="current_page_2" role="link" aria-current="false">2</div>
+ <div id="current_page_3" role="link">3</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_attrchange.html b/accessible/tests/mochitest/events/test_attrchange.html
new file mode 100644
index 0000000000..edd9195ddd
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_attrchange.html
@@ -0,0 +1,107 @@
+<html>
+
+<head>
+ <title>Accessible attr change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ async function testGotAttrChange(elem, name, value) {
+ const waitFor = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, elem);
+ if (value) {
+ document.getElementById(elem).setAttribute(name, value);
+ } else {
+ document.getElementById(elem).removeAttribute(name);
+ }
+ await waitFor;
+ }
+
+ async function doTests() {
+ info("Removing summary attr");
+ // after summary is removed, we should have a layout table
+ await testGotAttrChange(
+ "sampleTable",
+ "summary",
+ null
+ );
+
+ info("Setting abbr attr");
+ // after abbr is set we should have a data table again
+ await testGotAttrChange(
+ "cellOne",
+ "abbr",
+ "hello world"
+ );
+
+ info("Removing abbr attr");
+ // after abbr is removed we should have a layout table again
+ await testGotAttrChange(
+ "cellOne",
+ "abbr",
+ null
+ );
+
+ info("Setting scope attr");
+ // after scope is set we should have a data table again
+ await testGotAttrChange(
+ "cellOne",
+ "scope",
+ "col"
+ );
+
+ info("Removing scope attr");
+ // remove scope should give layout
+ await testGotAttrChange(
+ "cellOne",
+ "scope",
+ null
+ );
+
+ info("Setting headers attr");
+ // add headers attr should give data
+ await testGotAttrChange(
+ "cellThree",
+ "headers",
+ "cellOne"
+ );
+
+ info("Removing headers attr");
+ // remove headers attr should give layout
+ await testGotAttrChange(
+ "cellThree",
+ "headers",
+ null
+ );
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<body>
+ <table id="sampleTable" summary="example summary">
+ <tr>
+ <td id="cellOne">cell1</td>
+ <td>cell2</td>
+ </tr>
+ <tr>
+ <td id="cellThree">cell3</td>
+ <td>cell4</td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_attrs.html b/accessible/tests/mochitest/events/test_attrs.html
new file mode 100644
index 0000000000..c09bd9cf1e
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_attrs.html
@@ -0,0 +1,85 @@
+<html>
+
+<head>
+ <title>Event object attributes tests</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Test "event-from-input" object attribute.
+ */
+ function eventFromInputChecker(aEventType, aID, aValue, aNoTargetID) {
+ this.type = aEventType;
+ this.target = getAccessible(aID);
+
+ this.noTarget = getNode(aNoTargetID);
+
+ this.check = function checker_check(aEvent) {
+ testAttrs(aEvent.accessible, { "event-from-input": aValue }, true);
+
+ var accessible = getAccessible(this.noTarget);
+ testAbsentAttrs(accessible, { "event-from-input": "" });
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ var id = "textbox", noTargetId = "textarea";
+ let checker =
+ new eventFromInputChecker(EVENT_FOCUS, id, "false", noTargetId);
+ gQueue.push(new synthFocus(id, checker));
+
+ if (!MAC) { // Mac failure is bug 541093
+ checker =
+ new eventFromInputChecker(EVENT_TEXT_CARET_MOVED, id, "true", noTargetId);
+ gQueue.push(new synthHomeKey(id, checker));
+ }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=540285"
+ title="Event object attributes testing">
+ Mozilla Bug 540285
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox" value="hello">
+ <textarea id="textarea"></textarea>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_bug1322593-2.html b/accessible/tests/mochitest/events/test_bug1322593-2.html
new file mode 100644
index 0000000000..05bd31ffa6
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_bug1322593-2.html
@@ -0,0 +1,77 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function changeMultipleElements() {
+ this.node1 = getNode("span1");
+ this.node2 = getNode("span2");
+
+ this.eventSeq = [
+ new textChangeChecker("container", 0, 5, "hello", false, undefined, true),
+ new textChangeChecker("container", 6, 11, "world", false, undefined, true),
+ new orderChecker(),
+ new textChangeChecker("container", 0, 1, "a", true, undefined, true),
+ new textChangeChecker("container", 7, 8, "b", true, undefined, true),
+ ];
+
+ this.invoke = function changeMultipleElements_invoke() {
+ this.node1.textContent = "a";
+ this.node2.textContent = "b";
+ };
+
+ this.getID = function changeMultipleElements_invoke_getID() {
+ return "Change the text content of multiple sibling divs";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+// gA11yEventDumpToConsole = true; // debugging
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeMultipleElements());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593"
+ title="missing text change events when multiple elements updated at once">
+ Mozilla Bug 1322593
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container">
+ <span id="span1">hello</span>
+ <span>your</span>
+ <span id="span2">world</span>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_bug1322593.html b/accessible/tests/mochitest/events/test_bug1322593.html
new file mode 100644
index 0000000000..968e808106
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_bug1322593.html
@@ -0,0 +1,74 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function changeMultipleElements() {
+ this.node1 = getNode("div1");
+ this.node2 = getNode("div2");
+
+ this.eventSeq = [
+ new textChangeChecker("div1", 0, 5, "hello", false, undefined, true),
+ new textChangeChecker("div2", 0, 5, "world", false, undefined, true),
+ new orderChecker(),
+ new textChangeChecker("div1", 0, 1, "a", true, undefined, true),
+ new textChangeChecker("div2", 0, 1, "b", true, undefined, true),
+ ];
+
+ this.invoke = function changeMultipleElements_invoke() {
+ this.node1.textContent = "a";
+ this.node2.textContent = "b";
+ };
+
+ this.getID = function changeMultipleElements_invoke_getID() {
+ return "Change the text content of multiple sibling divs";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+// gA11yEventDumpToConsole = true; // debugging
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeMultipleElements());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593"
+ title="missing text change events when multiple elements updated at once">
+ Mozilla Bug 1322593
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="div1">hello</div>
+ <div id="div2">world</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_caretmove.html b/accessible/tests/mochitest/events/test_caretmove.html
new file mode 100644
index 0000000000..d1091ac7f1
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_caretmove.html
@@ -0,0 +1,142 @@
+<html>
+
+<head>
+ <title>Accessible caret move events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Click checker.
+ */
+ function clickChecker(aCaretOffset, aIsSelectionCollapsed, aID, aExtraNodeOrID, aExtraCaretOffset) {
+ this.__proto__ = new caretMoveChecker(aCaretOffset, aIsSelectionCollapsed, aID);
+
+ this.extraNode = getNode(aExtraNodeOrID);
+
+ this.check = function clickChecker_check(aEvent) {
+ this.__proto__.check(aEvent);
+
+ if (this.extraNode) {
+ var acc = getAccessible(this.extraNode, [nsIAccessibleText]);
+ is(acc.caretOffset, aExtraCaretOffset,
+ "Wrong caret offset for " + aExtraNodeOrID);
+ }
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ // test caret move events and caret offsets
+ gQueue = new eventQueue();
+
+ var id = "textbox";
+ gQueue.push(new synthFocus(id, new caretMoveChecker(5, true, id)));
+ gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id)));
+ gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+
+ if (!MAC) {
+ gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id)));
+ gQueue.push(new synthHomeKey(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+ }
+ else {
+ todo(false, "Make these tests pass on OSX (bug 650294)");
+ }
+
+ id = "textarea";
+ gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+ gQueue.push(new synthDownKey(id, new caretMoveChecker(12, true, id)));
+
+ id = "textarea_wrapped";
+ gQueue.push(new setCaretOffset(id, 4, id));
+ gQueue.push(new synthLeftKey(id, new caretMoveChecker(4, true, id)));
+
+ id = "p";
+ gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+ gQueue.push(new synthDownKey(id, new caretMoveChecker(6, true, id)));
+
+ id = "p1_in_div";
+ gQueue.push(new synthClick(id, new clickChecker(0, true, id, "p2_in_div", -1)));
+
+ id = "p";
+ gQueue.push(new synthShiftTab(id, new caretMoveChecker(0, true, id)));
+ id = "textarea";
+ gQueue.push(new synthShiftTab(id, new caretMoveChecker(12, true, id)));
+ id = "p";
+ gQueue.push(new synthTab(id, new caretMoveChecker(0, true, id)));
+
+ // Set caret after a child of span element, i.e. after 'text' text.
+ gQueue.push(new moveCaretToDOMPoint("test1", getNode("test1_span"), 1,
+ 4, "test1"));
+ gQueue.push(new moveCaretToDOMPoint("test2", getNode("test2_span"), 1,
+ 4, "test2"));
+
+ // empty text element
+ gQueue.push(new moveCaretToDOMPoint("test3", getNode("test3"), 0,
+ 0, "test3"));
+ gQueue.push(new moveCaretToDOMPoint("test4", getNode("test4_span"), 0,
+ 0, "test4"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=454377"
+ title="Accessible caret move events testing">
+ Bug 454377
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=567571"
+ title="caret-moved events missing at the end of a wrapped line of text">
+ Bug 567571
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=824901"
+ title="HyperTextAccessible::DOMPointToHypertextOffset fails for node and offset equal to node child count">
+ Bug 824901
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox" value="hello"/>
+
+ <textarea id="textarea">text<br>text</textarea>
+ <p id="p" contentEditable="true"><span>text</span><br/>text</p>
+ <div id="div" contentEditable="true"><p id="p1_in_div">text</p><p id="p2_in_div">text</p></div>
+
+ <p contentEditable="true" id="test1"><span id="test1_span">text</span>ohoho</p>
+ <p contentEditable="true" id="test2"><span><span id="test2_span">text</span></span>ohoho</p>
+ <p contentEditable="true" id="test3"></p>
+ <p contentEditable="true" id="test4"><span id="test4_span"></span></p>
+
+ <textarea id="textarea_wrapped" cols="5">hey friend</textarea>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_coalescence.html b/accessible/tests/mochitest/events/test_coalescence.html
new file mode 100644
index 0000000000..0f8ad52a8b
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_coalescence.html
@@ -0,0 +1,817 @@
+<html>
+
+<head>
+ <title>Accessible mutation events coalescence testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invoker base classes
+
+ const kRemoveElm = 1;
+ const kHideElm = 2;
+ const kAddElm = 3;
+ const kShowElm = 4;
+
+ /**
+ * Base class to test of mutation events coalescence.
+ */
+ function coalescenceBase(aChildAction, aParentAction,
+ aPerformActionOnChildInTheFirstPlace) {
+ // Invoker interface
+
+ this.invoke = function coalescenceBase_invoke() {
+ if (aPerformActionOnChildInTheFirstPlace) {
+ this.invokeAction(this.childNode, aChildAction);
+ this.invokeAction(this.parentNode, aParentAction);
+ } else {
+ this.invokeAction(this.parentNode, aParentAction);
+ this.invokeAction(this.childNode, aChildAction);
+ }
+ };
+
+ this.getID = function coalescenceBase_getID() {
+ var childAction = this.getActionName(aChildAction) + " child";
+ var parentAction = this.getActionName(aParentAction) + " parent";
+
+ if (aPerformActionOnChildInTheFirstPlace)
+ return childAction + " and then " + parentAction;
+
+ return parentAction + " and then " + childAction;
+ };
+
+ this.finalCheck = function coalescenceBase_check() {
+ if (this.getEventType(aChildAction) == EVENT_HIDE) {
+ testIsDefunct(this.child);
+ }
+ if (this.getEventType(aParentAction) == EVENT_HIDE) {
+ testIsDefunct(this.parent);
+ }
+ };
+
+ // Implementation details
+
+ this.invokeAction = function coalescenceBase_invokeAction(aNode, aAction) {
+ switch (aAction) {
+ case kRemoveElm:
+ aNode.remove();
+ break;
+
+ case kHideElm:
+ aNode.style.display = "none";
+ break;
+
+ case kAddElm:
+ if (aNode == this.parentNode)
+ this.hostNode.appendChild(this.parentNode);
+ else
+ this.parentNode.appendChild(this.childNode);
+ break;
+
+ case kShowElm:
+ aNode.style.display = "block";
+ break;
+
+ default:
+ return INVOKER_ACTION_FAILED;
+ }
+ // 0 means the action succeeded.
+ return 0;
+ };
+
+ this.getEventType = function coalescenceBase_getEventType(aAction) {
+ switch (aAction) {
+ case kRemoveElm: case kHideElm:
+ return EVENT_HIDE;
+ case kAddElm: case kShowElm:
+ return EVENT_SHOW;
+ }
+ return 0;
+ };
+
+ this.getActionName = function coalescenceBase_getActionName(aAction) {
+ switch (aAction) {
+ case kRemoveElm:
+ return "remove";
+ case kHideElm:
+ return "hide";
+ case kAddElm:
+ return "add";
+ case kShowElm:
+ return "show";
+ default:
+ return "??";
+ }
+ };
+
+ this.initSequence = function coalescenceBase_initSequence() {
+ // expected events
+ var eventType = this.getEventType(aParentAction);
+ this.eventSeq = [
+ new invokerChecker(eventType, this.parentNode),
+ new invokerChecker(EVENT_REORDER, this.hostNode),
+ ];
+
+ // unexpected events
+ this.unexpectedEventSeq = [
+ new invokerChecker(this.getEventType(aChildAction), this.childNode),
+ new invokerChecker(EVENT_REORDER, this.parentNode),
+ ];
+ };
+ }
+
+ /**
+ * Remove or hide mutation events coalescence testing.
+ */
+ function removeOrHideCoalescenceBase(aChildID, aParentID,
+ aChildAction, aParentAction,
+ aPerformActionOnChildInTheFirstPlace) {
+ this.__proto__ = new coalescenceBase(aChildAction, aParentAction,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init = function removeOrHideCoalescenceBase_init() {
+ this.childNode = getNode(aChildID);
+ this.parentNode = getNode(aParentID);
+ this.child = getAccessible(this.childNode);
+ this.parent = getAccessible(this.parentNode);
+ this.hostNode = this.parentNode.parentNode;
+ };
+
+ // Initalization
+
+ this.init();
+ this.initSequence();
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Remove child node and then its parent node from DOM tree.
+ */
+ function removeChildNParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kRemoveElm,
+ true);
+ }
+
+ /**
+ * Remove parent node and then its child node from DOM tree.
+ */
+ function removeParentNChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kRemoveElm,
+ false);
+ }
+
+ /**
+ * Hide child node and then its parent node.
+ */
+ function hideChildNParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kHideElm,
+ true);
+ }
+
+ /**
+ * Hide parent node and then its child node.
+ */
+ function hideParentNChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kHideElm,
+ false);
+ }
+
+ /**
+ * Hide child node and then remove its parent node.
+ */
+ function hideChildNRemoveParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kRemoveElm,
+ true);
+ }
+
+ /**
+ * Hide parent node and then remove its child node.
+ */
+ function hideParentNRemoveChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kHideElm,
+ false);
+ }
+
+ /**
+ * Remove child node and then hide its parent node.
+ */
+ function removeChildNHideParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kHideElm,
+ true);
+ }
+
+ /**
+ * Remove parent node and then hide its child node.
+ */
+ function removeParentNHideChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kRemoveElm,
+ false);
+ }
+
+ /**
+ * Create and append parent node and create and append child node to it.
+ */
+ function addParentNChild(aHostID, aPerformActionOnChildInTheFirstPlace) {
+ this.init = function addParentNChild_init() {
+ this.hostNode = getNode(aHostID);
+ this.parentNode = document.createElement("select");
+ this.childNode = document.createElement("option");
+ this.childNode.textContent = "testing";
+ };
+
+ this.__proto__ = new coalescenceBase(kAddElm, kAddElm,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init();
+ this.initSequence();
+ }
+
+ /**
+ * Show parent node and show child node to it.
+ */
+ function showParentNChild(aParentID, aChildID,
+ aPerformActionOnChildInTheFirstPlace) {
+ this.init = function showParentNChild_init() {
+ this.parentNode = getNode(aParentID);
+ this.hostNode = this.parentNode.parentNode;
+ this.childNode = getNode(aChildID);
+ };
+
+ this.__proto__ = new coalescenceBase(kShowElm, kShowElm,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init();
+ this.initSequence();
+ }
+
+ /**
+ * Create and append child node to the DOM and then show parent node.
+ */
+ function showParentNAddChild(aParentID,
+ aPerformActionOnChildInTheFirstPlace) {
+ this.init = function showParentNAddChild_init() {
+ this.parentNode = getNode(aParentID);
+ this.hostNode = this.parentNode.parentNode;
+ this.childNode = document.createElement("option");
+ this.childNode.textContent = "testing";
+ };
+
+ this.__proto__ = new coalescenceBase(kAddElm, kShowElm,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init();
+ this.initSequence();
+ }
+
+ /**
+ * Remove children and parent
+ */
+ function removeGrandChildrenNHideParent(aChild1Id, aChild2Id, aParentId) {
+ this.child1 = getNode(aChild1Id);
+ this.child2 = getNode(aChild2Id);
+ this.parent = getNode(aParentId);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aParentId)),
+ new invokerChecker(EVENT_REORDER, getNode(aParentId).parentNode),
+ new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild1Id)),
+ new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild2Id)),
+ new unexpectedInvokerChecker(EVENT_REORDER, getAccessible(aParentId)),
+ ];
+
+ this.invoke = function removeGrandChildrenNHideParent_invoke() {
+ this.child1.remove();
+ this.child2.remove();
+ this.parent.hidden = true;
+ };
+
+ this.getID = function removeGrandChildrenNHideParent_getID() {
+ return "remove grand children of different parents and then hide their grand parent";
+ };
+ }
+
+ /**
+ * Remove a child, and then its parent.
+ */
+ function test3() {
+ this.o = getAccessible("t3_o");
+ this.ofc = getAccessible("t3_o").firstChild;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.o),
+ new invokerChecker(EVENT_REORDER, "t3_lb"),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.ofc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o),
+ ];
+
+ this.invoke = function test3_invoke() {
+ getNode("t3_o").textContent = "";
+ getNode("t3_lb").removeChild(getNode("t3_o"));
+ };
+
+ this.finalCheck = function test3_finalCheck() {
+ testIsDefunct(this.o);
+ testIsDefunct(this.ofc);
+ };
+
+ this.getID = function test3_getID() {
+ return "remove a child, and then its parent";
+ };
+ }
+
+ /**
+ * Remove children, and then a parent of 2nd child.
+ */
+ function test4() {
+ this.o1 = getAccessible("t4_o1");
+ this.o1fc = this.o1.firstChild;
+ this.o2 = getAccessible("t4_o2");
+ this.o2fc = this.o2.firstChild;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.o1fc),
+ new invokerChecker(EVENT_HIDE, this.o2),
+ new invokerChecker(EVENT_REORDER, "t4_lb"),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.o2fc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o1),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o2),
+ ];
+
+ this.invoke = function test4_invoke() {
+ getNode("t4_o1").textContent = "";
+ getNode("t4_o2").textContent = "";
+ getNode("t4_lb").removeChild(getNode("t4_o2"));
+ };
+
+ this.finalCheck = function test4_finalCheck() {
+ testIsDefunct(this.o1fc);
+ testIsDefunct(this.o2);
+ testIsDefunct(this.o2fc);
+ };
+
+ this.getID = function test4_getID() {
+ return "remove children, and then a parent of 2nd child";
+ };
+ }
+
+ /**
+ * Remove a child, remove a parent sibling, remove the parent
+ */
+ function test5() {
+ this.o = getAccessible("t5_o");
+ this.ofc = this.o.firstChild;
+ this.b = getAccessible("t5_b");
+ this.lb = getAccessible("t5_lb");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.b),
+ new invokerChecker(EVENT_HIDE, this.o),
+ new invokerChecker(EVENT_REORDER, "t5"),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.ofc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.lb),
+ ];
+
+ this.invoke = function test5_invoke() {
+ getNode("t5_o").textContent = "";
+ getNode("t5").removeChild(getNode("t5_b"));
+ getNode("t5_lb").removeChild(getNode("t5_o"));
+ };
+
+ this.finalCheck = function test5_finalCheck() {
+ testIsDefunct(this.ofc);
+ testIsDefunct(this.o);
+ testIsDefunct(this.b);
+ };
+
+ this.getID = function test5_getID() {
+ return "remove a child, remove a parent sibling, remove the parent";
+ };
+ }
+
+ /**
+ * Insert accessibles with a child node moved by aria-owns
+ * Markup:
+ * <div id="t6_fc">
+ * <div id="t6_owns"></div>
+ * </div>
+ * <div id="t6_sc" aria-owns="t6_owns"></div>
+ */
+ function test6() {
+ this.parent = getNode("t6");
+ this.fc = document.createElement("div");
+ this.fc.setAttribute("id", "t6_fc");
+ this.owns = document.createElement("div");
+ this.owns.setAttribute("id", "t6_owns");
+ this.sc = document.createElement("div");
+ this.sc.setAttribute("id", "t6_sc");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.fc),
+ new invokerChecker(EVENT_SHOW, this.sc),
+ new invokerChecker(EVENT_REORDER, this.parent),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.fc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.sc),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.owns),
+ new unexpectedInvokerChecker(EVENT_SHOW, this.owns),
+ ];
+
+ this.invoke = function test6_invoke() {
+ getNode("t6").appendChild(this.fc);
+ getNode("t6_fc").appendChild(this.owns);
+ getNode("t6").appendChild(this.sc);
+ getNode("t6_sc").setAttribute("aria-owns", "t6_owns");
+ };
+
+ this.getID = function test6_getID() {
+ return "Insert accessibles with a child node moved by aria-owns";
+ };
+ }
+
+ /**
+ * Insert text nodes under direct and grand children, and then hide
+ * their container by means of aria-owns.
+ *
+ * Markup:
+ * <div id="t7_moveplace" aria-owns="t7_c"></div>
+ * <div id="t7_c">
+ * <div id="t7_c_directchild">ha</div>
+ * <div><div id="t7_c_grandchild">ha</div></div>
+ * </div>
+ */
+ function test7() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t7_c")),
+ new invokerChecker(EVENT_SHOW, getNode("t7_c")),
+ new invokerChecker(EVENT_REORDER, getNode("t7")),
+ new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_directchild")),
+ new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_grandchild")),
+ new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_directchild").firstChild),
+ new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_grandchild").firstChild),
+ ];
+
+ this.invoke = function test7_invoke() {
+ getNode("t7_c_directchild").textContent = "ha";
+ getNode("t7_c_grandchild").textContent = "ha";
+ getNode("t7_moveplace").setAttribute("aria-owns", "t7_c");
+ };
+
+ this.getID = function test7_getID() {
+ return "Show child accessibles and then hide their container";
+ };
+ }
+
+ /**
+ * Move a node by aria-owns from right to left in the tree, so that
+ * the eventing looks this way:
+ * reorder for 't8_c1'
+ * hide for 't8_c1_child'
+ * show for 't8_c2_moved'
+ * reorder for 't8_c2'
+ * hide for 't8_c2_moved'
+ *
+ * The hide event should be delivered before the paired show event.
+ */
+ function test8() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t8_c1_child")),
+ new invokerChecker(EVENT_HIDE, "t8_c2_moved"),
+ new invokerChecker(EVENT_SHOW, "t8_c2_moved"),
+ new invokerChecker(EVENT_REORDER, "t8_c2"),
+ new invokerChecker(EVENT_REORDER, "t8_c1"),
+ ];
+
+ this.invoke = function test8_invoke() {
+ // Remove a node from 't8_c1' container to give the event tree a
+ // desired structure (the 't8_c1' container node goes first in the event
+ // tree)
+ getNode("t8_c1_child").remove();
+ // then move 't8_c2_moved' from 't8_c2' to 't8_c1'.
+ getNode("t8_c1").setAttribute("aria-owns", "t8_c2_moved");
+ };
+
+ this.getID = function test8_getID() {
+ return "Move a node by aria-owns to left within the tree";
+ };
+ }
+
+ /**
+ * Move 't9_c3_moved' node under 't9_c2_moved', and then move 't9_c2_moved'
+ * node by aria-owns (same as test10 but has different aria-owns
+ * ordering), the eventing looks same way as in test10:
+ * reorder for 't9_c1'
+ * hide for 't9_c1_child'
+ * show for 't9_c2_moved'
+ * reorder for 't9_c2'
+ * hide for 't9_c2_child'
+ * hide for 't9_c2_moved'
+ * reorder for 't9_c3'
+ * hide for 't9_c3_moved'
+ *
+ * The hide events for 't9_c2_moved' and 't9_c3_moved' should be delivered
+ * before the show event for 't9_c2_moved'.
+ */
+ function test9() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t9_c1_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t9_c2_child")),
+ new invokerChecker(EVENT_HIDE, "t9_c3_moved"),
+ new invokerChecker(EVENT_HIDE, "t9_c2_moved"),
+ new invokerChecker(EVENT_SHOW, "t9_c2_moved"),
+ new invokerChecker(EVENT_REORDER, "t9_c3"),
+ new invokerChecker(EVENT_REORDER, "t9_c2"),
+ new invokerChecker(EVENT_REORDER, "t9_c1"),
+ new unexpectedInvokerChecker(EVENT_SHOW, "t9_c3_moved"),
+ ];
+
+ this.invoke = function test9_invoke() {
+ // Remove child nodes from 't9_c1' and 't9_c2' containers to give
+ // the event tree a needed structure ('t9_c1' and 't9_c2' nodes go
+ // first in the event tree),
+ getNode("t9_c1_child").remove();
+ getNode("t9_c2_child").remove();
+ // then do aria-owns magic.
+ getNode("t9_c2_moved").setAttribute("aria-owns", "t9_c3_moved");
+ getNode("t9_c1").setAttribute("aria-owns", "t9_c2_moved");
+ };
+
+ this.getID = function test9_getID() {
+ return "Move node #1 by aria-owns and then move node #2 into node #1";
+ };
+ }
+
+ /**
+ * Move a node 't10_c3_moved' by aria-owns under a node 't10_c2_moved',
+ * moved by under 't10_1', so that the eventing looks this way:
+ * reorder for 't10_c1'
+ * hide for 't10_c1_child'
+ * show for 't10_c2_moved'
+ * reorder for 't10_c2'
+ * hide for 't10_c2_child'
+ * hide for 't10_c2_moved'
+ * reorder for 't10_c3'
+ * hide for 't10_c3_moved'
+ *
+ * The hide events for 't10_c2_moved' and 't10_c3_moved' should be delivered
+ * before the show event for 't10_c2_moved'.
+ */
+ function test10() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t10_c1_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t10_c2_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t10_c2_moved")),
+ new invokerChecker(EVENT_HIDE, getNode("t10_c3_moved")),
+ new invokerChecker(EVENT_SHOW, getNode("t10_c2_moved")),
+ new invokerChecker(EVENT_REORDER, "t10_c2"),
+ new invokerChecker(EVENT_REORDER, "t10_c1"),
+ new invokerChecker(EVENT_REORDER, "t10_c3"),
+ ];
+
+ this.invoke = function test10_invoke() {
+ // Remove child nodes from 't10_c1' and 't10_c2' containers to give
+ // the event tree a needed structure ('t10_c1' and 't10_c2' nodes go first
+ // in the event tree),
+ getNode("t10_c1_child").remove();
+ getNode("t10_c2_child").remove();
+ // then do aria-owns stuff.
+ getNode("t10_c1").setAttribute("aria-owns", "t10_c2_moved");
+ getNode("t10_c2_moved").setAttribute("aria-owns", "t10_c3_moved");
+ };
+
+ this.getID = function test10_getID() {
+ return "Move a node by aria-owns into a node moved by aria-owns to left within the tree";
+ };
+ }
+
+ /**
+ * Move a node by aria-owns from right to left in the tree, and then
+ * move its parent too by aria-owns. No hide event should be fired for
+ * original node.
+ */
+ function test11() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t11_c1_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t11_c2")),
+ new orderChecker(),
+ new asyncInvokerChecker(EVENT_SHOW, "t11_c2_child"),
+ new asyncInvokerChecker(EVENT_SHOW, "t11_c2"),
+ new orderChecker(),
+ new invokerChecker(EVENT_REORDER, "t11"),
+ new unexpectedInvokerChecker(EVENT_HIDE, "t11_c2_child"),
+ new unexpectedInvokerChecker(EVENT_REORDER, "t11_c1"),
+ new unexpectedInvokerChecker(EVENT_REORDER, "t11_c2"),
+ new unexpectedInvokerChecker(EVENT_REORDER, "t11_c3"),
+ ];
+
+ this.invoke = function test11_invoke() {
+ // Remove a node from 't11_c1' container to give the event tree a
+ // desired structure (the 't11_c1' container node goes first in
+ // the event tree),
+ getNode("t11_c1_child").remove();
+ // then move 't11_c2_moved' from 't11_c2' to 't11_c1', and then move
+ // 't11_c2' to 't11_c3'.
+ getNode("t11_c1").setAttribute("aria-owns", "t11_c2_child");
+ getNode("t11_c3").setAttribute("aria-owns", "t11_c2");
+ };
+
+ this.getID = function test11_getID() {
+ return "Move a node by aria-owns to left within the tree";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests.
+
+ gA11yEventDumpToConsole = true; // debug stuff
+ // enableLogging("eventTree");
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new removeChildNParent("option1", "select1"));
+ gQueue.push(new removeParentNChild("option2", "select2"));
+ gQueue.push(new hideChildNParent("option3", "select3"));
+ gQueue.push(new hideParentNChild("option4", "select4"));
+ gQueue.push(new hideChildNRemoveParent("option5", "select5"));
+ gQueue.push(new hideParentNRemoveChild("option6", "select6"));
+ gQueue.push(new removeChildNHideParent("option7", "select7"));
+ gQueue.push(new removeParentNHideChild("option8", "select8"));
+
+ gQueue.push(new addParentNChild("testContainer", false));
+ gQueue.push(new addParentNChild("testContainer", true));
+ gQueue.push(new showParentNChild("select9", "option9", false));
+ gQueue.push(new showParentNChild("select10", "option10", true));
+ gQueue.push(new showParentNAddChild("select11", false));
+ gQueue.push(new showParentNAddChild("select12", true));
+
+ gQueue.push(new removeGrandChildrenNHideParent("t1_child1", "t1_child2", "t1_parent"));
+ gQueue.push(new test3());
+ gQueue.push(new test4());
+ gQueue.push(new test5());
+ gQueue.push(new test6());
+ gQueue.push(new test7());
+ gQueue.push(new test8());
+ gQueue.push(new test9());
+ gQueue.push(new test10());
+ gQueue.push(new test11());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=513213"
+ title="coalesce events when new event is appended to the queue">
+ Mozilla Bug 513213
+ </a><br>
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer">
+ <select id="select1">
+ <option id="option1">option</option>
+ </select>
+ <select id="select2">
+ <option id="option2">option</option>
+ </select>
+ <select id="select3">
+ <option id="option3">option</option>
+ </select>
+ <select id="select4">
+ <option id="option4">option</option>
+ </select>
+ <select id="select5">
+ <option id="option5">option</option>
+ </select>
+ <select id="select6">
+ <option id="option6">option</option>
+ </select>
+ <select id="select7">
+ <option id="option7">option</option>
+ </select>
+ <select id="select8">
+ <option id="option8">option</option>
+ </select>
+
+ <select id="select9" style="display: none">
+ <option id="option9" style="display: none">testing</option>
+ </select>
+ <select id="select10" style="display: none">
+ <option id="option10" style="display: none">testing</option>
+ </select>
+ <select id="select11" style="display: none"></select>
+ <select id="select12" style="display: none"></select>
+ </div>
+
+ <div id="testContainer2">
+ <div id="t1_parent">
+ <div id="t1_mid1"><div id="t1_child1"></div></div>
+ <div id="t1_mid2"><div id="t1_child2"></div></div>
+ </div>
+ </div>
+
+ <div id="t3">
+ <div role="listbox" id="t3_lb">
+ <div role="option" id="t3_o">opt</div>
+ </div>
+ </div>
+
+ <div id="t4">
+ <div role="listbox" id="t4_lb">
+ <div role="option" id="t4_o1">opt1</div>
+ <div role="option" id="t4_o2">opt2</div>
+ </div>
+ </div>
+
+ <div id="t5">
+ <div role="button" id="t5_b">btn</div>
+ <div role="listbox" id="t5_lb">
+ <div role="option" id="t5_o">opt</div>
+ </div>
+ </div>
+
+ <div id="t6">
+ </div>
+
+ <div id="t7">
+ <div id="t7_moveplace"></div>
+ <div id="t7_c">
+ <div><div id="t7_c_grandchild"></div></div>
+ <div id="t7_c_directchild"></div>
+ </div>
+ </div>
+
+ <div id="t8">
+ <div id="t8_c1"><div id="t8_c1_child"></div></div>
+ <div id="t8_c2">
+ <div id="t8_c2_moved"></div>
+ </div>
+ </div>
+
+ <div id="t9">
+ <div id="t9_c1"><div id="t9_c1_child"></div></div>
+ <div id="t9_c2">
+ <div id="t9_c2_child"></div>
+ <div id="t9_c2_moved"></div>
+ </div>
+ <div id="t9_c3">
+ <div id="t9_c3_moved"></div>
+ </div>
+ </div>
+
+ <div id="t10">
+ <div id="t10_c1"><div id="t10_c1_child"></div></div>
+ <div id="t10_c2">
+ <div id="t10_c2_child"></div>
+ <div id="t10_c2_moved"></div>
+ </div>
+ <div id="t10_c3">
+ <div id="t10_c3_moved"></div>
+ </div>
+ </div>
+
+ <div id="t11">
+ <div id="t11_c1"><div id="t11_c1_child"></div></div>
+ <div id="t11_c2"><div id="t11_c2_child"></div></div>
+ <div id="t11_c3"></div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_contextmenu.html b/accessible/tests/mochitest/events/test_contextmenu.html
new file mode 100644
index 0000000000..6200efc00d
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_contextmenu.html
@@ -0,0 +1,133 @@
+<html>
+
+<head>
+ <title>Context menu tests</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function showContextMenu(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENUPOPUP_START, getContextMenuNode()),
+ ];
+
+ this.invoke = function showContextMenu_invoke() {
+ synthesizeMouse(this.DOMNode, 4, 4, { type: "contextmenu", button: 2 });
+ };
+
+ this.getID = function showContextMenu_getID() {
+ return "show context menu";
+ };
+ }
+
+ function selectMenuItem() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getFocusedMenuItem),
+ ];
+
+ this.invoke = function selectMenuItem_invoke() {
+ synthesizeKey("KEY_ArrowDown");
+ };
+
+ this.getID = function selectMenuItem_getID() {
+ return "select first menuitem";
+ };
+ }
+
+ function closeContextMenu(aID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENUPOPUP_END,
+ getAccessible(getContextMenuNode())),
+ ];
+
+ this.invoke = function closeContextMenu_invoke() {
+ synthesizeKey("KEY_Escape");
+ };
+
+ this.getID = function closeContextMenu_getID() {
+ return "close context menu";
+ };
+ }
+
+ function getContextMenuNode() {
+ return getRootAccessible().DOMDocument.
+ getElementById("contentAreaContextMenu");
+ }
+
+ function getFocusedMenuItem() {
+ var menu = getAccessible(getAccessible(getContextMenuNode()));
+ for (var idx = 0; idx < menu.childCount; idx++) {
+ var item = menu.getChildAt(idx);
+
+ if (hasState(item, STATE_FOCUSED))
+ return getAccessible(item);
+ }
+ return null;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new showContextMenu("input"));
+ gQueue.push(new selectMenuItem());
+ gQueue.push(new closeContextMenu());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ const {AppConstants} = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ if (AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
+ ok(true, "Native context menus handle accessibility notifications natively and cannot be tested with synthesized key events.");
+ } else {
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ }
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=580535"
+ title="Broken accessibility in context menus">
+ Mozilla Bug 580535
+ </a><br>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input">
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_descrchange.html b/accessible/tests/mochitest/events/test_descrchange.html
new file mode 100644
index 0000000000..1eaecd6b59
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_descrchange.html
@@ -0,0 +1,142 @@
+<html>
+
+<head>
+ <title>Accessible description change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function setAttr(aID, aAttr, aValue, aChecker) {
+ this.eventSeq = [ aChecker ];
+ this.invoke = function setAttr_invoke() {
+ getNode(aID).setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function setAttr_getID() {
+ return "set attr '" + aAttr + "', value '" + aValue + "'";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ async function doTests() {
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new setAttr("tst1", "aria-describedby", "display",
+ new invokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "title", "title",
+ new unexpectedInvokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1")));
+
+ // A title has lower priority over text content. There should be no name change event.
+ gQueue.push(new setAttr("tst2", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ const describedBy = getNode("describedBy");
+ const description = getNode("description");
+ let descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ describedBy
+ );
+ info("Changing text of aria-describedby target");
+ description.textContent = "d2";
+ await descChanged;
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ describedBy
+ );
+ info("Adding node to aria-describedby target");
+ description.innerHTML = '<p id="descriptionChild">d3</p>';
+ await descChanged;
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ describedBy
+ );
+ info("Changing text of aria-describedby target's child");
+ getNode("descriptionChild").textContent = "d4";
+ await descChanged;
+
+ const lateDescribedBy = getNode("lateDescribedBy");
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ lateDescribedBy
+ );
+ info("Setting aria-describedby");
+ lateDescribedBy.setAttribute("aria-describedby", "lateDescription");
+ await descChanged;
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ lateDescribedBy
+ );
+ info("Changing text of late aria-describedby target's child");
+ getNode("lateDescriptionChild").textContent = "d2";
+ await descChanged;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969"
+ title="Event not fired when description changes">
+ Bug 991969
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="tst1">btn1</button>
+ <button id="tst2">btn2</button>
+
+ <div id="describedBy" aria-describedby="description"></div>
+ <div id="description">d1</div>
+
+ <div id="lateDescribedBy"></div>
+ <div id="lateDescription"><p id="lateDescriptionChild">d1</p></div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_dragndrop.html b/accessible/tests/mochitest/events/test_dragndrop.html
new file mode 100644
index 0000000000..2613a310a2
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_dragndrop.html
@@ -0,0 +1,106 @@
+<html>
+
+<head>
+ <title>Accessible drag and drop event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // aria grabbed invoker
+ function changeGrabbed(aNodeOrID, aGrabValue) {
+ this.DOMNode = getNode(aNodeOrID);
+
+ this.invoke = function changeGrabbed_invoke() {
+ if (aGrabValue != undefined) {
+ this.DOMNode.setAttribute("aria-grabbed", aGrabValue);
+ }
+ };
+
+ this.check = function changeGrabbed_check() {
+ testAttrs(aNodeOrID, {"grabbed": aGrabValue}, true);
+ };
+
+ this.getID = function changeGrabbed_getID() {
+ return prettyName(aNodeOrID) + " aria-grabbed changed";
+ };
+ }
+
+ // aria dropeffect invoker
+ function changeDropeffect(aNodeOrID, aDropeffectValue) {
+ this.DOMNode = getNode(aNodeOrID);
+
+ this.invoke = function changeDropeffect_invoke() {
+ if (aDropeffectValue != undefined) {
+ this.DOMNode.setAttribute("aria-dropeffect", aDropeffectValue);
+ }
+ };
+
+ this.check = function changeDropeffect_check() {
+ testAttrs(aNodeOrID, {"dropeffect": aDropeffectValue}, true);
+ };
+
+ this.getID = function changeDropeffect_getID() {
+ return prettyName(aNodeOrID) + " aria-dropeffect changed";
+ };
+ }
+
+ function doTests() {
+ // Test aria attribute mutation events
+ gQueue = new eventQueue(nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED);
+
+ let id = "grabbable";
+ gQueue.push(new changeGrabbed(id, "true"));
+ gQueue.push(new changeGrabbed(id, "false"));
+ todo(false, "uncomment this test when 472142 is fixed.");
+ // gQueue.push(new changeGrabbed(id, "undefined"));
+
+ id = "dropregion";
+ gQueue.push(new changeDropeffect(id, "copy"));
+ gQueue.push(new changeDropeffect(id, "execute"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=510441"
+ title="Add support for nsIAccessibleEvent::OBJECT_ATTRIBUTE_CHANGED">
+ Mozilla Bug 510441
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- ARIA grabbed -->
+ <div id="grabbable" role="button" aria-grabbed="foo">button</div>
+
+ <!-- ARIA dropeffect -->
+ <div id="dropregion" role="region" aria-dropeffect="none">button</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_flush.html b/accessible/tests/mochitest/events/test_flush.html
new file mode 100644
index 0000000000..7d7b60b81e
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_flush.html
@@ -0,0 +1,74 @@
+<html>
+
+<head>
+ <title>Flush delayed events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ SimpleTest.expectAssertions(0, 1);
+
+ var gFocusHandler = {
+ handleEvent(aEvent) {
+ switch (this.count) {
+ case 0:
+ is(aEvent.DOMNode, getNode("input1"),
+ "Focus event for input1 was expected!");
+ getAccessible("input2").takeFocus();
+ break;
+
+ case 1:
+ is(aEvent.DOMNode, getNode("input2"),
+ "Focus event for input2 was expected!");
+
+ unregisterA11yEventListener(EVENT_FOCUS, gFocusHandler);
+ SimpleTest.finish();
+ break;
+
+ default:
+ ok(false, "Wrong focus event!");
+ }
+
+ this.count++;
+ },
+
+ count: 0,
+ };
+
+ function doTests() {
+ registerA11yEventListener(EVENT_FOCUS, gFocusHandler);
+
+ getAccessible("input1").takeFocus();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=477551"
+ title="DocAccessible::FlushPendingEvents isn't robust">
+ Mozilla Bug 477551
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input1">
+ <input id="input2">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html
new file mode 100644
index 0000000000..661284619a
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html
@@ -0,0 +1,327 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=429547
+-->
+<head>
+ <title>aria-activedescendant focus tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+ // gA11yEventDumpToConsole = true; // debugging
+
+ function changeARIAActiveDescendant(aContainer, aItem, aPrevItemId) {
+ let itemID = Node.isInstance(aItem) ? aItem.id : aItem;
+ this.eventSeq = [new focusChecker(aItem)];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aItem)
+ );
+
+ this.invoke = function changeARIAActiveDescendant_invoke() {
+ getNode(aContainer).setAttribute("aria-activedescendant", itemID);
+ };
+
+ this.getID = function changeARIAActiveDescendant_getID() {
+ return "change aria-activedescendant on " + itemID;
+ };
+ }
+
+ function clearARIAActiveDescendant(aID, aPrevItemId) {
+ this.eventSeq = [
+ new focusChecker(aID),
+ ];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.invoke = function clearARIAActiveDescendant_invoke() {
+ getNode(aID).removeAttribute("aria-activedescendant");
+ };
+
+ this.getID = function clearARIAActiveDescendant_getID() {
+ return "clear aria-activedescendant on container " + aID;
+ };
+ }
+
+ /**
+ * Change aria-activedescendant to an invalid (non-existent) id.
+ * Ensure that focus is fired on the element itself.
+ */
+ function changeARIAActiveDescendantInvalid(aID, aInvalidID, aPrevItemId) {
+ if (!aInvalidID) {
+ aInvalidID = "invalid";
+ }
+
+ this.eventSeq = [
+ new focusChecker(aID),
+ ];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.invoke = function changeARIAActiveDescendant_invoke() {
+ getNode(aID).setAttribute("aria-activedescendant", aInvalidID);
+ };
+
+ this.getID = function changeARIAActiveDescendant_getID() {
+ return "change aria-activedescendant to invalid id";
+ };
+ }
+
+ function insertItemNFocus(aID, aNewItemID, aPrevItemId) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, aNewItemID),
+ new focusChecker(aNewItemID),
+ ];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aNewItemID)
+ );
+
+ this.invoke = function insertItemNFocus_invoke() {
+ var container = getNode(aID);
+
+ var itemNode = document.createElement("div");
+ itemNode.setAttribute("id", aNewItemID);
+ itemNode.setAttribute("role", "listitem");
+ itemNode.textContent = aNewItemID;
+ container.appendChild(itemNode);
+
+ container.setAttribute("aria-activedescendant", aNewItemID);
+ };
+
+ this.getID = function insertItemNFocus_getID() {
+ return "insert new node and focus it with ID: " + aNewItemID;
+ };
+ }
+
+ /**
+ * Change the id of an element to another id which is the target of
+ * aria-activedescendant.
+ * If another element already has the desired id, remove it from that
+ * element first.
+ * Ensure that focus is fired on the target element which was given the
+ * desired id.
+ * @param aFromID The existing id of the target element.
+ * @param aToID The desired id to be given to the target element.
+ */
+ function moveARIAActiveDescendantID(aFromID, aToID) {
+ this.eventSeq = [
+ new focusChecker(aToID),
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aToID),
+ ];
+
+ this.invoke = function moveARIAActiveDescendantID_invoke() {
+ let orig = document.getElementById(aToID);
+ if (orig) {
+ orig.id = "";
+ }
+ document.getElementById(aFromID).id = aToID;
+ };
+
+ this.getID = function moveARIAActiveDescendantID_getID() {
+ return "move aria-activedescendant id " + aToID;
+ };
+ }
+
+ var gQueue = null;
+ async function doTest() {
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new synthFocus("listbox", new focusChecker("item1")));
+ gQueue.push(new changeARIAActiveDescendant("listbox", "item2", "item1"));
+ gQueue.push(new changeARIAActiveDescendant("listbox", "item3", "item2"));
+
+ gQueue.push(new synthFocus("combobox_entry", new focusChecker("combobox_entry")));
+ gQueue.push(new changeARIAActiveDescendant("combobox", "combobox_option2"));
+
+ gQueue.push(new synthFocus("listbox", new focusChecker("item3")));
+ gQueue.push(new insertItemNFocus("listbox", "item4", "item3"));
+
+ gQueue.push(new clearARIAActiveDescendant("listbox", "item4"));
+ gQueue.push(new changeARIAActiveDescendant("listbox", "item1"));
+ gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "invalid", "item1"));
+
+ gQueue.push(new changeARIAActiveDescendant("listbox", "roaming"));
+ gQueue.push(new moveARIAActiveDescendantID("roaming2", "roaming"));
+ gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "roaming3", "roaming"));
+ gQueue.push(new moveARIAActiveDescendantID("roaming", "roaming3"));
+
+ gQueue.push(new synthFocus("activedesc_nondesc_input",
+ new focusChecker("activedesc_nondesc_option")));
+
+ let shadowRoot = document.getElementById("shadow").shadowRoot;
+ let shadowListbox = shadowRoot.getElementById("shadowListbox");
+ let shadowItem1 = shadowRoot.getElementById("shadowItem1");
+ let shadowItem2 = shadowRoot.getElementById("shadowItem2");
+ gQueue.push(new synthFocus(shadowListbox, new focusChecker(shadowItem1)));
+ gQueue.push(new changeARIAActiveDescendant(shadowListbox, shadowItem2));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ info("Testing simultaneous insertion, relocation and aria-activedescendant");
+ let comboboxWithHiddenList = getNode("comboboxWithHiddenList");
+ let evtProm = PromEvents.waitForEvent(EVENT_FOCUS, comboboxWithHiddenList);
+ comboboxWithHiddenList.focus();
+ await evtProm;
+ testStates(comboboxWithHiddenList, STATE_FOCUSED);
+ // hiddenList is owned, so unhiding causes insertion and relocation.
+ getNode("hiddenList").hidden = false;
+ evtProm = Promise.all([
+ PromEvents.waitForEvent(EVENT_FOCUS, "hiddenListOption"),
+ PromEvents.waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true),
+ ]);
+ comboboxWithHiddenList.setAttribute("aria-activedescendant", "hiddenListOption");
+ await evtProm;
+ testStates("hiddenListOption", STATE_FOCUSED);
+
+ info("Testing active state changes when not focused");
+ testStates("listbox", 0, 0, STATE_FOCUSED);
+ evtProm = Promise.all([
+ PromEvents.waitForStateChange("roaming3", EXT_STATE_ACTIVE, false, true),
+ PromEvents.waitForStateChange("item1", EXT_STATE_ACTIVE, true, true),
+ ]);
+ getNode("listbox").setAttribute("aria-activedescendant", "item1");
+ await evtProm;
+
+ info("Testing that focus is always fired first");
+ const listbox = getNode("listbox");
+ evtProm = PromEvents.waitForEvent(EVENT_FOCUS, "item1");
+ listbox.focus();
+ await evtProm;
+ const item1 = getNode("item1");
+ evtProm = PromEvents.waitForOrderedEvents([
+ [EVENT_FOCUS, "item2"],
+ [EVENT_NAME_CHANGE, item1],
+ ], "Focus then name change");
+ item1.setAttribute("aria-label", "changed");
+ listbox.setAttribute("aria-activedescendant", "item2");
+ await evtProm;
+
+ info("Setting aria-activedescendant to invalid id on non-focused node");
+ const combobox_entry = getNode("combobox_entry");
+ evtProm = PromEvents.waitForEvents({
+ expected: [[EVENT_FOCUS, combobox_entry]],
+ unexpected: [[EVENT_FOCUS, listbox]],
+ });
+ combobox_entry.focus();
+ listbox.setAttribute("aria-activedescendant", "invalid");
+ await evtProm;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=429547"
+ title="Support aria-activedescendant usage in nsIAccesible::TakeFocus()">
+ Mozilla Bug 429547
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=761102"
+ title="Focus may be missed when ARIA active-descendant is changed on active composite widget">
+ Mozilla Bug 761102
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1"
+ aria-owns="item3">
+ <div role="listitem" id="item1">item1</div>
+ <div role="listitem" id="item2">item2</div>
+ <div role="listitem" id="roaming">roaming</div>
+ <div role="listitem" id="roaming2">roaming2</div>
+ </div>
+ <div role="listitem" id="item3">item3</div>
+
+ <div role="combobox" id="combobox">
+ <input id="combobox_entry">
+ <ul>
+ <li role="option" id="combobox_option1">option1</li>
+ <li role="option" id="combobox_option2">option2</li>
+ </ul>
+ </div>
+
+ <!-- aria-activedescendant targeting a non-descendant -->
+ <input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option">
+ <div role="listbox">
+ <div role="option" id="activedesc_nondesc_option">option</div>
+ </div>
+
+ <div id="shadow"></div>
+ <script>
+ let host = document.getElementById("shadow");
+ let shadow = host.attachShadow({mode: "open"});
+ let listbox = document.createElement("div");
+ listbox.id = "shadowListbox";
+ listbox.setAttribute("role", "listbox");
+ listbox.setAttribute("tabindex", "0");
+ shadow.appendChild(listbox);
+ let item = document.createElement("div");
+ item.id = "shadowItem1";
+ item.setAttribute("role", "option");
+ listbox.appendChild(item);
+ listbox.setAttribute("aria-activedescendant", "shadowItem1");
+ item = document.createElement("div");
+ item.id = "shadowItem2";
+ item.setAttribute("role", "option");
+ listbox.appendChild(item);
+ </script>
+
+ <div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList">
+ </div>
+ <div id="hiddenList" hidden role="listbox">
+ <div id="hiddenListOption" role="option"></div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.html b/accessible/tests/mochitest/events/test_focus_autocomplete.html
new file mode 100644
index 0000000000..c7fdbbb6a5
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_autocomplete.html
@@ -0,0 +1,83 @@
+<!doctype html>
+
+<head>
+ <title>Form Autocomplete Tests</title>
+
+ <link rel="stylesheet"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../promisified-events.js"></script>
+ <script src="../role.js"></script>
+
+ <script type="application/javascript">
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs");
+
+ async function waitForFocusOnOptionWithname(name) {
+ let event = await waitForEvent(
+ EVENT_FOCUS,
+ evt => evt.accessible.role == ROLE_COMBOBOX_OPTION
+ );
+ while (!event.accessible.name) {
+ // Sometimes, the name is null for a very short time after the focus
+ // event.
+ await waitForEvent(EVENT_NAME_CHANGE, event.accessible);
+ }
+ is(event.accessible.name, name, "Got focus on option with name " + name);
+ }
+
+ async function doTests() {
+ const input = getNode("input");
+ info("Focusing the input");
+ let focused = waitForEvent(EVENT_FOCUS, input);
+ input.focus();
+ await focused;
+
+ let shown = waitForEvent(EVENT_SHOW, event =>
+ event.accessible.role == ROLE_GROUPING &&
+ event.accessible.firstChild.role == ROLE_COMBOBOX_LIST);
+ info("Pressing ArrowDown to open the popup");
+ synthesizeKey("KEY_ArrowDown");
+ await shown;
+ // The popup still doesn't seem to be ready even once it's fired an a11y
+ // show event!
+ const controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ info("Waiting for popup to be fully open and ready");
+ await TestUtils.waitForCondition(() => controller.input.popupOpen);
+
+ focused = waitForFocusOnOptionWithname("a");
+ info("Pressing ArrowDown to focus first item");
+ synthesizeKey("KEY_ArrowDown");
+ await focused;
+
+ focused = waitForFocusOnOptionWithname("b");
+ info("Pressing ArrowDown to focus the second item");
+ synthesizeKey("KEY_ArrowDown");
+ await focused;
+
+ focused = waitForEvent(EVENT_FOCUS, input);
+ info("Pressing enter to select the second item");
+ synthesizeKey("KEY_Enter");
+ await focused;
+ is(input.value, "b", "input value filled with second item");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<body>
+ <input id="input" list="list">
+ <datalist id="list">
+ <option id="a" value="a">
+ <option id="b" value="b">
+ </datalist>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml
new file mode 100644
index 0000000000..69cdac14c5
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml
@@ -0,0 +1,507 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<!-- Firefox searchbar -->
+<?xml-stylesheet href="chrome://browser/content/browser.css"
+ type="text/css"?>
+<!-- SeaMonkey searchbar -->
+<?xml-stylesheet href="chrome://navigator/content/navigator.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessible focus event testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript"
+ src="../autocomplete.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Hacky stuffs
+
+ // This is the hacks needed to use a searchbar without browser.js.
+ var BrowserSearch = {
+ updateOpenSearchBadge() {}
+ };
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function loadFormAutoComplete(aIFrameID)
+ {
+ this.iframeNode = getNode(aIFrameID);
+ this.iframe = getAccessible(aIFrameID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.iframe)
+ ];
+
+ this.invoke = function loadFormAutoComplete_invoke()
+ {
+ var url = "data:text/html,<html><body><form id='form'>" +
+ "<input id='input' name='a11ytest-formautocomplete'>" +
+ "</form></body></html>";
+ this.iframeNode.setAttribute("src", url);
+ }
+
+ this.getID = function loadFormAutoComplete_getID()
+ {
+ return "load form autocomplete page";
+ }
+ }
+
+ function initFormAutoCompleteBy(aIFrameID, aAutoCompleteValue)
+ {
+ this.iframe = getAccessible(aIFrameID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.iframe)
+ ];
+
+ this.invoke = function initFormAutoCompleteBy_invoke()
+ {
+ var iframeDOMDoc = getIFrameDOMDoc(aIFrameID);
+
+ var inputNode = iframeDOMDoc.getElementById("input");
+ inputNode.value = aAutoCompleteValue;
+ var formNode = iframeDOMDoc.getElementById("form");
+ formNode.submit();
+ }
+
+ this.getID = function initFormAutoCompleteBy_getID()
+ {
+ return "init form autocomplete by '" + aAutoCompleteValue + "'";
+ }
+ }
+
+ function loadHTML5ListAutoComplete(aIFrameID)
+ {
+ this.iframeNode = getNode(aIFrameID);
+ this.iframe = getAccessible(aIFrameID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.iframe)
+ ];
+
+ this.invoke = function loadHTML5ListAutoComplete_invoke()
+ {
+ var url = "data:text/html,<html><body>" +
+ "<datalist id='cities'><option>hello</option><option>hi</option></datalist>" +
+ "<input id='input' list='cities'>" +
+ "</body></html>";
+ this.iframeNode.setAttribute("src", url);
+ }
+
+ this.getID = function loadHTML5ListAutoComplete_getID()
+ {
+ return "load HTML5 list autocomplete page";
+ }
+ }
+
+ function removeChar(aID, aCheckerOrEventSeq)
+ {
+ this.__proto__ = new synthAction(aID, aCheckerOrEventSeq);
+
+ this.invoke = function removeChar_invoke()
+ {
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ synthesizeKey("KEY_Delete");
+ }
+
+ this.getID = function removeChar_getID()
+ {
+ return "remove char on " + prettyName(aID);
+ }
+ }
+
+ function replaceOnChar(aID, aChar, aCheckerOrEventSeq)
+ {
+ this.__proto__ = new synthAction(aID, aCheckerOrEventSeq);
+
+ this.invoke = function replaceOnChar_invoke()
+ {
+ this.DOMNode.select();
+ sendString(aChar);
+ }
+
+ this.getID = function replaceOnChar_getID()
+ {
+ return "replace on char '" + aChar + "' for" + prettyName(aID);
+ }
+ }
+
+ function focusOnMouseOver(aIDFunc, aIDFuncArg)
+ {
+ this.eventSeq = [ new focusChecker(aIDFunc, aIDFuncArg) ];
+
+ this.invoke = function focusOnMouseOver_invoke()
+ {
+ this.id = aIDFunc(aIDFuncArg);
+ this.node = getNode(this.id);
+ this.window = this.node.ownerGlobal;
+
+ if (this.node.localName == "tree") {
+ var tree = getAccessible(this.node);
+ var accessible = getAccessible(this.id);
+ if (tree != accessible) {
+ var itemX = {}, itemY = {}, treeX = {}, treeY = {};
+ accessible.getBounds(itemX, itemY, {}, {});
+ tree.getBounds(treeX, treeY, {}, {});
+ this.x = itemX.value - treeX.value;
+ this.y = itemY.value - treeY.value;
+ }
+ }
+
+ // Generate mouse move events in timeouts until autocomplete popup list
+ // doesn't have it, the reason is do that because autocomplete popup
+ // ignores mousemove events firing in too short range.
+ synthesizeMouse(this.node, this.x, this.y, { type: "mousemove" });
+ this.doMouseMoveFlood(this);
+ }
+
+ this.finalCheck = function focusOnMouseOver_getID()
+ {
+ this.isFlooding = false;
+ }
+
+ this.getID = function focusOnMouseOver_getID()
+ {
+ return "mouse over on " + prettyName(aIDFunc(aIDFuncArg));
+ }
+
+ this.doMouseMoveFlood = function focusOnMouseOver_doMouseMoveFlood(aThis)
+ {
+ synthesizeMouse(aThis.node, aThis.x + 1, aThis.y + 1,
+ { type: "mousemove" }, aThis.window);
+
+ if (aThis.isFlooding)
+ aThis.window.setTimeout(aThis.doMouseMoveFlood, 0, aThis);
+ }
+
+ this.id = null;
+ this.node = null;
+ this.window = null;
+
+ this.isFlooding = true;
+ this.x = 1;
+ this.y = 1;
+ }
+
+ function selectByClick(aIDFunc, aIDFuncArg,
+ aFocusTargetFunc, aFocusTargetFuncArg)
+ {
+ this.eventSeq = [ new focusChecker(aFocusTargetFunc, aFocusTargetFuncArg) ];
+
+ this.invoke = function selectByClick_invoke()
+ {
+ var id = aIDFunc(aIDFuncArg);
+ var node = getNode(id);
+ var targetWindow = node.ownerGlobal;
+
+ if (node.localName == "tree") {
+ var tree = getAccessible(node);
+ var accessible = getAccessible(id);
+ if (tree != accessible) {
+ var itemX = {}, itemY = {}, treeX = {}, treeY = {};
+ accessible.getBounds(itemX, itemY, {}, {});
+ tree.getBounds(treeX, treeY, {}, {});
+ this.x = itemX.value - treeX.value;
+ this.y = itemY.value - treeY.value;
+ }
+ }
+
+ synthesizeMouseAtCenter(node, {}, targetWindow);
+ }
+
+ this.getID = function selectByClick_getID()
+ {
+ return "select by click " + prettyName(aIDFunc(aIDFuncArg));
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Target getters
+
+ function getItem(aItemObj)
+ {
+ var autocompleteNode = aItemObj.autocompleteNode;
+
+ // XUL searchbar
+ if (autocompleteNode.localName == "searchbar") {
+ let popupNode = autocompleteNode._popup;
+ if (popupNode) {
+ let list = getAccessible(popupNode);
+ return list.getChildAt(aItemObj.index);
+ }
+ }
+
+ // XUL autocomplete
+ let popupNode = autocompleteNode.popup;
+ if (!popupNode) {
+ // HTML form autocomplete
+ var controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ popupNode = controller.input.popup;
+ }
+
+ if (popupNode) {
+ if ("richlistbox" in popupNode) {
+ let list = getAccessible(popupNode.richlistbox);
+ return list.getChildAt(aItemObj.index);
+ }
+
+ let list = getAccessible(popupNode.tree);
+ return list.getChildAt(aItemObj.index + 1);
+ }
+ return null;
+ }
+
+ function getTextEntry(aID)
+ {
+ // For form autocompletes the autocomplete widget and text entry widget
+ // is the same widget, for XUL autocompletes the text entry is a first
+ // child.
+ var localName = getNode(aID).localName;
+
+ // HTML form autocomplete
+ if (localName == "input")
+ return getAccessible(aID);
+
+ // XUL searchbar
+ if (localName == "searchbar")
+ return getAccessible(getNode(aID).textbox);
+
+ return null;
+ }
+
+ function itemObj(aID, aIdx)
+ {
+ this.autocompleteNode = getNode(aID);
+
+ this.autocomplete = this.autocompleteNode.localName == "searchbar" ?
+ getAccessible(this.autocompleteNode.textbox) :
+ getAccessible(this.autocompleteNode);
+
+ this.index = aIdx;
+ }
+
+ function getIFrameDOMDoc(aIFrameID)
+ {
+ return getNode(aIFrameID).contentDocument;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test helpers
+
+ function queueAutoCompleteTests(aID)
+ {
+ // focus autocomplete text entry
+ gQueue.push(new synthFocus(aID, new focusChecker(getTextEntry, aID)));
+
+ // open autocomplete popup
+ gQueue.push(new synthDownKey(aID, new nofocusChecker()));
+
+ // select second option ('hi' option), focus on it
+ gQueue.push(new synthUpKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 1))));
+
+ // choose selected option, focus on text entry
+ gQueue.push(new synthEnterKey(aID, new focusChecker(getTextEntry, aID)));
+
+ // remove char, autocomplete popup appears
+ gQueue.push(new removeChar(aID, new nofocusChecker()));
+
+ // select first option ('hello' option), focus on it
+ gQueue.push(new synthDownKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 0))));
+
+ // mouse move on second option ('hi' option), focus on it
+ gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 1)));
+
+ // autocomplete popup updated (no selected item), focus on textentry
+ gQueue.push(new synthKey(aID, "e", null, new focusChecker(getTextEntry, aID)));
+
+ // select first option ('hello' option), focus on it
+ gQueue.push(new synthDownKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 0))));
+
+ // popup gets hidden, focus on textentry
+ gQueue.push(new synthRightKey(aID, new focusChecker(getTextEntry, aID)));
+
+ // popup gets open, no focus
+ gQueue.push(new synthOpenComboboxKey(aID, new nofocusChecker()));
+
+ // select first option again ('hello' option), focus on it
+ gQueue.push(new synthDownKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 0))));
+
+ // no option is selected, focus on text entry
+ gQueue.push(new synthUpKey(aID, new focusChecker(getTextEntry, aID)));
+
+ // close popup, no focus
+ gQueue.push(new synthEscapeKey(aID, new nofocusChecker()));
+
+ // autocomplete popup appears (no selected item), focus stays on textentry
+ gQueue.push(new replaceOnChar(aID, "h", new nofocusChecker()));
+
+ // mouse move on first option ('hello' option), focus on it
+ gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 0)));
+
+ // click first option ('hello' option), popup closes, focus on text entry
+ gQueue.push(new selectByClick(getItem, new itemObj(aID, 0), getTextEntry, aID));
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gInitQueue = null;
+ function initTests()
+ {
+ if (SEAMONKEY || MAC) {
+ todo(false, "Skipping this test on SeaMonkey ftb. (Bug 718237), and on Mac (bug 746177)");
+ shutdownAutoComplete();
+ SimpleTest.finish();
+ return;
+ }
+
+ gInitQueue = new eventQueue();
+ gInitQueue.push(new loadFormAutoComplete("iframe"));
+ gInitQueue.push(new initFormAutoCompleteBy("iframe", "hello"));
+ gInitQueue.push(new initFormAutoCompleteBy("iframe", "hi"));
+ gInitQueue.push(new loadHTML5ListAutoComplete("iframe2"));
+ gInitQueue.onFinish = function initQueue_onFinish()
+ {
+ SimpleTest.executeSoon(doTests);
+ return DO_NOT_FINISH_TEST;
+ }
+ gInitQueue.invoke();
+ }
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // tree popup autocomplete tests
+ queueAutoCompleteTests("autocomplete");
+
+ ////////////////////////////////////////////////////////////////////////////
+ // richlistbox popup autocomplete tests
+ queueAutoCompleteTests("richautocomplete");
+
+ ////////////////////////////////////////////////////////////////////////////
+ // HTML form autocomplete tests
+ queueAutoCompleteTests(getIFrameDOMDoc("iframe").getElementById("input"));
+
+ ////////////////////////////////////////////////////////////////////////////
+ // HTML5 list autocomplete tests
+ queueAutoCompleteTests(getIFrameDOMDoc("iframe2").getElementById("input"));
+
+ ////////////////////////////////////////////////////////////////////////////
+ // searchbar tests
+
+ // focus searchbar, focus on text entry
+ gQueue.push(new synthFocus("searchbar",
+ new focusChecker(getTextEntry, "searchbar")));
+ // open search engine popup, no focus
+ gQueue.push(new synthOpenComboboxKey("searchbar", new nofocusChecker()));
+ // select first item, focus on it
+ gQueue.push(new synthDownKey("searchbar",
+ new focusChecker(getItem, new itemObj("searchbar", 0))));
+ // mouse over on second item, focus on it
+ gQueue.push(new focusOnMouseOver(getItem, new itemObj("searchbar", 1)));
+ // press enter key, focus on text entry
+ gQueue.push(new synthEnterKey("searchbar",
+ new focusChecker(getTextEntry, "searchbar")));
+ // click on search button, open popup, focus goes to document
+ var searchBtn = getAccessible(getNode("searchbar").searchButton);
+ gQueue.push(new synthClick(searchBtn, new focusChecker(document)));
+ // select first item, focus on it
+ gQueue.push(new synthDownKey("searchbar",
+ new focusChecker(getItem, new itemObj("searchbar", 0))));
+ // close popup, focus goes on document
+ gQueue.push(new synthEscapeKey("searchbar", new focusChecker(document)));
+
+ gQueue.onFinish = function()
+ {
+ // unregister 'test-a11y-search' autocomplete search
+ shutdownAutoComplete();
+ }
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Register 'test-a11y-search' autocomplete search.
+ // XPFE AutoComplete needs to register early.
+ initAutoComplete([ "hello", "hi" ],
+ [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]);
+
+ addA11yLoadEvent(initTests);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=383759"
+ title="Focus event inconsistent for search box autocomplete">
+ Mozilla Bug 383759
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766"
+ title="Add accessibility support for @list on HTML input and for HTML datalist">
+ Mozilla Bug 559766
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <html:input is="autocomplete-input"
+ id="autocomplete"
+ autocompletesearch="test-a11y-search"/>
+
+ <html:input is="autocomplete-input"
+ id="richautocomplete"
+ autocompletesearch="test-a11y-search"
+ autocompletepopup="richpopup"/>
+ <panel is="autocomplete-richlistbox-popup"
+ id="richpopup"
+ type="autocomplete-richlistbox"
+ noautofocus="true"/>
+
+ <iframe id="iframe"/>
+
+ <iframe id="iframe2"/>
+
+ <searchbar id="searchbar"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_canvas.html b/accessible/tests/mochitest/events/test_focus_canvas.html
new file mode 100644
index 0000000000..e2464e41a6
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_canvas.html
@@ -0,0 +1,58 @@
+<html>
+
+<head>
+ <title>Accessible focus testing in canvas subdom</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("button"));
+ gQueue.push(new synthTab("button", new focusChecker("textbox")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="Expose content in Canvas element"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912">
+ Mozilla Bug 495912
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <canvas>
+ <input id="button" type="button">
+ <input id="textbox">
+ </canvas>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml
new file mode 100644
index 0000000000..a0c92212dc
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Context menu focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var winLowerThanVista = navigator.platform.indexOf("Win") == 0;
+ if (winLowerThanVista) {
+ var version = Services.sysinfo.getProperty("version");
+ version = parseFloat(version);
+ winLowerThanVista = !(version >= 6.0);
+ }
+
+ var gQueue = null;
+ function doTests()
+ {
+ // bug 746183 - Whole file times out on OS X
+ if (MAC || winLowerThanVista) {
+ todo(false, "Reenable on mac after fixing bug 746183!");
+ SimpleTest.finish();
+ return;
+ }
+
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("button"));
+ gQueue.push(new synthContextMenu("button", [
+ new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu"),
+ new invokerChecker("popupshown", "contextmenu"),
+ ]));
+ gQueue.push(new synthDownKey("button", new focusChecker("item1")));
+ gQueue.push(new synthEscapeKey("contextmenu", new focusChecker("button")));
+
+ gQueue.push(new synthContextMenu("button",
+ new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu")));
+ gQueue.push(new synthDownKey("contextmenu", new focusChecker("item1")));
+ gQueue.push(new synthDownKey("item1", new focusChecker("item2")));
+ gQueue.push(new synthRightKey("item2", new focusChecker("item2.1")));
+ if (WIN) {
+ todo(false, "synthEscapeKey for item2.1 and item2 disabled due to bug 691580");
+ } else {
+ gQueue.push(new synthEscapeKey("item2.1", new focusChecker("item2")));
+ gQueue.push(new synthEscapeKey("item2", new focusChecker("button")));
+ }
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <button id="button" context="contextmenu" label="button"/>
+ <menupopup id="contextmenu">
+ <menuitem id="item1" label="item1"/>
+ <menu id="item2" label="item2">
+ <menupopup>
+ <menuitem id="item2.1" label="item2.1"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_controls.html b/accessible/tests/mochitest/events/test_focus_controls.html
new file mode 100644
index 0000000000..268ec5d0e4
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_controls.html
@@ -0,0 +1,76 @@
+<html>
+
+<head>
+ <title>Accessible focus testing on HTML controls</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue(EVENT_FOCUS);
+
+ gQueue.push(new synthFocus("textbox"));
+ gQueue.push(new synthFocus("textarea"));
+ gQueue.push(new synthFocus("button1"));
+ gQueue.push(new synthFocus("button2"));
+ gQueue.push(new synthFocus("checkbox"));
+ gQueue.push(new synthFocus("radio1"));
+ gQueue.push(new synthDownKey("radio1", new focusChecker("radio2")));
+
+ // no focus events for checkbox or radio inputs when they are checked
+ // programmatically
+ gQueue.push(new changeCurrentItem("checkbox"));
+ gQueue.push(new changeCurrentItem("radio1"));
+
+ let fileBrowseButton = getAccessible("file");
+ gQueue.push(new synthFocus("file", new focusChecker(fileBrowseButton)));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox">
+ <textarea id="textarea"></textarea>
+
+ <input id="button1" type="button" value="button">
+ <button id="button2">button</button>
+ <input id="checkbox" type="checkbox">
+ <input id="radio1" type="radio" name="radiogroup">
+ <input id="radio2" type="radio" name="radiogroup">
+ <input id="file" type="file">
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_doc.html b/accessible/tests/mochitest/events/test_focus_doc.html
new file mode 100644
index 0000000000..a35fc06ed0
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_doc.html
@@ -0,0 +1,92 @@
+<html>
+
+<head>
+ <title>Accessible document focus event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ var gQueue = null;
+
+ // var gA11yEventDumpID = "eventdump";
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ // setup
+ var frameDoc = document.getElementById("iframe").contentDocument;
+ frameDoc.designMode = "on";
+ var frameDocAcc = getAccessible(frameDoc, [nsIAccessibleDocument]);
+ var buttonAcc = getAccessible("b1");
+
+ var frame2Doc = document.getElementById("iframe2").contentDocument;
+ var frame2Input = frame2Doc.getElementById("input");
+ var frame2DocAcc = getAccessible(frame2Doc);
+ var frame2InputAcc = getAccessible(frame2Input);
+
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ // try to give focus to contentEditable frame twice to cover bug 512059
+ gQueue.push(new synthFocus(buttonAcc));
+ gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc)));
+ gQueue.push(new synthFocus(buttonAcc));
+ gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc)));
+
+ // focus on not editable document
+ gQueue.push(new synthFocus(frame2InputAcc));
+ gQueue.push(new synthShiftTab(frame2DocAcc, new focusChecker(frame2DocAcc)));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512058"
+ title="Can't set focus to designMode document via accessibility APIs">
+ Mozilla Bug 512058
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512059"
+ title="Accessibility focus event never fired for designMode document after the first focus">
+ Mozilla Bug 512059
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=618046"
+ title="No focus change event when Shift+Tab at top of screen">
+ Mozilla Bug 618046
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="eventdump"></div>
+
+ <div id="testContainer">
+ <button id="b1">a button</button>
+ <iframe id="iframe" src="about:blank"></iframe>
+ <button id="b2">a button</button>
+ <iframe id="iframe2" src="data:text/html,<html><input id='input'></html>"></iframe>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_general.html b/accessible/tests/mochitest/events/test_focus_general.html
new file mode 100644
index 0000000000..6919ed8860
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_general.html
@@ -0,0 +1,176 @@
+<html>
+
+<head>
+ <title>Accessible focus testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function focusElmWhileSubdocIsFocused(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.invoke = function focusElmWhileSubdocIsFocused_invoke() {
+ this.DOMNode.focus();
+ };
+
+ this.eventSeq = [
+ new focusChecker(this.DOMNode),
+ ];
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.DOMNode.ownerDocument),
+ ];
+
+ this.getID = function focusElmWhileSubdocIsFocused_getID() {
+ return "Focus element while subdocument is focused " + prettyName(aID);
+ };
+ }
+
+ function imageMapChecker(aID) {
+ var node = getNode(aID);
+ this.type = EVENT_FOCUS;
+ this.match = function imageMapChecker_match(aEvent) {
+ return aEvent.DOMNode == node;
+ };
+ }
+
+ function topMenuChecker() {
+ this.type = EVENT_FOCUS;
+ this.match = function topMenuChecker_match(aEvent) {
+ return aEvent.accessible.role == ROLE_PARENT_MENUITEM;
+ };
+ }
+
+ function contextMenuChecker() {
+ this.type = EVENT_MENUPOPUP_START;
+ this.match = function contextMenuChecker_match(aEvent) {
+ return aEvent.accessible.role == ROLE_MENUPOPUP;
+ };
+ }
+
+ function focusContextMenuItemChecker() {
+ this.__proto__ = new focusChecker();
+
+ this.match = function focusContextMenuItemChecker_match(aEvent) {
+ return aEvent.accessible.role == ROLE_MENUITEM;
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTests() {
+ var frameDoc = document.getElementById("iframe").contentDocument;
+
+ var editableDoc = document.getElementById("editabledoc").contentDocument;
+ editableDoc.designMode = "on";
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("editablearea"));
+ gQueue.push(new synthFocus("navarea"));
+ gQueue.push(new synthTab("navarea", new focusChecker(frameDoc)));
+ gQueue.push(new focusElmWhileSubdocIsFocused("link"));
+
+ gQueue.push(new synthTab(editableDoc, new focusChecker(editableDoc)));
+ if (WIN || LINUX) {
+ // Alt key is used to active menubar and focus menu item on Windows,
+ // other platforms requires setting a ui.key.menuAccessKeyFocuses
+ // preference.
+ gQueue.push(new toggleTopMenu(editableDoc, new topMenuChecker()));
+ gQueue.push(new toggleTopMenu(editableDoc, new focusChecker(editableDoc)));
+ }
+ if (!(MAC && Services.prefs.getBoolPref("widget.macos.native-context-menus", false))) {
+ // Context menu accessibility is handled natively and not testable when
+ // native context menus are used on macOS.
+ gQueue.push(new synthContextMenu(editableDoc, new contextMenuChecker()));
+ gQueue.push(new synthDownKey(editableDoc, new focusContextMenuItemChecker()));
+ gQueue.push(new synthEscapeKey(editableDoc, new focusChecker(editableDoc)));
+ } else {
+ // If this test is run as part of multiple tests, it is displayed in the test harness iframe.
+ // In the non-native context menu case, right-clicking the editableDoc causes the editableDoc
+ // to scroll fully into view, and as a side-effect, the img below it ends up on the screen.
+ // When we're skipping the context menu check, scroll img onto the screen manually, because
+ // otherwise it may remain out-of-view and clipped by the test harness iframe.
+ var img = document.querySelector("img");
+ gQueue.push(new scrollIntoView(img, new nofocusChecker(img)));
+ }
+ if (SEAMONKEY) {
+ todo(false, "shift tab from editable document fails on (Windows) SeaMonkey! (Bug 718235)");
+ } else if (LINUX || MAC) {
+ todo(false, "shift tab from editable document fails on linux and Mac, bug 746519!");
+ } else {
+ gQueue.push(new synthShiftTab("link", new focusChecker("link")));
+ } // ! SEAMONKEY
+
+ gQueue.push(new synthFocus("a", new imageMapChecker("a")));
+ gQueue.push(new synthFocus("b", new imageMapChecker("b")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=352220"
+ title="Inconsistent focus events when returning to a document frame">
+ Mozilla Bug 352220
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=550338"
+ title="Broken focus when returning to editable documents from menus">
+ Mozilla Bug 550338
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=961696"
+ title="Accessible object:state-changed:focused events for imagemap links are broken">
+ Mozilla Bug 961696
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="editablearea" contentEditable="true">editable area</div>
+ <div id="navarea" tabindex="0">navigable area</div>
+ <iframe id="iframe" src="data:text/html,<html></html>"></iframe>
+ <a id="link" href="">link</a>
+ <iframe id="editabledoc" src="about:blank"></iframe>
+
+ <map name="atoz_map">
+ <area id="a" coords="0,0,13,14" shape="rect">
+ <area id="b" coords="17,0,30,14" shape="rect">
+ </map>
+ <img width="447" height="15" usemap="#atoz_map" src="../letters.gif">
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_general.xhtml b/accessible/tests/mochitest/events/test_focus_general.xhtml
new file mode 100644
index 0000000000..c446359b32
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_general.xhtml
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessible focus event testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("textbox",
+ new focusChecker(getNode("textbox"))));
+ gQueue.push(new synthFocusOnFrame("editabledoc"));
+ gQueue.push(new synthFocus("radioclothes",
+ new focusChecker("radiosweater")));
+ gQueue.push(new synthDownKey("radiosweater",
+ new focusChecker("radiojacket")));
+ gQueue.push(new synthFocus("checkbox"));
+ gQueue.push(new synthFocus("button"));
+ gQueue.push(new synthFocus("checkbutton"));
+ gQueue.push(new synthFocus("radiobutton"));
+
+ // focus menubutton
+ gQueue.push(new synthFocus("menubutton"));
+ // click menubutton, open popup, focus stays on menu button
+ gQueue.push(new synthClick("menubutton", new nofocusChecker()));
+ // select first menu item ("item 1"), focus on menu item
+ gQueue.push(new synthDownKey("menubutton", new focusChecker("mb_item1")));
+ // choose select menu item, focus gets back to menubutton
+ gQueue.push(new synthEnterKey("mb_item1", new focusChecker("menubutton")));
+ // press enter to open popup, focus stays on menubutton
+ gQueue.push(new synthEnterKey("menubutton", new nofocusChecker()));
+ // select second menu item ("item 2"), focus on menu item
+ gQueue.push(new synthUpKey("menubutton", new focusChecker("mb_item2")));
+ // close the popup
+ gQueue.push(new synthEscapeKey("menubutton", new focusChecker("menubutton")));
+
+ // clicking on button having associated popup doesn't change focus
+ gQueue.push(new synthClick("popupbutton", [
+ new nofocusChecker(),
+ new invokerChecker("popupshown", "backpopup")
+ ]));
+
+ // select first menu item ("item 1"), focus on menu item
+ gQueue.push(new synthDownKey("popupbutton", new focusChecker("bp_item1")));
+ // choose select menu item, focus gets back to menubutton
+ gQueue.push(new synthEnterKey("bp_item1", new focusChecker("menubutton")));
+ // show popup again for the next test
+ gQueue.push(new synthClick("popupbutton", new nofocusChecker()));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368"
+ title=" fire focus event on document accessible whenever the root or body element is focused">
+ Mozilla Bug 552368
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <html:input id="textbox" value="hello"/>
+ <iframe id="editabledoc" src="focus.html"/>
+ <radiogroup id="radioclothes">
+ <radio id="radiosweater" label="radiosweater"/>
+ <radio id="radiocap" label="radiocap" disabled="true"/>
+ <radio id="radiojacket" label="radiojacket"/>
+ </radiogroup>
+ <checkbox id="checkbox" label="checkbox"/>
+ <button id="button" label="button"/>
+
+ <button id="menubutton" type="menu" label="menubutton">
+ <menupopup>
+ <menuitem id="mb_item1" label="item1"/>
+ <menuitem id="mb_item2" label="item2"/>
+ </menupopup>
+ </button>
+
+ <button id="checkbutton" type="checkbox" label="checkbutton"/>
+ <button id="radiobutton" type="radio" group="rbgroup" label="radio1"/>
+
+ <popupset>
+ <menupopup id="backpopup" position="after_start">
+ <menuitem id="bp_item1" label="Page 1"/>
+ <menuitem id="bp_item2" label="Page 2"/>
+ </menupopup>
+ </popupset>
+ <button id="popupbutton" label="Pop Me Up" popup="backpopup"/>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml
new file mode 100644
index 0000000000..848657d3b3
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml
@@ -0,0 +1,153 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible focus event testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ async function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new synthFocus("richlistbox", new focusChecker("rlb_item1")));
+ gQueue.push(new synthDownKey("rlb_item1", new focusChecker("rlb_item2")));
+ gQueue.push(new synthFocus("multiselrichlistbox", new focusChecker("msrlb_item1")));
+ gQueue.push(new synthDownKey("msrlb_item1", new focusChecker("msrlb_item2"), { shiftKey: true }));
+ gQueue.push(new synthFocus("emptyrichlistbox", new focusChecker("emptyrichlistbox")));
+
+ gQueue.push(new synthFocus("menulist"));
+ gQueue.push(new synthClick("menulist", new focusChecker("ml_tangerine"),
+ { where: "center" }));
+ gQueue.push(new synthDownKey("ml_tangerine", new focusChecker("ml_marmalade")));
+ gQueue.push(new synthEscapeKey("ml_marmalade", new focusChecker("menulist")));
+
+ // On Windows, items get selected during navigation.
+ let expectedItem = WIN ? "ml_strawberry" : "ml_marmalade";
+ gQueue.push(new synthDownKey("menulist", new nofocusChecker(expectedItem)));
+ gQueue.push(new synthOpenComboboxKey("menulist", new focusChecker(expectedItem)));
+ gQueue.push(new synthEnterKey(expectedItem, new focusChecker("menulist")));
+
+ // no focus events for unfocused list controls when current item is
+ // changed.
+ gQueue.push(new synthFocus("emptyrichlistbox"));
+
+ gQueue.push(new changeCurrentItem("richlistbox", "rlb_item1"));
+ gQueue.push(new changeCurrentItem("menulist", WIN ? "ml_marmalade" : "ml_tangerine"));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ // When a menulist contains something other than XUL menuitems, we need
+ // to manage focus with aria-activedescendant.
+ info("Testing opening a menupopup with aria-activedescendant");
+ let popupDiv1 = getNode("menupopup_ad_div1");
+ let focused = PromEvents.waitForEvent(EVENT_FOCUS, popupDiv1);
+ let popup = getNode("menupopup_ad");
+ popup.openPopup();
+ await focused;
+ info("Testing removal of previous active descendant + setting new active descendant");
+ focused = PromEvents.waitForEvent(EVENT_FOCUS, "menupopup_ad_div2");
+ popupDiv1.remove();
+ popup.setAttribute("aria-activedescendant", "menupopup_ad_div2");
+ await focused;
+ popup.hidePopup();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418"
+ title="Accessibles for focused HTML Select elements are not getting focused state">
+ Mozilla Bug 433418
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893"
+ title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused">
+ Mozilla Bug 474893
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368"
+ title=" fire focus event on document accessible whenever the root or body element is focused">
+ Mozilla Bug 552368
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <richlistbox id="richlistbox">
+ <richlistitem id="rlb_item1">
+ <description>A XUL Description!</description>
+ </richlistitem>
+ <richlistitem id="rlb_item2">
+ <button label="A XUL Button"/>
+ </richlistitem>
+ </richlistbox>
+ <richlistbox id="multiselrichlistbox" seltype="multiple">
+ <richlistitem id="msrlb_item1">
+ <description>A XUL Description!</description>
+ </richlistitem>
+ <richlistitem id="msrlb_item2">
+ <button label="A XUL Button"/>
+ </richlistitem>
+ </richlistbox>
+ <richlistbox id="emptyrichlistbox" seltype="multiple"/>
+
+ <menulist id="menulist">
+ <menupopup>
+ <menuitem id="ml_tangerine" label="tangerine trees"/>
+ <menuitem id="ml_marmalade" label="marmalade skies"/>
+ <menuitem id="ml_strawberry" label="strawberry fields"/>
+ </menupopup>
+ </menulist>
+
+ <menulist>
+ <menupopup id="menupopup_ad" aria-activedescendant="menupopup_ad_div1">
+ <div id="menupopup_ad_div1" role="option"></div>
+ <div id="menupopup_ad_div2" role="option"></div>
+ </menupopup>
+ </menulist>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_menu.xhtml b/accessible/tests/mochitest/events/test_focus_menu.xhtml
new file mode 100644
index 0000000000..dda10517eb
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_menu.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Menu focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ if (WIN) {
+ gQueue.push(new toggleTopMenu("fruit", new focusChecker("fruit")));
+ gQueue.push(new synthRightKey("fruit", new focusChecker("vehicle")));
+ gQueue.push(new synthEscapeKey("vehicle", new focusChecker(document)));
+ }
+
+ // mouse move activate items but no focus event until menubar is active
+ gQueue.push(new synthMouseMove("fruit", new nofocusChecker("apple")));
+
+ // mouseover and click on menuitem makes it active before menubar is
+ // active
+ gQueue.push(new synthClick("fruit", new focusChecker("fruit"), { where: "center" }));
+
+ // mouseover on menuitem when menubar is active
+ gQueue.push(new synthMouseMove("apple", new focusChecker("apple")));
+
+ // keydown on disabled menuitem (disabled items are skipped on linux)
+ if (WIN)
+ gQueue.push(new synthDownKey("apple", new focusChecker("orange")));
+
+ // menu and menuitem are both active
+ // XXX: intermitent failure because two focus events may be coalesced,
+ // think to workaround or fix this issue, when done enable queue invoker
+ // below and remove next two.
+ //gQueue.push(new synthRightKey("apple",
+ // [ new focusChecker("vehicle"),
+ // new focusChecker("cycle")]));
+ gQueue.push(new synthMouseMove("vehicle", new focusChecker("vehicle")));
+ gQueue.push(new synthDownKey("vehicle", new focusChecker("cycle")));
+
+ // open submenu
+ gQueue.push(new synthRightKey("cycle", new focusChecker("tricycle")));
+
+ // move to first menu in cycle, DOMMenuItemActive is fired for fruit,
+ // cycle and apple menuitems (bug 685191)
+ todo(false, "focus is fired for 'cycle' menuitem");
+ //gQueue.push(new synthRightKey("vehicle", new focusChecker("apple")));
+
+ // click menuitem to close menu, focus gets back to document
+ gQueue.push(new synthClick("tricycle", new focusChecker(document), { where: "center" }));
+
+ //enableLogging("focus,DOMEvents,tree"); // logging for bug708927
+ //gQueue.onFinish = function() { disableLogging(); }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menubar>
+ <menu id="fruit" label="Fruit">
+ <menupopup>
+ <menuitem id="apple" label="Apple"/>
+ <menuitem id="orange" label="Orange" disabled="true"/>
+ </menupopup>
+ </menu>
+ <menu id="vehicle" label="Vehicle">
+ <menupopup id="vehiclePopup">
+ <menu id="cycle" label="cycle">
+ <menupopup>
+ <menuitem id="tricycle" label="tricycle"/>
+ </menupopup>
+ </menu>
+ <menuitem id="car" label="Car" disabled="true"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_name.html b/accessible/tests/mochitest/events/test_focus_name.html
new file mode 100644
index 0000000000..aa77923909
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_name.html
@@ -0,0 +1,116 @@
+<html>
+
+<head>
+ <title>Accessible name testing on focus</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Checker for invokers.
+ */
+ function actionChecker(aID, aDescription) {
+ this.__proto__ = new invokerChecker(EVENT_FOCUS, aID);
+
+ this.check = function actionChecker_check(aEvent) {
+ var target = aEvent.accessible;
+ is(target.description, aDescription,
+ "Wrong description for " + prettyName(target));
+ };
+ }
+
+ var gFocusHandler = {
+ handleEvent: function gFocusHandler_handleEvent(aEvent) {
+ var elm = aEvent.target;
+ if (elm.nodeType != Node.ELEMENT_NODE)
+ return;
+
+ gTooltipElm.style.display = "block";
+
+ elm.setAttribute("aria-describedby", "tooltip");
+ },
+ };
+
+ var gBlurHandler = {
+ handleEvent: function gBlurHandler_handleEvent(aEvent) {
+ gTooltipElm.style.display = "none";
+
+ var elm = aEvent.target;
+ if (elm.nodeType == Node.ELEMENT_NODE)
+ elm.removeAttribute("aria-describedby");
+ },
+ };
+
+ /**
+ * Do tests.
+ */
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ var gButtonElm = null;
+ var gTextboxElm = null;
+ var gTooltipElm = null;
+
+ function doTests() {
+ gButtonElm = getNode("button");
+ gTextboxElm = getNode("textbox");
+ gTooltipElm = getNode("tooltip");
+
+ gButtonElm.addEventListener("focus", gFocusHandler);
+ gButtonElm.addEventListener("blur", gBlurHandler);
+ gTextboxElm.addEventListener("focus", gFocusHandler);
+ gTextboxElm.addEventListener("blur", gBlurHandler);
+
+ // The aria-describedby is changed on DOM focus. Accessible description
+ // should be updated when a11y focus is fired.
+ gQueue = new eventQueue(nsIAccessibleEvent.EVENT_FOCUS);
+ gQueue.onFinish = function() {
+ gButtonElm.removeEventListener("focus", gFocusHandler);
+ gButtonElm.removeEventListener("blur", gBlurHandler);
+ gTextboxElm.removeEventListener("focus", gFocusHandler);
+ gTextboxElm.removeEventListener("blur", gBlurHandler);
+ };
+
+ var descr = "It's a tooltip";
+ gQueue.push(new synthFocus("button", new actionChecker("button", descr)));
+ gQueue.push(new synthTab("textbox", new actionChecker("textbox", descr)));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=520709"
+ title="mochitest to ensure name/description are updated on a11y focus if they were changed on DOM focus">
+ Mozilla Bug 520709
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="tooltip" style="display: none" aria-hidden="true">It's a tooltip</div>
+ <button id="button">button</button>
+ <input id="textbox">
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_removal.html b/accessible/tests/mochitest/events/test_focus_removal.html
new file mode 100644
index 0000000000..eb47b07075
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_removal.html
@@ -0,0 +1,95 @@
+<html>
+
+<head>
+ <title>Test removal of focused accessible</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function setFocus(aNodeToFocus, aExpectedFocus) {
+ let expected = aExpectedFocus || aNodeToFocus;
+ let focused = waitForEvent(EVENT_FOCUS, expected);
+ info("Focusing " + aNodeToFocus.id);
+ aNodeToFocus.focus();
+ await focused;
+ ok(true, expected.id + " focused after " +
+ aNodeToFocus.id + ".focus()");
+ }
+
+ async function expectFocusAfterRemove(aNodeToRemove, aExpectedFocus, aDisplayNone = false) {
+ let focused = waitForEvent(EVENT_FOCUS, aExpectedFocus);
+ info("Removing " + aNodeToRemove.id);
+ if (aDisplayNone) {
+ aNodeToRemove.style.display = "none";
+ } else {
+ aNodeToRemove.remove();
+ }
+ await focused;
+ let friendlyExpected = aExpectedFocus == document ?
+ "document" : aExpectedFocus.id;
+ ok(true, friendlyExpected + " focused after " +
+ aNodeToRemove.id + " removed");
+ }
+
+ async function doTests() {
+ info("Testing removal of focused node itself");
+ let button = getNode("button");
+ await setFocus(button);
+ await expectFocusAfterRemove(button, document);
+
+ info("Testing removal of focused node's parent");
+ let dialog = getNode("dialog");
+ let dialogButton = getNode("dialogButton");
+ await setFocus(dialogButton);
+ await expectFocusAfterRemove(dialog, document);
+
+ info("Testing removal of aria-activedescendant target");
+ let listbox = getNode("listbox");
+ let option = getNode("option");
+ await setFocus(listbox, option);
+ await expectFocusAfterRemove(option, listbox);
+
+ info("Test hiding focused element with display: none");
+ let groupingButton = getNode("groupingButton");
+ await setFocus(groupingButton);
+ await expectFocusAfterRemove(groupingButton, document, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="button"></button>
+
+ <div role="dialog" id="dialog">
+ <button id="dialogButton"></button>
+ </div>
+
+ <div role="listbox" id="listbox" tabindex="0" aria-activedescendant="option">
+ <div role="option" id="option"></div>
+ </div>
+
+ <div role="grouping" id="grouping">
+ <button id="groupingButton">
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_selects.html b/accessible/tests/mochitest/events/test_focus_selects.html
new file mode 100644
index 0000000000..2346691dfd
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_selects.html
@@ -0,0 +1,190 @@
+<html>
+
+<head>
+ <title>Accessible focus testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+ var gQueue = null;
+
+ async function doTests() {
+ // Bug 746534 - File causes crash or hang on OS X
+ if (MAC) {
+ todo(false, "Bug 746534 - test file causes crash or hang on OS X");
+ SimpleTest.finish();
+ return;
+ }
+
+ let p = waitForEvent(EVENT_FOCUS, "orange");
+ // first item is focused until there's selection
+ getNode("list").focus();
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "orange"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // item is selected and stays focused and active
+ synthesizeKey("VK_DOWN");
+ await p;
+
+ p = waitForEvents([
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, false, true),
+ stateChangeEventArgs("apple", EXT_STATE_ACTIVE, true, true),
+ [EVENT_FOCUS, "apple"],
+ ]);
+ // last selected item is focused
+ synthesizeKey("VK_DOWN", { shiftKey: true });
+ await p;
+
+ p = waitForEvents({
+ expected: [
+ [EVENT_FOCUS, "orange"],
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ unexpected: [
+ [EVENT_FOCUS, "apple"],
+ stateChangeEventArgs("apple", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // no focus event if nothing is changed
+ synthesizeKey("VK_DOWN");
+ // current item is focused
+ synthesizeKey("VK_UP", { ctrlKey: true });
+ await p;
+
+ p = waitForEvent(EVENT_FOCUS, "emptylist");
+ // focus on empty list (no items to be focused)
+ synthesizeKey("VK_TAB");
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_FOCUS, "orange"]],
+ unexpected: [stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true)],
+ });
+ // current item is focused
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ await p;
+
+ p = waitForEvent(EVENT_FOCUS, "combobox");
+ getNode("combobox").focus();
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "cb_apple"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("cb_apple", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // collapsed combobox keeps a focus
+ synthesizeKey("VK_DOWN");
+ await p;
+
+ // no focus events for unfocused list controls when current item is
+ // changed
+
+ p = waitForEvent(EVENT_FOCUS, "emptylist");
+ getNode("emptylist").focus();
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "orange"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // An unfocused selectable list gets selection change events,
+ // but not active or focus change events.
+ getNode("list").selectedIndex = getNode("orange").index;
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "cb_orange"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("cb_orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // An unfocused selectable combobox gets selection change events,
+ // but not focus events nor active state change events.
+ getNode("cb_orange").selected = true;
+ await p;
+
+ // Bug 1838983: Make sure we don't fail a C++ assertion when the focused
+ // active item is destroyed.
+ info("Focusing recreate");
+ p = waitForEvent(EVENT_FOCUS, "recreateA");
+ const recreate = getNode("recreate");
+ recreate.focus();
+ await p;
+ info("Changing recreate size");
+ p = waitForEvent(EVENT_FOCUS, recreate);
+ // This will recreate the select and its children.
+ recreate.size = 1;
+ await p;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418"
+ title="Accessibles for focused HTML Select elements are not getting focused state">
+ Mozilla Bug 433418
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893"
+ title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused">
+ Mozilla Bug 474893
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="list" size="5" multiple="">
+ <option id="orange">Orange</option>
+ <option id="apple">Apple</option>
+ </select>
+
+ <select id="emptylist" size="5"></select>
+
+ <select id="combobox">
+ <option id="cb_orange">Orange</option>
+ <option id="cb_apple">Apple</option>
+ </select>
+
+ <select id="recreate" size="5">
+ <option id="recreateA">a</option>
+ </select>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_tabbox.xhtml b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml
new file mode 100644
index 0000000000..1b808831dc
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tabbox focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ if (MAC) {
+ todo(false, "Tests disabled because of imminent failure.");
+ SimpleTest.finish();
+ return;
+ }
+
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ var input = getNode("input");
+ gQueue.push(new synthClick("tab1", new focusChecker("tab1")));
+ gQueue.push(new synthTab("tab1", new focusChecker("checkbox1")));
+ gQueue.push(new synthKey("tab1", "VK_TAB", { ctrlKey: true },
+ new focusChecker(input)));
+ gQueue.push(new synthKey("tab2", "VK_TAB", { ctrlKey: true },
+ new focusChecker("tab3")));
+ gQueue.push(new synthKey("tab3", "VK_TAB", { ctrlKey: true },
+ new focusChecker("tab1")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=370396"
+ title="Control+Tab to an empty tab panel in a tabbox causes focus to leave the tabbox">
+ Mozilla Bug 370396
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tabbox>
+ <tabs>
+ <tab id="tab1" label="Tab1" selected="true"/>
+ <tab id="tab2" label="Tab2" />
+ <tab id="tab3" label="Tab3" />
+ </tabs>
+ <tabpanels>
+ <tabpanel orient="vertical">
+ <groupbox orient="vertical">
+ <checkbox id="checkbox1" label="Monday" width="75"/>
+ <checkbox label="Tuesday" width="75"/>
+ <checkbox label="Wednesday" width="75"/>
+ <checkbox label="Thursday" width="75"/>
+ <checkbox label="Friday" width="75"/>
+ <checkbox label="Saturday" width="75"/>
+ <checkbox label="Sunday" width="75"/>
+ </groupbox>
+
+ <spacer style="height: 10px" />
+ <label value="Label After checkboxes" />
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <html:input id="input" />
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <description>Tab 3 content</description>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_tree.xhtml b/accessible/tests/mochitest/events/test_focus_tree.xhtml
new file mode 100644
index 0000000000..f36816c788
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_tree.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tree focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function focusTree(aTreeID)
+ {
+ var checker = new focusChecker(getFirstTreeItem, aTreeID);
+ this.__proto__ = new synthFocus(aTreeID, [ checker ]);
+ }
+
+ function moveToNextItem(aTreeID)
+ {
+ var checker = new focusChecker(getSecondTreeItem, aTreeID);
+ this.__proto__ = new synthDownKey(aTreeID, [ checker ]);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Helpers
+
+ function getTreeItemAt(aTreeID, aIdx)
+ { return getAccessible(aTreeID).getChildAt(aIdx + 1); }
+
+ function getFirstTreeItem(aTreeID)
+ { return getTreeItemAt(aTreeID, 0); }
+
+ function getSecondTreeItem(aTreeID)
+ { return getTreeItemAt(aTreeID, 1); }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ var gQueue = null;
+
+ //gA11yEventDumpID = "debug"; // debugging
+ //gA11yEventDumpToConsole = true; // debugging
+
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new focusTree("tree"));
+ gQueue.push(new moveToNextItem("tree"));
+ gQueue.push(new synthFocus("emptytree"));
+
+ // no focus event for changed current item for unfocused tree
+ gQueue.push(new changeCurrentItem("tree", 0));
+
+ gQueue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(5));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=386821"
+ title="Need better solution for firing delayed event against xul tree">
+ Mozilla Bug 386821
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=406308"
+ title="Don't fire accessible focus events if widget is not actually in focus, confuses screen readers">
+ Mozilla Bug 406308
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ <tree id="emptytree" flex="1">
+ <treecols>
+ <treecol id="emptytree_col1" flex="1" primary="true" label="column"/>
+ <treecol id="emptytree_col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="emptytree_treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/events/test_focusable_statechange.html b/accessible/tests/mochitest/events/test_focusable_statechange.html
new file mode 100644
index 0000000000..03f4f12587
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focusable_statechange.html
@@ -0,0 +1,122 @@
+<html>
+
+<head>
+ <title>Test removal of focused accessible</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ function focusableStateChange(id, enabled) {
+ return [EVENT_STATE_CHANGE, e => {
+ e.QueryInterface(nsIAccessibleStateChangeEvent);
+ return getAccessible(id) == e.accessible &&
+ e.state == STATE_FOCUSABLE && (enabled == undefined || e.isEnabled == enabled);
+ }];
+ }
+
+ function editableStateChange(id, enabled) {
+ return [EVENT_STATE_CHANGE, e => {
+ e.QueryInterface(nsIAccessibleStateChangeEvent);
+ return getAccessible(id) == e.accessible &&
+ e.state == EXT_STATE_EDITABLE && e.isExtraState &&
+ (enabled == undefined || e.isEnabled == enabled);
+ }];
+ }
+
+ async function doTests() {
+ info("disable buttons.");
+ // Expect focusable change with 'disabled',
+ // and don't expect it with 'aria-disabled'.
+ let p = waitForEvents({
+ expected: [focusableStateChange("button2", false)],
+ unexpected: [focusableStateChange("button1")]
+ });
+ getNode("button1").setAttribute("aria-disabled", "true");
+ getNode("button2").disabled = true;
+ await p;
+
+ info("re-enable button");
+ // Expect focusable change with 'disabled',
+ // and don't expect it with 'aria-disabled'.
+ p = waitForEvents({
+ expected: [focusableStateChange("button2", true)],
+ unexpected: [focusableStateChange("button1")]
+ });
+ getNode("button1").setAttribute("aria-disabled", "false");
+ getNode("button2").disabled = false;
+ await p;
+
+ info("add tabindex");
+ // Expect focusable change on non-input,
+ // and don't expect event on an already focusable input.
+ p = waitForEvents({
+ expected: [focusableStateChange("div", true)],
+ unexpected: [focusableStateChange("button2")]
+ });
+ getNode("button2").tabIndex = "0";
+ getNode("div").tabIndex = "0";
+ await p;
+
+ info("remove tabindex");
+ // Expect focusable change when removing tabindex.
+ p = waitForEvent(...focusableStateChange("div", false));
+ getNode("div").removeAttribute("tabindex");
+ await p;
+
+ info("add contenteditable");
+ // Expect editable change on non-input,
+ // and don't expect event on a native input.
+ p = waitForEvents({
+ expected: [focusableStateChange("div", true), editableStateChange("div", true)],
+ unexpected: [focusableStateChange("input"), editableStateChange("input")]
+ });
+ getNode("input").contentEditable = true;
+ getNode("div").contentEditable = true;
+ await p;
+
+ info("remove contenteditable");
+ // Expect editable change on non-input,
+ // and don't expect event on a native input.
+ p = waitForEvents({
+ expected: [focusableStateChange("div", false), editableStateChange("div", false)],
+ unexpected: [focusableStateChange("input"), editableStateChange("input")]
+ });
+ getNode("input").contentEditable = false;
+ getNode("div").contentEditable = false;
+ await p;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="button1"></button>
+ <button id="button2"></button>
+
+ <div id="div">Hello</div>
+
+ <input id="input" value="Hello">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_fromUserInput.html b/accessible/tests/mochitest/events/test_fromUserInput.html
new file mode 100644
index 0000000000..b3617358cf
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_fromUserInput.html
@@ -0,0 +1,112 @@
+<html>
+
+<head>
+ <title>Testing of isFromUserInput in text events</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Remove text data from HTML input.
+ */
+ function removeTextFromInput(aID, aStart, aEnd, aText, aFromUser) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser),
+ ];
+
+ this.invoke = function removeTextFromInput_invoke() {
+ this.DOMNode.focus();
+ this.DOMNode.setSelectionRange(aStart, aEnd);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromInput_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove text data from text node.
+ */
+ function removeTextFromContentEditable(aID, aStart, aEnd, aText, aFromUser) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser),
+ ];
+
+ this.invoke = function removeTextFromContentEditable_invoke() {
+ this.DOMNode.focus();
+ this.textNode = getNode(aID).firstChild;
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.setStart(this.textNode, aStart);
+ range.setEnd(this.textNode, aEnd);
+ selection.addRange(range);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromContentEditable_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+
+ var gQueue = null;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // Focused editable text node
+ gQueue.push(new removeTextFromContentEditable("div", 0, 3, "hel", true));
+
+ // Focused editable HTML input
+ gQueue.push(new removeTextFromInput("input", 1, 2, "n", true));
+
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+
+ </script>
+</head>
+
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=686909"
+ title="isFromUserInput flag on accessible text change events not correct">
+ Mozilla Bug 686909
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ <div id="eventdump"></div>
+
+ <div id="div" contentEditable="true">hello</div>
+ <input id="input" value="input">
+
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/events/test_label.xhtml b/accessible/tests/mochitest/events/test_label.xhtml
new file mode 100644
index 0000000000..5780629dc6
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_label.xhtml
@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tests: accessible XUL label/description events">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ const kRecreated = 0;
+ const kTextRemoved = 1;
+ const kTextChanged = 2;
+
+ const kNoValue = 0;
+
+ /**
+ * Set/remove @value attribute.
+ */
+ function setValue(aID, aValue, aResult, aOldValue)
+ {
+ this.labelNode = getNode(aID);
+
+ this.eventSeq = [];
+
+ switch (aResult) {
+ case kRecreated:
+ this.eventSeq.push(new invokerChecker(EVENT_HIDE, this.labelNode));
+ this.eventSeq.push(new invokerChecker(EVENT_SHOW, this.labelNode));
+ break;
+ case kTextRemoved:
+ this.eventSeq.push(
+ new textChangeChecker(this.labelNode, 0, aOldValue.length,
+ aOldValue, false));
+ break;
+ case kTextChanged:
+ this.eventSeq.push(
+ new textChangeChecker(this.labelNode, 0, aOldValue.length,
+ aOldValue, false));
+ this.eventSeq.push(
+ new textChangeChecker(this.labelNode, 0, aValue.length,
+ aValue, true));
+ break;
+ }
+
+ this.invoke = function setValue_invoke()
+ {
+ if (aValue === kNoValue)
+ this.labelNode.removeAttribute("value");
+ else
+ this.labelNode.setAttribute("value", aValue);
+ }
+
+ this.finalCheck = function setValue_finalCheck()
+ {
+ let tree =
+ { LABEL: [] };
+
+ const expectChild = (() => {
+ if (aValue === kNoValue) {
+ return false;
+ }
+ if (aValue === "") {
+ return this.labelNode.nodeName == "label";
+ }
+ return true;
+ })();
+
+ if (expectChild) {
+ tree.LABEL.push({ STATICTEXT: [ ] });
+ }
+ testAccessibleTree(aID, tree);
+ }
+
+ this.getID = function setValue_getID()
+ {
+ return "set @value='" + aValue + "' for label " + prettyName(aID);
+ }
+ }
+
+ /**
+ * Change @crop attribute.
+ */
+ function setCrop(aID, aCropValue, aRemovedText, aInsertedText)
+ {
+ this.labelNode = getNode(aID);
+ this.width = this.labelNode.getBoundingClientRect().width;
+ this.charWidth = this.width / this.labelNode.value.length;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.labelNode),
+ new invokerChecker(EVENT_SHOW, this.labelNode),
+ ];
+
+ this.invoke = function setCrop_invoke()
+ {
+ if (!this.labelNode.hasAttribute("crop"))
+ this.labelNode.style.width = Math.floor(this.width - 2 * this.charWidth) + "px";
+
+ this.labelNode.setAttribute("crop", aCropValue);
+ }
+
+ this.getID = function setCrop_finalCheck()
+ {
+ return "set crop " + aCropValue;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new setValue("label", "shiroka strana", kRecreated));
+ gQueue.push(new setValue("label", "?<>!+_", kTextChanged, "shiroka strana"));
+ gQueue.push(new setValue("label", "", kRecreated));
+ gQueue.push(new setValue("label", kNoValue, kRecreated));
+
+ gQueue.push(new setValue("descr", "hello world", kRecreated));
+ gQueue.push(new setValue("descr", "si_ya", kTextChanged, "hello world"));
+ gQueue.push(new setValue("descr", "", kTextRemoved, "si_ya"));
+ gQueue.push(new setValue("descr", kNoValue, kRecreated));
+
+ gQueue.push(new setCrop("croplabel", "center"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166"
+ title="xul:label@value accessible should implement nsIAccessibleText">
+ Bug 396166
+ </a>
+ <br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <label id="label"/>
+ <description id="descr"/>
+
+ <hbox>
+ <label id="croplabel" value="valuetocro"
+ style="font-family: monospace;"/>
+ </hbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/events/test_menu.xhtml b/accessible/tests/mochitest/events/test_menu.xhtml
new file mode 100644
index 0000000000..271831ba83
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_menu.xhtml
@@ -0,0 +1,200 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible menu events testing for XUL menu">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script><![CDATA[
+ function openFileMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENU_START, "menubar"),
+ new invokerChecker("popupshown", "menupopup-file")
+ // new invokerChecker(EVENT_FOCUS, getNode("menuitem-newtab")) intermitent failure
+ ];
+
+ this.invoke = function openFileMenu_invoke()
+ {
+ synthesizeKey("F", {altKey: true, shiftKey: true});
+ }
+
+ this.getID = function openFileMenu_getID()
+ {
+ return "open file menu by alt+F press";
+ }
+ }
+
+ function openEditMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker("popuphidden", "menupopup-file"),
+ new invokerChecker("popupshown", "menupopup-edit"),
+ // new invokerChecker(EVENT_FOCUS, getNode("menuitem-undo")) intermitent failure
+ ];
+
+ this.invoke = function openEditMenu_invoke()
+ {
+ synthesizeKey("KEY_ArrowRight");
+ }
+
+ this.getID = function openEditMenu_getID()
+ {
+ return "open edit menu by lef arrow press";
+ }
+ }
+
+ function closeEditMenu()
+ {
+ this.eventSeq = [
+ //new invokerChecker(EVENT_FOCUS, document), intermitent failure
+ new invokerChecker("popuphidden", "menupopup-edit"),
+ ];
+
+ this.invoke = function closeEditMenu_invoke()
+ {
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function closeEditMenu_getID()
+ {
+ return "close edit menu";
+ }
+ }
+
+ function focusFileMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENU_START, getNode("menubar"))
+ // new invokerChecker(EVENT_FOCUS, getNode("menuitem-file")) //intermitent failure
+ ];
+
+ this.invoke = function focusFileMenu_invoke()
+ {
+ synthesizeKey("KEY_Alt");
+ }
+
+ this.getID = function focusFileMenu_getID()
+ {
+ return "activate menubar, focus file menu (atl press)";
+ }
+ }
+
+ function focusEditMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode("menuitem-edit"))
+ ];
+
+ this.invoke = function focusEditMenu_invoke()
+ {
+ synthesizeKey("KEY_ArrowRight");
+ }
+
+ this.getID = function focusEditMenu_getID()
+ {
+ return "focus edit menu";
+ }
+ }
+
+ function leaveMenubar()
+ {
+ this.eventSeq = [
+ //new invokerChecker(EVENT_FOCUS, document), intermitent failure
+ new invokerChecker(EVENT_MENU_END, "menubar")
+ ];
+
+ this.invoke = function leaveMenubar_invoke()
+ {
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function leaveMenubar_getID()
+ {
+ return "leave menubar";
+ }
+ }
+
+ /**
+ * Do tests.
+ */
+
+ //gA11yEventDumpID = "eventdump";
+ //gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTests()
+ {
+ if (!WIN && !LINUX) {
+ todo(false, "Enable this test on other platforms.");
+ SimpleTest.finish();
+ return;
+ }
+
+ todo(false,
+ "Fix intermitent failures. Focus may randomly occur before or after menupopup events!");
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new openFileMenu());
+ gQueue.push(new openEditMenu());
+ gQueue.push(new closeEditMenu());
+ gQueue.push(new leaveMenubar());
+
+ // Alt key is used to active menubar and focus menu item on Windows,
+ // other platforms requires setting a ui.key.menuAccessKeyFocuses
+ // preference.
+ if (WIN || LINUX) {
+ gQueue.push(new focusFileMenu());
+ gQueue.push(new focusEditMenu());
+ gQueue.push(new leaveMenubar());
+ }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]></script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189"
+ title="Clean up FireAccessibleFocusEvent">
+ Mozilla Bug 615189
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menubar id="menubar">
+ <menu id="menuitem-file" label="File" accesskey="F">
+ <menupopup id="menupopup-file">
+ <menuitem id="menuitem-newtab" label="New Tab"/>
+ </menupopup>
+ </menu>
+ <menu id="menuitem-edit" label="Edit" accesskey="E">
+ <menupopup id="menupopup-edit">
+ <menuitem id="menuitem-undo" label="Undo"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <vbox id="eventdump" role="log"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_mutation.html b/accessible/tests/mochitest/events/test_mutation.html
new file mode 100644
index 0000000000..7ee876570b
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_mutation.html
@@ -0,0 +1,580 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ div.displayNone a { display:none; }
+ div.visibilityHidden a { visibility:hidden; }
+</style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Invokers.
+ */
+ var kNoEvents = 0;
+
+ var kShowEvent = 1;
+ var kHideEvent = 2;
+ var kReorderEvent = 4;
+ var kShowEvents = kShowEvent | kReorderEvent;
+ var kHideEvents = kHideEvent | kReorderEvent;
+ var kHideAndShowEvents = kHideEvents | kShowEvent;
+
+ /**
+ * Base class to test mutation a11y events.
+ *
+ * @param aNodeOrID [in] node invoker's action is executed for
+ * @param aEventTypes [in] events to register (see constants above)
+ * @param aDoNotExpectEvents [in] boolean indicates if events are expected
+ */
+ function mutateA11yTree(aNodeOrID, aEventTypes, aDoNotExpectEvents) {
+ // Interface
+ this.DOMNode = getNode(aNodeOrID);
+ this.doNotExpectEvents = aDoNotExpectEvents;
+ this.eventSeq = [];
+ this.unexpectedEventSeq = [];
+
+ /**
+ * Change default target (aNodeOrID) registered for the given event type.
+ */
+ this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget) {
+ var type = this.getA11yEventType(aEventType);
+ for (var idx = 0; idx < this.getEventSeq().length; idx++) {
+ if (this.getEventSeq()[idx].type == type) {
+ this.getEventSeq()[idx].target = aTarget;
+ return idx;
+ }
+ }
+ return -1;
+ };
+
+ /**
+ * Replace the default target currently registered for a given event type
+ * with the nodes in the targets array.
+ */
+ this.setTargets = function mutateA11yTree_setTargets(aEventType, aTargets) {
+ var targetIdx = this.setTarget(aEventType, aTargets[0]);
+
+ var type = this.getA11yEventType(aEventType);
+ for (var i = 1; i < aTargets.length; i++) {
+ let checker = new invokerChecker(type, aTargets[i]);
+ this.getEventSeq().splice(++targetIdx, 0, checker);
+ }
+ };
+
+ // Implementation
+ this.getA11yEventType = function mutateA11yTree_getA11yEventType(aEventType) {
+ if (aEventType == kReorderEvent)
+ return nsIAccessibleEvent.EVENT_REORDER;
+
+ if (aEventType == kHideEvent)
+ return nsIAccessibleEvent.EVENT_HIDE;
+
+ if (aEventType == kShowEvent)
+ return nsIAccessibleEvent.EVENT_SHOW;
+
+ return 0;
+ };
+
+ this.getEventSeq = function mutateA11yTree_getEventSeq() {
+ return this.doNotExpectEvents ? this.unexpectedEventSeq : this.eventSeq;
+ };
+
+ if (aEventTypes & kHideEvent) {
+ let checker = new invokerChecker(this.getA11yEventType(kHideEvent),
+ this.DOMNode);
+ this.getEventSeq().push(checker);
+ }
+
+ if (aEventTypes & kShowEvent) {
+ let checker = new invokerChecker(this.getA11yEventType(kShowEvent),
+ this.DOMNode);
+ this.getEventSeq().push(checker);
+ }
+
+ if (aEventTypes & kReorderEvent) {
+ let checker = new invokerChecker(this.getA11yEventType(kReorderEvent),
+ this.DOMNode.parentNode);
+ this.getEventSeq().push(checker);
+ }
+ }
+
+ /**
+ * Change CSS style for the given node.
+ */
+ function changeStyle(aNodeOrID, aProp, aValue, aEventTypes) {
+ this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false);
+
+ this.invoke = function changeStyle_invoke() {
+ this.DOMNode.style[aProp] = aValue;
+ };
+
+ this.getID = function changeStyle_getID() {
+ return aNodeOrID + " change style " + aProp + " on value " + aValue;
+ };
+ }
+
+ /**
+ * Change class name for the given node.
+ */
+ function changeClass(aParentNodeOrID, aNodeOrID, aClassName, aEventTypes) {
+ this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false);
+
+ this.invoke = function changeClass_invoke() {
+ this.parentDOMNode.className = aClassName;
+ };
+
+ this.getID = function changeClass_getID() {
+ return aNodeOrID + " change class " + aClassName;
+ };
+
+ this.parentDOMNode = getNode(aParentNodeOrID);
+ }
+
+ /**
+ * Clone the node and append it to its parent.
+ */
+ function cloneAndAppendToDOM(aNodeOrID, aEventTypes,
+ aTargetsFunc, aReorderTargetFunc) {
+ var eventTypes = aEventTypes || kShowEvents;
+ var doNotExpectEvents = (aEventTypes == kNoEvents);
+
+ this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes,
+ doNotExpectEvents);
+
+ this.invoke = function cloneAndAppendToDOM_invoke() {
+ var newElm = this.DOMNode.cloneNode(true);
+ newElm.removeAttribute("id");
+
+ var targets = aTargetsFunc ?
+ aTargetsFunc(newElm) : [newElm];
+ this.setTargets(kShowEvent, targets);
+
+ if (aReorderTargetFunc) {
+ var reorderTarget = aReorderTargetFunc(this.DOMNode);
+ this.setTarget(kReorderEvent, reorderTarget);
+ }
+
+ this.DOMNode.parentNode.appendChild(newElm);
+ };
+
+ this.getID = function cloneAndAppendToDOM_getID() {
+ return aNodeOrID + " clone and append to DOM.";
+ };
+ }
+
+ /**
+ * Removes the node from DOM.
+ */
+ function removeFromDOM(aNodeOrID, aEventTypes,
+ aTargetsFunc, aReorderTargetFunc) {
+ var eventTypes = aEventTypes || kHideEvents;
+ var doNotExpectEvents = (aEventTypes == kNoEvents);
+
+ this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes,
+ doNotExpectEvents);
+
+ this.invoke = function removeFromDOM_invoke() {
+ this.DOMNode.remove();
+ };
+
+ this.getID = function removeFromDOM_getID() {
+ return prettyName(aNodeOrID) + " remove from DOM.";
+ };
+
+ if (aTargetsFunc && (eventTypes & kHideEvent))
+ this.setTargets(kHideEvent, aTargetsFunc(this.DOMNode));
+
+ if (aReorderTargetFunc && (eventTypes & kReorderEvent))
+ this.setTarget(kReorderEvent, aReorderTargetFunc(this.DOMNode));
+ }
+
+ /**
+ * Clone the node and replace the original node by cloned one.
+ */
+ function cloneAndReplaceInDOM(aNodeOrID) {
+ this.__proto__ = new mutateA11yTree(aNodeOrID, kHideAndShowEvents,
+ false);
+
+ this.invoke = function cloneAndReplaceInDOM_invoke() {
+ this.DOMNode.parentNode.replaceChild(this.newElm, this.DOMNode);
+ };
+
+ this.getID = function cloneAndReplaceInDOM_getID() {
+ return aNodeOrID + " clone and replace in DOM.";
+ };
+
+ this.newElm = this.DOMNode.cloneNode(true);
+ this.newElm.removeAttribute("id");
+ this.setTarget(kShowEvent, this.newElm);
+ }
+
+ /**
+ * Trigger content insertion (flush layout), removal and insertion of
+ * the same element for the same parent.
+ */
+ function test1(aContainerID) {
+ this.divNode = document.createElement("div");
+ this.divNode.setAttribute("id", "div-test1");
+ this.containerNode = getNode(aContainerID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.divNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function test1_invoke() {
+ this.containerNode.appendChild(this.divNode);
+ getComputedStyle(this.divNode, "").color;
+ this.containerNode.removeChild(this.divNode);
+ this.containerNode.appendChild(this.divNode);
+ };
+
+ this.getID = function test1_getID() {
+ return "fuzzy test #1: content insertion (flush layout), removal and" +
+ "reinsertion";
+ };
+ }
+
+ /**
+ * Trigger content insertion (flush layout), removal and insertion of
+ * the same element for the different parents.
+ */
+ function test2(aContainerID, aTmpContainerID) {
+ this.divNode = document.createElement("div");
+ this.divNode.setAttribute("id", "div-test2");
+ this.containerNode = getNode(aContainerID);
+ this.tmpContainerNode = getNode(aTmpContainerID);
+ this.container = getAccessible(this.containerNode);
+ this.tmpContainer = getAccessible(this.tmpContainerNode);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.divNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_REORDER, this.tmpContainerNode),
+ ];
+
+ this.invoke = function test2_invoke() {
+ this.tmpContainerNode.appendChild(this.divNode);
+ getComputedStyle(this.divNode, "").color;
+ this.tmpContainerNode.removeChild(this.divNode);
+ this.containerNode.appendChild(this.divNode);
+ };
+
+ this.getID = function test2_getID() {
+ return "fuzzy test #2: content insertion (flush layout), removal and" +
+ "reinsertion under another container";
+ };
+ }
+
+ /**
+ * Content insertion (flush layout) and then removal (nothing was changed).
+ */
+ function test3(aContainerID) {
+ this.divNode = document.createElement("div");
+ this.divNode.setAttribute("id", "div-test3");
+ this.containerNode = getNode(aContainerID);
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_SHOW, this.divNode),
+ new invokerChecker(EVENT_HIDE, this.divNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function test3_invoke() {
+ this.containerNode.appendChild(this.divNode);
+ getComputedStyle(this.divNode, "").color;
+ this.containerNode.removeChild(this.divNode);
+ };
+
+ this.getID = function test3_getID() {
+ return "fuzzy test #3: content insertion (flush layout) and removal";
+ };
+ }
+
+ function insertReferredElm(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.firstChild; }, this.containerNode),
+ new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.lastChild; }, this.containerNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function insertReferredElm_invoke() {
+ let span = document.createElement("span");
+ span.setAttribute("id", "insertReferredElms_span");
+ let input = document.createElement("input");
+ input.setAttribute("aria-labelledby", "insertReferredElms_span");
+ this.containerNode.appendChild(span);
+ this.containerNode.appendChild(input);
+ };
+
+ this.getID = function insertReferredElm_getID() {
+ return "insert inaccessible element and then insert referring element to make it accessible";
+ };
+ }
+
+ function showHiddenParentOfVisibleChild() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("c4_child")),
+ new invokerChecker(EVENT_SHOW, getNode("c4_middle")),
+ new invokerChecker(EVENT_REORDER, getNode("c4")),
+ ];
+
+ this.invoke = function showHiddenParentOfVisibleChild_invoke() {
+ getNode("c4_middle").style.visibility = "visible";
+ };
+
+ this.getID = function showHiddenParentOfVisibleChild_getID() {
+ return "show hidden parent of visible child";
+ };
+ }
+
+ function hideNDestroyDoc() {
+ this.txt = null;
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, () => { return this.txt; }),
+ ];
+
+ this.invoke = function hideNDestroyDoc_invoke() {
+ this.txt = getAccessible("c5").firstChild.firstChild;
+ this.txt.DOMNode.remove();
+ };
+
+ this.check = function hideNDestroyDoc_check() {
+ getNode("c5").remove();
+ };
+
+ this.getID = function hideNDestroyDoc_getID() {
+ return "remove text node and destroy a document on hide event";
+ };
+ }
+
+ function hideHideNDestroyDoc() {
+ this.target = null;
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, () => { return this.target; }),
+ ];
+
+ this.invoke = function hideHideNDestroyDoc_invoke() {
+ var doc = getAccessible("c6").firstChild;
+ var l1 = doc.firstChild;
+ this.target = l1.firstChild;
+ var l2 = doc.lastChild;
+ l1.DOMNode.firstChild.remove();
+ l2.DOMNode.firstChild.remove();
+ };
+
+ this.check = function hideHideNDestroyDoc_check() {
+ getNode("c6").remove();
+ };
+
+ this.getID = function hideHideNDestroyDoc_getID() {
+ return "remove text nodes (2 events in the queue) and destroy a document on first hide event";
+ };
+ }
+
+ /**
+ * Target getters.
+ */
+ function getFirstChild(aNode) {
+ return [aNode.firstChild];
+ }
+ function getLastChild(aNode) {
+ return [aNode.lastChild];
+ }
+
+ function getNEnsureFirstChild(aNode) {
+ var node = aNode.firstChild;
+ getAccessible(node);
+ return [node];
+ }
+
+ function getNEnsureChildren(aNode) {
+ var children = [];
+ var node = aNode.firstChild;
+ do {
+ children.push(node);
+ getAccessible(node);
+ node = node.nextSibling;
+ } while (node);
+
+ return children;
+ }
+
+ function getParent(aNode) {
+ return aNode.parentNode;
+ }
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+ // enableLogging("events,verbose");
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // Show/hide events by changing of display style of accessible DOM node
+ // from 'inline' to 'none', 'none' to 'inline'.
+ let id = "link1";
+ getAccessible(id); // ensure accessible is created
+ gQueue.push(new changeStyle(id, "display", "none", kHideEvents));
+ gQueue.push(new changeStyle(id, "display", "inline", kShowEvents));
+
+ // Show/hide events by changing of visibility style of accessible DOM node
+ // from 'visible' to 'hidden', 'hidden' to 'visible'.
+ id = "link2";
+ getAccessible(id);
+ gQueue.push(new changeStyle(id, "visibility", "hidden", kHideEvents));
+ gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents));
+
+ // Show/hide events by changing of visibility style of accessible DOM node
+ // from 'collapse' to 'visible', 'visible' to 'collapse'.
+ id = "link4";
+ gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents));
+ gQueue.push(new changeStyle(id, "visibility", "collapse", kHideEvents));
+
+ // Show/hide events by adding new accessible DOM node and removing old one.
+ id = "link5";
+ gQueue.push(new cloneAndAppendToDOM(id));
+ gQueue.push(new removeFromDOM(id));
+
+ // No show/hide events by adding new not accessible DOM node and removing
+ // old one, no reorder event for their parent.
+ id = "child1";
+ gQueue.push(new cloneAndAppendToDOM(id, kNoEvents));
+ gQueue.push(new removeFromDOM(id, kNoEvents));
+
+ // Show/hide events by adding new accessible DOM node and removing
+ // old one, there is reorder event for their parent.
+ id = "child2";
+ gQueue.push(new cloneAndAppendToDOM(id));
+ gQueue.push(new removeFromDOM(id));
+
+ // Show/hide events by adding new DOM node containing accessible DOM and
+ // removing old one, there is reorder event for their parent.
+ id = "child3";
+ gQueue.push(new cloneAndAppendToDOM(id, kShowEvents, getFirstChild,
+ getParent));
+
+ // Hide event for accessible child of unaccessible removed DOM node and
+ // reorder event for its parent.
+ gQueue.push(new removeFromDOM(id, kHideEvents,
+ getNEnsureFirstChild, getParent));
+
+ // Hide events for accessible children of unaccessible removed DOM node
+ // and reorder event for its parent.
+ gQueue.push(new removeFromDOM("child4", kHideEvents,
+ getNEnsureChildren, getParent));
+
+ // Show/hide events by creating new accessible DOM node and replacing
+ // old one.
+ getAccessible("link6"); // ensure accessible is created
+ gQueue.push(new cloneAndReplaceInDOM("link6"));
+
+ // Show/hide events by changing class name on the parent node.
+ gQueue.push(new changeClass("container2", "link7", "", kShowEvents));
+ gQueue.push(new changeClass("container2", "link7", "displayNone",
+ kHideEvents));
+
+ gQueue.push(new changeClass("container3", "link8", "", kShowEvents));
+ gQueue.push(new changeClass("container3", "link8", "visibilityHidden",
+ kHideEvents));
+
+ gQueue.push(new test1("testContainer"));
+ gQueue.push(new test2("testContainer", "testContainer2"));
+ gQueue.push(new test2("testContainer", "testNestedContainer"));
+ gQueue.push(new test3("testContainer"));
+ gQueue.push(new insertReferredElm("testContainer3"));
+ gQueue.push(new showHiddenParentOfVisibleChild());
+
+ gQueue.push(new hideNDestroyDoc());
+ gQueue.push(new hideHideNDestroyDoc());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=469985"
+ title=" turn the test from bug 354745 into mochitest">
+ Mozilla Bug 469985</a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=472662"
+ title="no reorder event when html:link display property is changed from 'none' to 'inline'">
+ Mozilla Bug 472662</a>
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275</a>
+ <a target="_blank"
+ title="Develop a way to handle visibility style"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125">
+ Mozilla Bug 606125</a>
+ <a target="_blank"
+ title="Update accessible tree on content insertion after layout"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015">
+ Mozilla Bug 498015</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <div id="testContainer">
+ <a id="link1" href="http://www.google.com">Link #1</a>
+ <a id="link2" href="http://www.google.com">Link #2</a>
+ <a id="link3" href="http://www.google.com">Link #3</a>
+ <a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a>
+ <a id="link5" href="http://www.google.com">Link #5</a>
+
+ <div id="container" role="list">
+ <span id="child1"></span>
+ <span id="child2" role="listitem"></span>
+ <span id="child3"><span role="listitem"></span></span>
+ <span id="child4"><span id="child4_1" role="listitem"></span><span id="child4_2" role="listitem"></span></span>
+ </div>
+
+ <a id="link6" href="http://www.google.com">Link #6</a>
+
+ <div id="container2" class="displayNone"><a id="link7">Link #7</a></div>
+ <div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div>
+ <div id="testNestedContainer"></div>
+ </div>
+ <div id="testContainer2"></div>
+ <div id="testContainer3"></div>
+
+ <div id="c4">
+ <div style="visibility:hidden" id="c4_middle">
+ <div style="visibility:visible" id="c4_child"></div>
+ </div>
+
+ <iframe id="c5" src="data:text/html,hey"></iframe>
+ <iframe id="c6" src="data:text/html,<label>l</label><label>l</label>"></iframe>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_namechange.html b/accessible/tests/mochitest/events/test_namechange.html
new file mode 100644
index 0000000000..840e2dfb4f
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_namechange.html
@@ -0,0 +1,185 @@
+<html>
+
+<head>
+ <title>Accessible name change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function setAttr(aID, aAttr, aValue, aChecker) {
+ this.eventSeq = [ aChecker ];
+ this.invoke = function setAttr_invoke() {
+ getNode(aID).setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function setAttr_getID() {
+ return "set attr '" + aAttr + "', value '" + aValue + "'";
+ };
+ }
+
+ /**
+ * No name change on an accessible, because the accessible is recreated.
+ */
+ function setAttr_recreate(aID, aAttr, aValue) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aID)),
+ new invokerChecker(EVENT_SHOW, aID),
+ ];
+ this.invoke = function setAttr_recreate_invoke() {
+ todo(false, "No accessible recreation should happen, just name change event");
+ getNode(aID).setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function setAttr_recreate_getID() {
+ return "set attr '" + aAttr + "', value '" + aValue + "'";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ async function doTests() {
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new setAttr("tst1", "aria-label", "hi",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "alt", "alt",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "aria-labelledby", "display",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst1")));
+
+ gQueue.push(new setAttr("tst2", "aria-labelledby", "display",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst2")));
+ gQueue.push(new setAttr("tst2", "alt", "alt",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+ gQueue.push(new setAttr("tst2", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+ gQueue.push(new setAttr("tst2", "aria-label", "hi",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+
+ // When `alt` attribute is added or removed from a broken img,
+ // the accessible is recreated.
+ gQueue.push(new setAttr_recreate("tst3", "alt", "one"));
+ // When an `alt` attribute is changed, there is a name change event.
+ gQueue.push(new setAttr("tst3", "alt", "two",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst3")));
+ gQueue.push(new setAttr("tst3", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst3")));
+
+ gQueue.push(new setAttr("tst4", "title", "title",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst4")));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ const labelledBy = getNode("labelledBy");
+ const label = getNode("label");
+ let nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy);
+ info("Changing text of aria-labelledby target");
+ label.textContent = "l2";
+ await nameChanged;
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy);
+ info("Adding node to aria-labelledby target");
+ label.innerHTML = '<p id="labelChild">l3</p>';
+ await nameChanged;
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy);
+ info("Changing text of aria-labelledby target's child");
+ getNode("labelChild").textContent = "l4";
+ await nameChanged;
+
+ const lateLabelledBy = getNode("lateLabelledBy");
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, lateLabelledBy);
+ info("Setting aria-labelledby");
+ lateLabelledBy.setAttribute("aria-labelledby", "lateLabel");
+ await nameChanged;
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, lateLabelledBy);
+ info("Changing text of late aria-labelledby target's child");
+ getNode("lateLabelChild").textContent = "l2";
+ await nameChanged;
+
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "listitem");
+ info("Changing textContent of listitem child");
+ // Changing textContent replaces the text leaf with a new one.
+ getNode("listitem").textContent = "world";
+ await nameChanged;
+
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "button");
+ info("Changing text of button's text leaf");
+ // Changing the text node's data changes the text without replacing the
+ // leaf.
+ getNode("button").firstChild.data = "after";
+ await nameChanged;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969"
+ title="Event not fired when description changes">
+ Bug 991969
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <img id="tst1" alt="initial" src="../moz.png">
+ <img id="tst2" src="../moz.png">
+ <img id="tst3">
+ <img id="tst4" src="../moz.png">
+
+ <div id="labelledBy" aria-labelledby="label"></div>
+ <div id="label">l1</div>
+
+ <div id="lateLabelledBy"></div>
+ <div id="lateLabel"><p id="lateLabelChild">l1</p></div>
+
+ <ul><li id="listitem">hello</li></ul>
+
+ <button id="button">before</button>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_namechange.xhtml b/accessible/tests/mochitest/events/test_namechange.xhtml
new file mode 100644
index 0000000000..a6dd8cb218
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_namechange.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Check name changed a11y event.
+ */
+ function nameChangeChecker(aMsg, aID)
+ {
+ this.type = EVENT_NAME_CHANGE;
+
+ function targetGetter()
+ {
+ return getAccessible(aID);
+ }
+ Object.defineProperty(this, "target", { get: targetGetter });
+
+ this.getID = function getID()
+ {
+ return aMsg + " name changed";
+ }
+ }
+
+ function changeRichListItemChild()
+ {
+ this.invoke = function changeRichListItemChild_invoke()
+ {
+ getNode('childcontent').setAttribute('value', 'Changed.');
+ }
+
+ this.eventSeq =
+ [
+ new nameChangeChecker("changeRichListItemChild: ", "listitem")
+ ];
+
+ this.getID = function changeRichListItemChild_getID()
+ {
+ return "changeRichListItemChild";
+ }
+ }
+
+ function doTest()
+ {
+ var queue = new eventQueue();
+ queue.push(new changeRichListItemChild());
+ queue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=986054"
+ title="Propagate name change events">
+ Mozilla Bug 986054
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <richlistbox>
+ <richlistitem id="listitem">
+ <description id="childcontent" value="This will be changed."/>
+ </richlistitem>
+ </richlistbox>
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_scroll.xhtml b/accessible/tests/mochitest/events/test_scroll.xhtml
new file mode 100644
index 0000000000..d3cc2a7bda
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_scroll.xhtml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../promisified-events.js" />
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ function matchesAnchorJumpInTabDocument(aTabIdx) {
+ return evt => {
+ let tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument();
+ let anchorAcc = getAccessible(tabDoc.querySelector("a[name='link1']"));
+ return anchorAcc == evt.accessible;
+ }
+ }
+
+ function matchesTabDocumentAt(aTabIdx) {
+ return evt => {
+ let tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument();
+ return getAccessible(tabDoc) == evt.accessible;
+ }
+ }
+
+ async function doTest() {
+ const kURL = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#link1";
+ let evtProm = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument],
+ [EVENT_SCROLLING_START, matchesAnchorJumpInTabDocument()],
+ ], "Load foreground tab then scroll to anchor");
+
+ tabBrowser().loadURI(Services.io.newURI(kURL), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ // Flush layout, so as to guarantee that the a11y tree is constructed.
+ browserDocument().documentElement.getBoundingClientRect();
+ await evtProm;
+
+
+ evtProm = waitForEvents({
+ expected: [[EVENT_DOCUMENT_LOAD_COMPLETE, matchesTabDocumentAt(1)]],
+ unexpected: [[EVENT_SCROLLING_START, matchesAnchorJumpInTabDocument(1)]],
+ }, "Load background tab, don't dispatch scroll to anchor event");
+
+ tabBrowser().addTab(kURL, {
+ referrerURI: null,
+ charset: "",
+ postData: null,
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ // Flush layout, so as to guarantee that the a11y tree is constructed.
+ browserDocument().documentElement.getBoundingClientRect();
+ await evtProm;
+
+ evtProm = waitForEvent(EVENT_SCROLLING_START,
+ matchesAnchorJumpInTabDocument(),
+ "Scroll to anchor event dispatched when switching to loaded doc.");
+ tabBrowser().selectTabAtIndex(1);
+ await evtProm;
+
+ closeBrowserWindow();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=691734"
+ title="Make sure scrolling start event is fired when document receive focus">
+ Mozilla Bug 691734
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_scroll_caret.xhtml b/accessible/tests/mochitest/events/test_scroll_caret.xhtml
new file mode 100644
index 0000000000..f0f0fccfb2
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_scroll_caret.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ function getAnchorJumpInTabDocument(aTabIdx)
+ {
+ var tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument();
+ return tabDoc.querySelector("h1[id='heading_1']");
+ }
+
+ function loadTab(aURL)
+ {
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument),
+ new asyncCaretMoveChecker(0, getAnchorJumpInTabDocument)
+ ];
+
+ this.invoke = function loadTab_invoke()
+ {
+ tabBrowser().loadURI(Services.io.newURI(aURL), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+
+ this.getID = function loadTab_getID()
+ {
+ return "load tab: " + aURL;
+ }
+ }
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ var url = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#heading_1";
+ gQueue.push(new loadTab(url));
+ gQueue.onFinish = function() { closeBrowserWindow(); }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1056459"
+ title="Make sure caret move event is fired when document receive focus">
+ Mozilla Bug 1056459
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_selection.html b/accessible/tests/mochitest/events/test_selection.html
new file mode 100644
index 0000000000..a749dd9c4c
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_selection.html
@@ -0,0 +1,109 @@
+<html>
+
+<head>
+ <title>Accessible selection event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+
+ // closed combobox
+ gQueue.push(new synthFocus("combobox"));
+ gQueue.push(new synthDownKey("cb1_item1",
+ selChangeSeq("cb1_item1", "cb1_item2")));
+
+ // listbox
+ gQueue.push(new synthClick("lb1_item1",
+ new invokerChecker(EVENT_SELECTION, "lb1_item1")));
+ gQueue.push(new synthDownKey("lb1_item1",
+ selChangeSeq("lb1_item1", "lb1_item2")));
+
+ // multiselectable listbox
+ gQueue.push(new synthClick("lb2_item1",
+ selChangeSeq(null, "lb2_item1")));
+ gQueue.push(new synthDownKey("lb2_item1",
+ selAddSeq("lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthUpKey("lb2_item2",
+ selRemoveSeq("lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true },
+ selRemoveSeq("lb2_item1")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302"
+ title="Incorrect selection events in HTML, XUL and ARIA">
+ Bug 414302
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=810268"
+ title="There's no way to know unselected item when selection in single selection was changed">
+ Bug 810268
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="combobox">
+ <option id="cb1_item1" value="mushrooms">mushrooms
+ <option id="cb1_item2" value="greenpeppers">green peppers
+ <option id="cb1_item3" value="onions" id="onions">onions
+ <option id="cb1_item4" value="tomatoes">tomatoes
+ <option id="cb1_item5" value="olives">olives
+ </select>
+
+ <select id="listbox" size=5>
+ <option id="lb1_item1" value="mushrooms">mushrooms
+ <option id="lb1_item2" value="greenpeppers">green peppers
+ <option id="lb1_item3" value="onions" id="onions">onions
+ <option id="lb1_item4" value="tomatoes">tomatoes
+ <option id="lb1_item5" value="olives">olives
+ </select>
+
+ <p>Pizza</p>
+ <select id="listbox2" multiple size=5>
+ <option id="lb2_item1" value="mushrooms">mushrooms
+ <option id="lb2_item2" value="greenpeppers">green peppers
+ <option id="lb2_item3" value="onions" id="onions">onions
+ <option id="lb2_item4" value="tomatoes">tomatoes
+ <option id="lb2_item5" value="olives">olives
+ </select>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_selection.xhtml b/accessible/tests/mochitest/events/test_selection.xhtml
new file mode 100644
index 0000000000..9c34ddf286
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_selection.xhtml
@@ -0,0 +1,254 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Selection event tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ function advanceTab(aTabsID, aDirection, aNextTabID)
+ {
+ var eventSeq1 = [
+ new invokerChecker(EVENT_SELECTION, aNextTabID)
+ ]
+ defineScenario(this, eventSeq1);
+
+ var eventSeq2 = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aNextTabID)),
+ new invokerChecker(EVENT_SHOW, aNextTabID)
+ ];
+ defineScenario(this, eventSeq2);
+
+ this.invoke = function advanceTab_invoke()
+ {
+ todo(false, "No accessible recreation should happen, just selection event");
+ getNode(aTabsID).advanceSelectedTab(aDirection, true);
+ }
+
+ this.getID = function synthFocus_getID()
+ {
+ return "advanceTab on " + prettyName(aTabsID) + " to " + prettyName(aNextTabID);
+ }
+ }
+
+ function select4FirstItems(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(1)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(2)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(3))
+ ];
+
+ this.invoke = function select4FirstItems_invoke()
+ {
+ synthesizeKey("VK_DOWN", { shiftKey: true }); // selects two items
+ synthesizeKey("VK_DOWN", { shiftKey: true });
+ synthesizeKey("VK_DOWN", { shiftKey: true });
+ }
+
+ this.getID = function select4FirstItems_getID()
+ {
+ return "select 4 first items for " + prettyName(aID);
+ }
+ }
+
+ function unselect4FirstItems(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(3)),
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(2)),
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(1)),
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0))
+ ];
+
+ this.invoke = function unselect4FirstItems_invoke()
+ {
+ synthesizeKey("VK_UP", { shiftKey: true });
+ synthesizeKey("VK_UP", { shiftKey: true });
+ synthesizeKey("VK_UP", { shiftKey: true });
+ synthesizeKey(" ", { ctrlKey: true }); // unselect first item
+ }
+
+ this.getID = function unselect4FirstItems_getID()
+ {
+ return "unselect 4 first items for " + prettyName(aID);
+ }
+ }
+
+ function selectAllItems(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode))
+ ];
+
+ this.invoke = function selectAllItems_invoke()
+ {
+ synthesizeKey("VK_END", { shiftKey: true });
+ }
+
+ this.getID = function selectAllItems_getID()
+ {
+ return "select all items for " + prettyName(aID);
+ }
+ }
+
+ function unselectAllItemsButFirst(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode))
+ ];
+
+ this.invoke = function unselectAllItemsButFirst_invoke()
+ {
+ synthesizeKey("VK_HOME", { shiftKey: true });
+ }
+
+ this.getID = function unselectAllItemsButFirst_getID()
+ {
+ return "unselect all items for " + prettyName(aID);
+ }
+ }
+
+ function unselectSelectItem(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0))
+ ];
+
+ this.invoke = function unselectSelectItem_invoke()
+ {
+ synthesizeKey(" ", { ctrlKey: true }); // select item
+ synthesizeKey(" ", { ctrlKey: true }); // unselect item
+ }
+
+ this.getID = function unselectSelectItem_getID()
+ {
+ return "unselect and then select first item for " + prettyName(aID);
+ }
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ //enableLogging("events");
+ //gA11yEventDumpToConsole = true; // debuggin
+
+ function doTests()
+ {
+ gQueue = new eventQueue();
+
+ //////////////////////////////////////////////////////////////////////////
+ // tabbox
+ gQueue.push(new advanceTab("tabs", 1, "tab3"));
+
+ //////////////////////////////////////////////////////////////////////////
+ // single selection listbox, the first item is selected by default
+
+ gQueue.push(new synthClick("lb1_item2",
+ new invokerChecker(EVENT_SELECTION, "lb1_item2")));
+ gQueue.push(new synthUpKey("lb1_item2",
+ new invokerChecker(EVENT_SELECTION, "lb1_item1")));
+ gQueue.push(new synthDownKey("lb1_item1",
+ new invokerChecker(EVENT_SELECTION, "lb1_item2")));
+
+ //////////////////////////////////////////////////////////////////////////
+ // multiselectable listbox
+ gQueue.push(new synthClick("lb2_item1",
+ new invokerChecker(EVENT_SELECTION, "lb2_item1")));
+ gQueue.push(new synthDownKey("lb2_item1",
+ new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthUpKey("lb2_item2",
+ new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true },
+ new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1")));
+
+ //////////////////////////////////////////////////////////////////////////
+ // selection event coalescence
+
+ // fire 4 selection_add events
+ gQueue.push(new select4FirstItems("listbox2"));
+ // fire 4 selection_remove events
+ gQueue.push(new unselect4FirstItems("listbox2"));
+ // fire selection_within event
+ gQueue.push(new selectAllItems("listbox2"));
+ // fire selection_within event
+ gQueue.push(new unselectAllItemsButFirst("listbox2"));
+ // fire selection_remove/add events
+ gQueue.push(new unselectSelectItem("listbox2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302"
+ title="Incorrect selection events in HTML, XUL and ARIA">
+ Mozilla Bug 414302
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <tabbox id="tabbox" selectedIndex="1">
+ <tabs id="tabs">
+ <tab id="tab1" label="tab1"/>
+ <tab id="tab2" label="tab2"/>
+ <tab id="tab3" label="tab3"/>
+ <tab id="tab4" label="tab4"/>
+ </tabs>
+ <tabpanels>
+ <tabpanel><!-- tabpanel First elements go here --></tabpanel>
+ <tabpanel><button id="b1" label="b1"/></tabpanel>
+ <tabpanel><button id="b2" label="b2"/></tabpanel>
+ <tabpanel></tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ <richlistbox id="listbox">
+ <richlistitem id="lb1_item1"><label value="item1"/></richlistitem>
+ <richlistitem id="lb1_item2"><label value="item2"/></richlistitem>
+ </richlistbox>
+
+ <richlistbox id="listbox2" seltype="multiple">
+ <richlistitem id="lb2_item1"><label value="item1"/></richlistitem>
+ <richlistitem id="lb2_item2"><label value="item2"/></richlistitem>
+ <richlistitem id="lb2_item3"><label value="item3"/></richlistitem>
+ <richlistitem id="lb2_item4"><label value="item4"/></richlistitem>
+ <richlistitem id="lb2_item5"><label value="item5"/></richlistitem>
+ <richlistitem id="lb2_item6"><label value="item6"/></richlistitem>
+ <richlistitem id="lb2_item7"><label value="item7"/></richlistitem>
+ </richlistbox>
+
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_selection_aria.html b/accessible/tests/mochitest/events/test_selection_aria.html
new file mode 100644
index 0000000000..c479868e03
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_selection_aria.html
@@ -0,0 +1,122 @@
+<html>
+
+<head>
+ <title>ARIA selection event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function selectItem(aSelectID, aItemID) {
+ this.selectNode = getNode(aSelectID);
+ this.itemNode = getNode(aItemID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION, aItemID),
+ ];
+
+ this.invoke = function selectItem_invoke() {
+ var itemNode = this.selectNode.querySelector("*[aria-selected='true']");
+ if (itemNode)
+ itemNode.removeAttribute("aria-selected");
+
+ this.itemNode.setAttribute("aria-selected", "true");
+ };
+
+ this.getID = function selectItem_getID() {
+ return "select item " + prettyName(aItemID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new selectItem("tablist", "tab1"));
+ gQueue.push(new selectItem("tablist", "tab2"));
+
+ gQueue.push(new selectItem("tree", "treeitem1"));
+ gQueue.push(new selectItem("tree", "treeitem1a"));
+ gQueue.push(new selectItem("tree", "treeitem1a1"));
+
+ gQueue.push(new selectItem("tree2", "tree2item1"));
+ gQueue.push(new selectItem("tree2", "tree2item1a"));
+ gQueue.push(new selectItem("tree2", "tree2item1a1"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=569653"
+ title="Make selection events async">
+ Mozilla Bug 569653
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=804040"
+ title="Selection event not fired when selection of ARIA tab changes">
+ Mozilla Bug 804040
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="tablist" id="tablist">
+ <div role="tab" id="tab1">tab1</div>
+ <div role="tab" id="tab2">tab2</div>
+ </div>
+
+ <div id="tree" role="tree">
+ <div id="treeitem1" role="treeitem">Canada
+ <div id="treeitem1a" role="treeitem">- Ontario
+ <div id="treeitem1a1" role="treeitem">-- Toronto</div>
+ </div>
+ <div id="treeitem1b" role="treeitem">- Manitoba</div>
+ </div>
+ <div id="treeitem2" role="treeitem">Germany</div>
+ <div id="treeitem3" role="treeitem">Russia</div>
+ </div>
+
+ <div id="tree2" role="tree" aria-multiselectable="true">
+ <div id="tree2item1" role="treeitem">Canada
+ <div id="tree2item1a" role="treeitem">- Ontario
+ <div id="tree2item1a1" role="treeitem">-- Toronto</div>
+ </div>
+ <div id="tree2item1b" role="treeitem">- Manitoba</div>
+ </div>
+ <div id="tree2item2" role="treeitem">Germany</div>
+ <div id="tree2item3" role="treeitem">Russia</div>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_statechange.html b/accessible/tests/mochitest/events/test_statechange.html
new file mode 100644
index 0000000000..0642851408
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_statechange.html
@@ -0,0 +1,565 @@
+<html>
+
+<head>
+ <title>Accessible state change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ async function openNode(aIDDetails, aIDSummary, aIsOpen) {
+ let p = waitForStateChange(aIDSummary, STATE_EXPANDED, aIsOpen, false);
+ if (aIsOpen) {
+ getNode(aIDDetails).setAttribute("open", "");
+ } else {
+ getNode(aIDDetails).removeAttribute("open");
+ }
+ await p;
+ }
+
+ async function makeEditableDoc(aDocNode, aIsEnabled) {
+ let p = waitForStateChange(aDocNode, EXT_STATE_EDITABLE, true, true);
+ aDocNode.designMode = "on";
+ await p;
+ }
+
+ async function invalidInput(aNodeOrID) {
+ let p = waitForStateChange(aNodeOrID, STATE_INVALID, true, false);
+ getNode(aNodeOrID).value = "I am not an email";
+ await p;
+ }
+
+ async function changeCheckInput(aID, aIsChecked) {
+ let p = waitForStateChange(aID, STATE_CHECKED, aIsChecked, false);
+ getNode(aID).checked = aIsChecked;
+ await p;
+ }
+
+ async function changeRequiredState(aID, aIsRequired) {
+ let p = waitForStateChange(aID, STATE_REQUIRED, aIsRequired, false);
+ getNode(aID).required = aIsRequired;
+ await p;
+ }
+
+ async function stateChangeOnFileInput(aID, aAttr, aValue,
+ aState, aIsExtraState, aIsEnabled) {
+ let fileControlNode = getNode(aID);
+ let browseButton = getAccessible(fileControlNode);
+ let p = waitForStateChange(
+ browseButton, aState, aIsEnabled, aIsExtraState
+ );
+ fileControlNode.setAttribute(aAttr, aValue);
+ await p;
+ }
+
+ function toggleSentinel() {
+ let sentinel = getNode("sentinel");
+ if (sentinel.hasAttribute("aria-busy")) {
+ sentinel.removeAttribute("aria-busy");
+ } else {
+ sentinel.setAttribute("aria-busy", "true");
+ }
+ }
+
+ async function toggleStateChange(aID, aAttr, aState, aIsExtraState) {
+ let p = waitForEvents([
+ stateChangeEventArgs(aID, aState, true, aIsExtraState),
+ [EVENT_STATE_CHANGE, "sentinel"]
+ ]);
+ getNode(aID).setAttribute(aAttr, "true");
+ toggleSentinel();
+ await p;
+ p = waitForEvents([
+ stateChangeEventArgs(aID, aState, false, aIsExtraState),
+ [EVENT_STATE_CHANGE, "sentinel"]
+ ]);
+ getNode(aID).setAttribute(aAttr, "false");
+ toggleSentinel();
+ await p;
+ }
+
+ async function dupeStateChange(aID, aAttr, aValue,
+ aState, aIsExtraState, aIsEnabled) {
+ let p = waitForEvents([
+ stateChangeEventArgs(aID, aState, aIsEnabled, aIsExtraState),
+ [EVENT_STATE_CHANGE, "sentinel"]
+ ]);
+ getNode(aID).setAttribute(aAttr, aValue);
+ getNode(aID).setAttribute(aAttr, aValue);
+ toggleSentinel();
+ await p;
+ }
+
+ async function oppositeStateChange(aID, aAttr, aState, aIsExtraState) {
+ let p = waitForEvents({
+ expected: [[EVENT_STATE_CHANGE, "sentinel"]],
+ unexpected: [
+ stateChangeEventArgs(aID, aState, false, aIsExtraState),
+ stateChangeEventArgs(aID, aState, true, aIsExtraState)
+ ]
+ });
+ getNode(aID).setAttribute(aAttr, "false");
+ getNode(aID).setAttribute(aAttr, "true");
+ toggleSentinel();
+ await p;
+ }
+
+ /**
+ * Change concomitant ARIA and native attribute at once.
+ */
+ async function echoingStateChange(aID, aARIAAttr, aAttr, aValue,
+ aState, aIsExtraState, aIsEnabled) {
+ let p = waitForStateChange(aID, aState, aIsEnabled, aIsExtraState);
+ if (aValue == null) {
+ getNode(aID).removeAttribute(aARIAAttr);
+ getNode(aID).removeAttribute(aAttr);
+ } else {
+ getNode(aID).setAttribute(aARIAAttr, aValue);
+ getNode(aID).setAttribute(aAttr, aValue);
+ }
+ await p;
+ }
+
+ async function testHasPopup() {
+ let p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false);
+ getNode("popupButton").setAttribute("aria-haspopup", "true");
+ await p;
+
+ p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false);
+ getNode("popupButton").setAttribute("aria-haspopup", "false");
+ await p;
+
+ p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false);
+ getNode("popupButton").setAttribute("aria-haspopup", "true");
+ await p;
+
+ p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false);
+ getNode("popupButton").removeAttribute("aria-haspopup");
+ await p;
+ }
+
+ async function testDefaultSubmitChange() {
+ testStates("default-button",
+ STATE_DEFAULT, 0,
+ 0, 0,
+ "button should have DEFAULT state");
+ let button = document.createElement("button");
+ button.textContent = "new default";
+ let p = waitForStateChange("default-button", STATE_DEFAULT, false, false);
+ getNode("default-button").before(button);
+ await p;
+ testStates("default-button",
+ 0, 0,
+ STATE_DEFAULT, 0,
+ "button should not have DEFAULT state");
+ p = waitForStateChange("default-button", STATE_DEFAULT, true, false);
+ button.remove();
+ await p;
+ testStates("default-button",
+ STATE_DEFAULT, 0,
+ 0, 0,
+ "button should have DEFAULT state");
+ }
+
+ async function testReadOnly() {
+ let p = waitForStateChange("email", STATE_READONLY, true, false);
+ getNode("email").setAttribute("readonly", "true");
+ await p;
+ p = waitForStateChange("email", STATE_READONLY, false, false);
+ getNode("email").removeAttribute("readonly");
+ await p;
+ }
+
+ async function testReadonlyUntilEditable() {
+ testStates("article",
+ STATE_READONLY, 0,
+ 0, EXT_STATE_EDITABLE,
+ "article is READONLY and not EDITABLE");
+ let p = waitForEvents([
+ stateChangeEventArgs("article", STATE_READONLY, false, false),
+ stateChangeEventArgs("article", EXT_STATE_EDITABLE, true, true)]);
+ getNode("article").contentEditable = "true";
+ await p;
+ testStates("article",
+ 0, EXT_STATE_EDITABLE,
+ STATE_READONLY, 0,
+ "article is EDITABLE and not READONLY");
+ p = waitForEvents([
+ stateChangeEventArgs("article", STATE_READONLY, true, false),
+ stateChangeEventArgs("article", EXT_STATE_EDITABLE, false, true)]);
+ getNode("article").contentEditable = "false";
+ await p;
+ testStates("article",
+ STATE_READONLY, 0,
+ 0, EXT_STATE_EDITABLE,
+ "article is READONLY and not EDITABLE");
+ }
+
+ async function testAnimatedImage() {
+ testStates("animated-image",
+ STATE_ANIMATED, 0,
+ 0, 0,
+ "image should be animated 1");
+ let p = waitForStateChange("animated-image", STATE_ANIMATED, false, false);
+ getNode("animated-image").src = "../animated-gif-finalframe.gif";
+ await p;
+ testStates("animated-image",
+ 0, 0,
+ STATE_ANIMATED, 0,
+ "image should not be animated 2");
+ p = waitForStateChange("animated-image", STATE_ANIMATED, true, false);
+ getNode("animated-image").src = "../animated-gif.gif";
+ await p;
+ testStates("animated-image",
+ STATE_ANIMATED, 0,
+ 0, 0,
+ "image should be animated 3");
+ }
+
+ async function testImageLoad() {
+ let img = document.createElement("img");
+ img.id = "image";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ img.src = "http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs";
+ let p = waitForEvent(EVENT_SHOW, "image");
+ getNode("eventdump").before(img);
+ await p;
+ testStates("image",
+ STATE_INVISIBLE, 0,
+ 0, 0,
+ "image should be invisible");
+ p = waitForStateChange("image", STATE_INVISIBLE, false, false);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await fetch("http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete");
+ await p;
+ testStates("image",
+ 0, 0,
+ STATE_INVISIBLE, 0,
+ "image should be invisible");
+ }
+
+ async function testMultiSelectable(aID, aAttribute) {
+ testStates(aID,
+ 0, 0,
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0,
+ `${aID} should not be multiselectable`);
+ let p = waitForEvents([
+ stateChangeEventArgs(aID, STATE_MULTISELECTABLE, true, false),
+ stateChangeEventArgs(aID, STATE_EXTSELECTABLE, true, false),
+ ]);
+ getNode(aID).setAttribute(aAttribute, true);
+ await p;
+ testStates(aID,
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0,
+ 0, 0,
+ `${aID} should not be multiselectable`);
+ p = waitForEvents([
+ stateChangeEventArgs(aID, STATE_MULTISELECTABLE, false, false),
+ stateChangeEventArgs(aID, STATE_EXTSELECTABLE, false, false),
+ ]);
+ getNode(aID).removeAttribute(aAttribute);
+ await p;
+ testStates(aID,
+ 0, 0,
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0,
+ `${aID} should not be multiselectable`);
+ }
+
+ async function testAutocomplete() {
+ // A text input will have autocomplete via browser's form autofill...
+ testStates("input",
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ 0, 0,
+ "input supports autocompletion");
+ // unless it is explicitly turned off.
+ testStates("input-autocomplete-off",
+ 0, 0,
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ "input-autocomplete-off does not support autocompletion");
+ // An input with a datalist will always have autocomplete.
+ testStates("input-list",
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ 0, 0,
+ "input-list supports autocompletion");
+ // password fields don't get autocomplete.
+ testStates("input-password",
+ 0, 0,
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ "input-autocomplete-off does not support autocompletion");
+
+ let p = waitForEvents({
+ expected: [
+ // Setting the form's autocomplete attribute to "off" will cause
+ // "input" to lost its autocomplete state.
+ stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true)
+ ],
+ unexpected: [
+ // "input-list" should preserve its autocomplete state regardless of
+ // forms "autocomplete" attribute
+ [EVENT_STATE_CHANGE, "input-list"],
+ // "input-autocomplete-off" already has its autocomplte off, so no state
+ // change here.
+ [EVENT_STATE_CHANGE, "input-autocomplete-off"],
+ // passwords never get autocomplete
+ [EVENT_STATE_CHANGE, "input-password"],
+ ]
+ });
+
+ getNode("form").setAttribute("autocomplete", "off");
+
+ await p;
+
+ // Same when we remove the form's autocomplete attribute.
+ p = waitForEvents({
+ expected: [stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true)],
+ unexpected: [
+ [EVENT_STATE_CHANGE, "input-list"],
+ [EVENT_STATE_CHANGE, "input-autocomplete-off"],
+ [EVENT_STATE_CHANGE, "input-password"],
+ ]
+ });
+
+ getNode("form").removeAttribute("autocomplete");
+
+ await p;
+
+ p = waitForEvents({
+ expected: [
+ // Forcing autocomplete off on an input will cause a state change
+ stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true),
+ // Associating a datalist with an autocomplete=off input
+ // will give it an autocomplete state, regardless.
+ stateChangeEventArgs("input-autocomplete-off", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true),
+ // XXX: datalist inputs also get a HASPOPUP state, the inconsistent
+ // use of that state is inexplicable, but lets make sure we fire state
+ // change events for it anyway.
+ stateChangeEventArgs("input-autocomplete-off", STATE_HASPOPUP, true, false),
+ ],
+ unexpected: [
+ // Forcing autocomplete off with a dataset input does nothing.
+ [EVENT_STATE_CHANGE, "input-list"],
+ // passwords never get autocomplete
+ [EVENT_STATE_CHANGE, "input-password"],
+ ]
+ });
+
+ getNode("input").setAttribute("autocomplete", "off");
+ getNode("input-list").setAttribute("autocomplete", "off");
+ getNode("input-autocomplete-off").setAttribute("list", "browsers");
+ getNode("input-password").setAttribute("autocomplete", "off");
+
+ await p;
+ }
+
+ async function doTests() {
+ // Disable mixed-content upgrading as this test is expecting an HTTP load
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.upgrade_display_content", false]]
+ });
+
+ // Test opening details objects
+ await openNode("detailsOpen", "summaryOpen", true);
+ await openNode("detailsOpen", "summaryOpen", false);
+ await openNode("detailsOpen1", "summaryOpen1", true);
+ await openNode("detailsOpen2", "summaryOpen2", true);
+ await openNode("detailsOpen3", "summaryOpen3", true);
+ await openNode("detailsOpen4", "summaryOpen4", true);
+ await openNode("detailsOpen5", "summaryOpen5", true);
+ await openNode("detailsOpen6", "summaryOpen6", true);
+
+ // Test delayed editable state change
+ var doc = document.getElementById("iframe").contentDocument;
+ await makeEditableDoc(doc);
+
+ // invalid state change
+ await invalidInput("email");
+
+ // checked state change
+ await changeCheckInput("checkbox", true);
+ await changeCheckInput("checkbox", false);
+ await changeCheckInput("radio", true);
+ await changeCheckInput("radio", false);
+
+ // required state change
+ await changeRequiredState("checkbox", true);
+
+ // file input inherited state changes
+ await stateChangeOnFileInput("file", "aria-busy", "true",
+ STATE_BUSY, false, true);
+ await stateChangeOnFileInput("file", "aria-required", "true",
+ STATE_REQUIRED, false, true);
+ await stateChangeOnFileInput("file", "aria-invalid", "true",
+ STATE_INVALID, false, true);
+
+ await dupeStateChange("div", "aria-busy", "true",
+ STATE_BUSY, false, true);
+ await oppositeStateChange("div", "aria-busy",
+ STATE_BUSY, false);
+
+ await echoingStateChange("text1", "aria-disabled", "disabled", "true",
+ EXT_STATE_ENABLED, true, false);
+ await echoingStateChange("text1", "aria-disabled", "disabled", null,
+ EXT_STATE_ENABLED, true, true);
+
+ await testReadOnly();
+
+ await testReadonlyUntilEditable();
+
+ await testHasPopup();
+
+ await toggleStateChange("textbox", "aria-multiline", EXT_STATE_MULTI_LINE, true);
+
+ await testDefaultSubmitChange();
+
+ await testAnimatedImage();
+
+ await testImageLoad();
+
+ await testMultiSelectable("listbox", "aria-multiselectable");
+
+ await testMultiSelectable("select", "multiple");
+
+ await testAutocomplete();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<style>
+ details.openBefore::before{
+ content: "before detail content: ";
+ background: blue;
+ }
+ summary.openBefore::before{
+ content: "before summary content: ";
+ background: green;
+ }
+ details.openAfter::after{
+ content: " :after detail content";
+ background: blue;
+ }
+ summary.openAfter::after{
+ content: " :after summary content";
+ background: green;
+ }
+</style>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=564471"
+ title="Make state change events async">
+ Bug 564471
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=555728"
+ title="Fire a11y event based on HTML5 constraint validation">
+ Bug 555728
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017"
+ title="File input control should be propogate states to descendants">
+ Bug 699017
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=788389"
+ title="Fire statechange event whenever checked state is changed not depending on focused state">
+ Bug 788389
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=926812"
+ title="State change event not fired when both disabled and aria-disabled are toggled">
+ Bug 926812
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- open -->
+ <details id="detailsOpen"><summary id="summaryOpen">open</summary>details can be opened</details>
+ <details id="detailsOpen1">order doesn't matter<summary id="summaryOpen1">open</summary></details>
+ <details id="detailsOpen2"><div>additional elements don't matter</div><summary id="summaryOpen2">open</summary></details>
+ <details id="detailsOpen3" class="openBefore"><summary id="summaryOpen3">summary</summary>content</details>
+ <details id="detailsOpen4" class="openAfter"><summary id="summaryOpen4">summary</summary>content</details>
+ <details id="detailsOpen5"><summary id="summaryOpen5" class="openBefore">summary</summary>content</details>
+ <details id="detailsOpen6"><summary id="summaryOpen6" class="openAfter">summary</summary>content</details>
+
+
+ <div id="testContainer">
+ <iframe id="iframe"></iframe>
+ </div>
+
+ <input id="email" type='email'>
+
+ <input id="checkbox" type="checkbox">
+ <input id="radio" type="radio">
+
+ <input id="file" type="file">
+
+ <div id="div"></div>
+
+ <!-- A sentinal guards from events of interest being fired after it emits a state change -->
+ <div id="sentinel"></div>
+
+ <input id="text1">
+
+ <div id="textbox" role="textbox" aria-multiline="false">hello</div>
+
+ <form id="form">
+ <button id="default-button">hello</button>
+ <button>world</button>
+ <input id="input">
+ <input id="input-autocomplete-off" autocomplete="off">
+ <input id="input-list" list="browsers">
+ <input id="input-password" type="password">
+ <datalist id="browsers">
+ <option value="Internet Explorer">
+ <option value="Firefox">
+ <option value="Google Chrome">
+ <option value="Opera">
+ <option value="Safari">
+ </datalist>
+ </form>
+
+ <div id="article" role="article">hello</div>
+
+ <img id="animated-image" src="../animated-gif.gif">
+
+ <ul id="listbox" role="listbox">
+ <li role="option">one</li>
+ <li role="option">two</li>
+ <li role="option">three</li>
+ <li role="option">four</li>
+ <li role="option">five</li>
+ </ul>
+
+ <select id="select" size="2">
+ <option>one</option>
+ <option>two</option>
+ <option>three</option>
+ <option>four</option>
+ <option>five</option>
+ <option>size</option>
+ </select>
+
+ <div id="eventdump"></div>
+
+ <div id="eventdump"></div>
+ <button id="popupButton">action</button>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_statechange.xhtml b/accessible/tests/mochitest/events/test_statechange.xhtml
new file mode 100644
index 0000000000..4d63c664f1
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_statechange.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL state change event tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../promisified-events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function offscreenChangeEvent(acc, enabled) {
+ return [
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ return event.accessible == acc &&
+ scEvent.state == STATE_OFFSCREEN &&
+ scEvent.isEnabled == enabled;
+ }
+ ];
+ }
+
+ async function testTabpanels() {
+ const tabs = getNode("tabs");
+ is(tabs.selectedIndex, 0, "tab1 initially selected");
+ const panel1 = getAccessible("panel1");
+ testStates(panel1, 0, 0, STATE_OFFSCREEN);
+ const panel2 = getAccessible("panel2");
+ testStates(panel2, STATE_OFFSCREEN);
+ const panel3 = getAccessible("panel3");
+ testStates(panel3, STATE_OFFSCREEN);
+
+ let events = waitForEvents([
+ offscreenChangeEvent(panel1, true),
+ offscreenChangeEvent(panel2, false)
+ ]);
+ info("Selecting tab2");
+ tabs.selectedIndex = 1;
+ await events;
+
+ events = waitForEvents([
+ offscreenChangeEvent(panel2, true),
+ offscreenChangeEvent(panel3, false)
+ ]);
+ info("Selecting tab3");
+ tabs.selectedIndex = 2;
+ await events;
+
+ events = waitForEvents([
+ offscreenChangeEvent(panel3, true),
+ offscreenChangeEvent(panel1, false)
+ ]);
+ info("Selecting tab1");
+ tabs.selectedIndex = 0;
+ await events;
+ }
+
+ async function testPressed() {
+ const toolbarbuttonCheckbox = getNode("toolbarbuttonCheckbox");
+ testStates(toolbarbuttonCheckbox, 0, 0, STATE_PRESSED);
+ info("Checking toolbarbuttonCheckbox");
+ let changed = waitForStateChange(toolbarbuttonCheckbox, STATE_PRESSED, true);
+ toolbarbuttonCheckbox.setAttribute("checked", true);
+ await changed;
+ info("Unchecking toolbarbuttonCheckbox");
+ changed = waitForStateChange(toolbarbuttonCheckbox, STATE_PRESSED, false);
+ toolbarbuttonCheckbox.removeAttribute("checked");
+ await changed;
+ }
+
+ async function doTests() {
+ await testTabpanels();
+ await testPressed();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <tabbox id="tabbox" selectedIndex="0">
+ <tabs id="tabs">
+ <tab id="tab1" label="tab1"/>
+ <tab id="tab2" label="tab2"/>
+ <tab id="tab3" label="tab3"/>
+ </tabs>
+ <tabpanels>
+ <hbox id="panel1"><button label="b1"/></hbox>
+ <hbox id="panel2"><button label="b2"/></hbox>
+ <hbox id="panel3"><button label="b3"/></hbox>
+ </tabpanels>
+ </tabbox>
+
+ <toolbarbutton id="toolbarbuttonCheckbox" type="checkbox">toolbarbuttonCheckbox</toolbarbutton>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_text.html b/accessible/tests/mochitest/events/test_text.html
new file mode 100644
index 0000000000..8a0bd7f9a4
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_text.html
@@ -0,0 +1,310 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Base text remove invoker and checker.
+ */
+ function textRemoveInvoker(aID, aStart, aEnd, aText) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, false),
+ ];
+ }
+
+ function textInsertInvoker(aID, aStart, aEnd, aText) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, true),
+ ];
+ }
+
+ /**
+ * Remove inaccessible child node containing accessibles.
+ */
+ function removeChildSpan(aID) {
+ this.__proto__ = new textRemoveInvoker(aID, 0, 5, "33322");
+
+ this.invoke = function removeChildSpan_invoke() {
+ // remove HTML span, a first child of the node
+ this.DOMNode.firstChild.remove();
+ };
+
+ this.getID = function removeChildSpan_getID() {
+ return "Remove inaccessible span containing accessible nodes" + prettyName(aID);
+ };
+ }
+
+ /**
+ * Insert inaccessible child node containing accessibles.
+ */
+ function insertChildSpan(aID, aInsertAllTogether) {
+ this.__proto__ = new textInsertInvoker(aID, 0, 5, "33322");
+
+ this.invoke = function insertChildSpan_invoke() {
+ // <span><span>333</span><span>22</span></span>
+ if (aInsertAllTogether) {
+ let topSpan = document.createElement("span");
+ let fSpan = document.createElement("span");
+ fSpan.textContent = "333";
+ topSpan.appendChild(fSpan);
+ let sSpan = document.createElement("span");
+ sSpan.textContent = "22";
+ topSpan.appendChild(sSpan);
+
+ this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]);
+ } else {
+ let topSpan = document.createElement("span");
+ this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]);
+
+ let fSpan = document.createElement("span");
+ fSpan.textContent = "333";
+ topSpan.appendChild(fSpan);
+
+ let sSpan = document.createElement("span");
+ sSpan.textContent = "22";
+ topSpan.appendChild(sSpan);
+ }
+ };
+
+ this.getID = function insertChildSpan_getID() {
+ return "Insert inaccessible span containing accessibles" +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove child embedded accessible.
+ */
+ function removeChildDiv(aID) {
+ this.__proto__ = new textRemoveInvoker(aID, 5, 6, kEmbedChar);
+
+ this.invoke = function removeChildDiv_invoke() {
+ var childDiv = this.DOMNode.childNodes[1];
+
+ // Ensure accessible is created to get text remove event when it's
+ // removed.
+ getAccessible(childDiv);
+
+ this.DOMNode.removeChild(childDiv);
+ };
+
+ this.getID = function removeChildDiv_getID() {
+ return "Remove accessible div from the middle of text accessible " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Insert child embedded accessible.
+ */
+ function insertChildDiv(aID) {
+ this.__proto__ = new textInsertInvoker(aID, 5, 6, kEmbedChar);
+
+ this.invoke = function insertChildDiv_invoke() {
+ var childDiv = document.createElement("div");
+ // Note after bug 646216, a sole div without text won't be accessible
+ // and would not result in an embedded character.
+ // Therefore, add some text.
+ childDiv.textContent = "hello";
+ this.DOMNode.insertBefore(childDiv, this.DOMNode.childNodes[1]);
+ };
+
+ this.getID = function insertChildDiv_getID() {
+ return "Insert accessible div into the middle of text accessible " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove children from text container from first to last child or vice
+ * versa.
+ */
+ function removeChildren(aID, aLastToFirst, aStart, aEnd, aText) {
+ this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
+
+ this.invoke = function removeChildren_invoke() {
+ if (aLastToFirst) {
+ while (this.DOMNode.firstChild)
+ this.DOMNode.removeChild(this.DOMNode.lastChild);
+ } else {
+ while (this.DOMNode.firstChild)
+ this.DOMNode.firstChild.remove();
+ }
+ };
+
+ this.getID = function removeChildren_getID() {
+ return "remove children of " + prettyName(aID) +
+ (aLastToFirst ? " from last to first" : " from first to last");
+ };
+ }
+
+ /**
+ * Remove text from HTML input.
+ */
+ function removeTextFromInput(aID, aStart, aEnd, aText) {
+ this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
+
+ this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE,
+ this.DOMNode));
+
+ this.invoke = function removeTextFromInput_invoke() {
+ this.DOMNode.focus();
+ this.DOMNode.setSelectionRange(aStart, aEnd);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromInput_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Add text into HTML input.
+ */
+ function insertTextIntoInput(aID, aStart, aEnd, aText) {
+ this.__proto__ = new textInsertInvoker(aID, aStart, aEnd, aText);
+
+ this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE,
+ this.DOMNode));
+
+ this.invoke = function insertTextIntoInput_invoke() {
+ this.DOMNode.focus();
+ sendString("a");
+ };
+
+ this.getID = function insertTextIntoInput_getID() {
+ return "Insert text to " + aStart + " for " + prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove text data from text node of editable area.
+ */
+ function removeTextFromEditable(aID, aStart, aEnd, aText, aTextNode) {
+ this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
+
+ this.invoke = function removeTextFromEditable_invoke() {
+ this.DOMNode.focus();
+
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.setStart(this.textNode, aStart);
+ range.setEnd(this.textNode, aEnd);
+ selection.addRange(range);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromEditable_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+
+ this.textNode = getNode(aTextNode);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ gA11yEventDumpToConsole = true; // debugging
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // Text remove event on inaccessible child HTML span removal containing
+ // accessible text nodes.
+ gQueue.push(new removeChildSpan("p"));
+ gQueue.push(new insertChildSpan("p"), true);
+ gQueue.push(new insertChildSpan("p"), false);
+
+ // Remove embedded character.
+ gQueue.push(new removeChildDiv("div"));
+ gQueue.push(new insertChildDiv("div"));
+
+ // Remove all children.
+ var text = kEmbedChar + "txt" + kEmbedChar;
+ gQueue.push(new removeChildren("div2", true, 0, 5, text));
+ gQueue.push(new removeChildren("div3", false, 0, 5, text));
+
+ // Text remove from text node within hypertext accessible.
+ gQueue.push(new removeTextFromInput("input", 1, 3, "al"));
+ gQueue.push(new insertTextIntoInput("input", 1, 2, "a"));
+
+ // bug 570691
+ todo(false, "Fix text change events from editable area, see bug 570691");
+ // var textNode = getNode("editable").firstChild;
+ // gQueue.push(new removeTextFromEditable("editable", 1, 3, "al", textNode));
+ // textNode = getNode("editable2").firstChild.firstChild;
+ // gQueue.push(new removeTextFromEditable("editable2", 1, 3, "al", textNode));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=566293"
+ title=" wrong length of text remove event when inaccessible node containing accessible nodes is removed">
+ Mozilla Bug 566293
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570710"
+ title="Avoid extra array traversal during text event creation">
+ Mozilla Bug 570710
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=574003"
+ title="Coalesce text events on nodes removal">
+ Mozilla Bug 574003
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=575052"
+ title="Cache text offsets within hypertext accessible">
+ Mozilla Bug 575052
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"
+ title="Rework accessible tree update code">
+ Mozilla Bug 570275
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p"><span><span>333</span><span>22</span></span>1111</p>
+ <div id="div">hello<div>hello</div>hello</div>
+ <div id="div2"><div>txt</div>txt<div>txt</div></div>
+ <div id="div3"><div>txt</div>txt<div>txt</div></div>
+ <input id="input" value="value">
+ <div contentEditable="true" id="editable">value</div>
+ <div contentEditable="true" id="editable2"><span>value</span></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_text_alg.html b/accessible/tests/mochitest/events/test_text_alg.html
new file mode 100644
index 0000000000..f9b55c8b23
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_text_alg.html
@@ -0,0 +1,249 @@
+<html>
+
+<head>
+ <title>Accessible text update algorithm testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ const kRemoval = false;
+ const kInsertion = true;
+ const kUnexpected = true;
+
+ function changeText(aContainerID, aValue, aEventList) {
+ this.containerNode = getNode(aContainerID);
+ this.textNode = this.containerNode.firstChild;
+ this.textData = this.textNode.data;
+
+ this.eventSeq = [ ];
+ this.unexpectedEventSeq = [ ];
+
+ for (var i = 0; i < aEventList.length; i++) {
+ var event = aEventList[i];
+
+ var isInserted = event[0];
+ var str = event[1];
+ var offset = event[2];
+ var checker = new textChangeChecker(this.containerNode, offset,
+ offset + str.length, str,
+ isInserted);
+
+ if (event[3] == kUnexpected)
+ this.unexpectedEventSeq.push(checker);
+ else
+ this.eventSeq.push(checker);
+ }
+
+ this.invoke = function changeText_invoke() {
+ this.textNode.data = aValue;
+ };
+
+ this.getID = function changeText_getID() {
+ return "change text '" + shortenString(this.textData) + "' -> '" +
+ shortenString(this.textNode.data) + "' for " +
+ prettyName(this.containerNode);
+ };
+ }
+
+ function expStr(x, doublings) {
+ for (var i = 0; i < doublings; ++i)
+ x = x + x;
+ return x;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // ////////////////////////////////////////////////////////////////////////
+ // wqrema -> tqb: substitution coalesced with removal
+
+ var events = [
+ [ kRemoval, "w", 0 ], // wqrema -> qrema
+ [ kInsertion, "t", 0], // qrema -> tqrema
+ [ kRemoval, "rema", 2 ], // tqrema -> tq
+ [ kInsertion, "b", 2], // tq -> tqb
+ ];
+ gQueue.push(new changeText("p1", "tqb", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // b -> insa: substitution coalesced with insertion (complex substitution)
+
+ events = [
+ [ kRemoval, "b", 0 ], // b ->
+ [ kInsertion, "insa", 0], // -> insa
+ ];
+ gQueue.push(new changeText("p2", "insa", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abc -> def: coalesced substitutions
+
+ events = [
+ [ kRemoval, "abc", 0 ], // abc ->
+ [ kInsertion, "def", 0], // -> def
+ ];
+ gQueue.push(new changeText("p3", "def", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abcabc -> abcDEFabc: coalesced insertions
+
+ events = [
+ [ kInsertion, "DEF", 3], // abcabc -> abcDEFabc
+ ];
+ gQueue.push(new changeText("p4", "abcDEFabc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abc -> defabc: insertion into begin
+
+ events = [
+ [ kInsertion, "def", 0], // abc -> defabc
+ ];
+ gQueue.push(new changeText("p5", "defabc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abc -> abcdef: insertion into end
+
+ events = [
+ [ kInsertion, "def", 3], // abc -> abcdef
+ ];
+ gQueue.push(new changeText("p6", "abcdef", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // defabc -> abc: removal from begin
+
+ events = [
+ [ kRemoval, "def", 0], // defabc -> abc
+ ];
+ gQueue.push(new changeText("p7", "abc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abcdef -> abc: removal from the end
+
+ events = [
+ [ kRemoval, "def", 3], // abcdef -> abc
+ ];
+ gQueue.push(new changeText("p8", "abc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abcDEFabc -> abcabc: coalesced removals
+
+ events = [
+ [ kRemoval, "DEF", 3], // abcDEFabc -> abcabc
+ ];
+ gQueue.push(new changeText("p9", "abcabc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // !abcdef@ -> @axbcef!: insertion, deletion and substitutions
+
+ events = [
+ [ kRemoval, "!", 0 ], // !abcdef@ -> abcdef@
+ [ kInsertion, "@", 0], // abcdef@ -> @abcdef@
+ [ kInsertion, "x", 2 ], // @abcdef@ -> @axbcdef@
+ [ kRemoval, "d", 5], // @axbcdef@ -> @axbcef@
+ [ kRemoval, "@", 7 ], // @axbcef@ -> @axbcef
+ [ kInsertion, "!", 7 ], // @axbcef -> @axbcef!
+ ];
+ gQueue.push(new changeText("p10", "@axbcef!", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // meilenstein -> levenshtein: insertion, complex and simple substitutions
+
+ events = [
+ [ kRemoval, "m", 0 ], // meilenstein -> eilenstein
+ [ kInsertion, "l", 0], // eilenstein -> leilenstein
+ [ kRemoval, "il", 2 ], // leilenstein -> leenstein
+ [ kInsertion, "v", 2], // leenstein -> levenstein
+ [ kInsertion, "h", 6 ], // levenstein -> levenshtein
+ ];
+ gQueue.push(new changeText("p11", "levenshtein", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // long strings, remove/insert pair as the old string was replaced on
+ // new one
+
+ var longStr1 = expStr("x", 16);
+ var longStr2 = expStr("X", 16);
+
+ var newStr = "a" + longStr1 + "b", insStr = longStr1, rmStr = "";
+ events = [
+ [ kRemoval, rmStr, 1, kUnexpected ],
+ [ kInsertion, insStr, 1 ],
+ ];
+ gQueue.push(new changeText("p12", newStr, events));
+
+ newStr = "a" + longStr2 + "b";
+ insStr = longStr2;
+ rmStr = longStr1;
+ events = [
+ [ kRemoval, rmStr, 1 ],
+ [ kInsertion, insStr, 1],
+ ];
+ gQueue.push(new changeText("p12", newStr, events));
+
+ newStr = "ab";
+ insStr = "";
+ rmStr = longStr2;
+ events = [
+ [ kRemoval, rmStr, 1 ],
+ [ kInsertion, insStr, 1, kUnexpected ],
+ ];
+ gQueue.push(new changeText("p12", newStr, events));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"
+ title="Cache rendered text on a11y side">
+ Mozilla Bug 626660
+ </a>
+ <br>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- Note: only editable text gets diffed this way. -->
+ <div contenteditable="true">
+ <p id="p1">wqrema</p>
+ <p id="p2">b</p>
+ <p id="p3">abc</p>
+ <p id="p4">abcabc</p>
+ <p id="p5">abc</p>
+ <p id="p6">abc</p>
+ <p id="p7">defabc</p>
+ <p id="p8">abcdef</p>
+ <p id="p9">abcDEFabc</p>
+ <p id="p10">!abcdef@</p>
+ <p id="p11">meilenstein</p>
+ <p id="p12">ab</p>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_textattrchange.html b/accessible/tests/mochitest/events/test_textattrchange.html
new file mode 100644
index 0000000000..b46e1ef399
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_textattrchange.html
@@ -0,0 +1,107 @@
+<html>
+
+<head>
+ <title>Text attribute changed event for misspelled text</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ const {InlineSpellChecker} = ChromeUtils.importESModule(
+ "resource://gre/modules/InlineSpellChecker.sys.mjs"
+ );
+
+ function spelledTextInvoker(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_TEXT_ATTRIBUTE_CHANGED, this.DOMNode),
+ ];
+
+ this.invoke = function spelledTextInvoker_invoke() {
+ var editor = this.DOMNode.editor;
+ var spellChecker = new InlineSpellChecker(editor);
+ spellChecker.enabled = true;
+
+ // var spellchecker = editor.getInlineSpellChecker(true);
+ // spellchecker.enableRealTimeSpell = true;
+
+ this.DOMNode.value = "valid text inalid tixt";
+ };
+
+ this.finalCheck = function spelledTextInvoker_finalCheck() {
+ var defAttrs = buildDefaultTextAttrs(this.DOMNode, kInputFontSize,
+ kNormalFontWeight,
+ kInputFontFamily);
+ testDefaultTextAttrs(aID, defAttrs);
+
+ var attrs = { };
+ var misspelledAttrs = {
+ "invalid": "spelling",
+ };
+
+ testTextAttrs(aID, 0, attrs, defAttrs, 0, 11);
+ testTextAttrs(aID, 11, misspelledAttrs, defAttrs, 11, 17);
+ testTextAttrs(aID, 17, attrs, defAttrs, 17, 18);
+ testTextAttrs(aID, 18, misspelledAttrs, defAttrs, 18, 22);
+ };
+
+ this.getID = function spelledTextInvoker_getID() {
+ return "text attribute change for misspelled text";
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ // Synth focus before spellchecking turning on to make sure editor
+ // gets a time for initialization.
+
+ gQueue = new eventQueue();
+ gQueue.push(new synthFocus("input"));
+ gQueue.push(new spelledTextInvoker("input"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=345759"
+ title="Implement text attributes">
+ Mozilla Bug 345759
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input"/>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_textselchange.html b/accessible/tests/mochitest/events/test_textselchange.html
new file mode 100644
index 0000000000..3dce0760eb
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_textselchange.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+ <title>Accessible text selection change events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function getOnclickSeq(aID) {
+ return [
+ new caretMoveChecker(0, true, aID),
+ new unexpectedInvokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID),
+ ];
+ }
+
+ function doTests() {
+ // test caret move events and caret offsets
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthClick("c1_p1", getOnclickSeq("c1_p1")));
+ gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 1, "c1_p1", 0, "c1_p2", 0), { shiftKey: true }));
+ gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 2, "c1_p1", 0, "c1_p2", 9), { shiftKey: true }));
+
+ gQueue.push(new synthClick("ta1", getOnclickSeq("ta1")));
+ gQueue.push(new synthRightKey("ta1",
+ new textSelectionChecker("ta1", 0, 1, "ta1", 0, "ta1", 1),
+ { shiftKey: true }));
+ gQueue.push(new synthLeftKey("ta1",
+ [new textSelectionChecker("ta1", 0, 0, "ta1", 0, "ta1", 0),
+ new caretMoveChecker(0, true, "ta1")]));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=762934"
+ title="Text selection change event has a wrong target when selection is spanned through several objects">
+ Bug 762934
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=956032"
+ title="Text selection change event missed when selected text becomes unselected">
+ Bug 956032
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1" contentEditable="true">
+ <p id="c1_p1">paragraph</p>
+ <p id="c1_p2">paragraph</p>
+ </div>
+
+ <textarea id="ta1">Hello world</textarea>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_tree.xhtml b/accessible/tests/mochitest/events/test_tree.xhtml
new file mode 100644
index 0000000000..af7feafde8
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_tree.xhtml
@@ -0,0 +1,358 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="DOM TreeRowCountChanged and a11y name change events.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ var gView;
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invoker's checkers
+
+ /**
+ * Check TreeRowCountChanged event.
+ */
+ function rowCountChangedChecker(aMsg, aIdx, aCount)
+ {
+ this.type = "TreeRowCountChanged";
+ this.target = gTree;
+ this.check = function check(aEvent)
+ {
+ var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2);
+ var index = propBag.getPropertyAsInt32("index");
+ is(index, aIdx, "Wrong 'index' data of 'treeRowCountChanged' event.");
+
+ var count = propBag.getPropertyAsInt32("count");
+ is(count, aCount, "Wrong 'count' data of 'treeRowCountChanged' event.");
+ }
+ this.getID = function getID()
+ {
+ return aMsg + "TreeRowCountChanged";
+ }
+ }
+
+ /**
+ * Check TreeInvalidated event.
+ */
+ function treeInvalidatedChecker(aMsg, aStartRow, aEndRow, aStartCol, aEndCol)
+ {
+ this.type = "TreeInvalidated";
+ this.target = gTree;
+ this.check = function check(aEvent)
+ {
+ var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2);
+ try {
+ var startRow = propBag.getPropertyAsInt32("startrow");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ startRow = null;
+ }
+ is(startRow, aStartRow,
+ "Wrong 'startrow' of 'treeInvalidated' event on " + aMsg);
+
+ try {
+ var endRow = propBag.getPropertyAsInt32("endrow");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ endRow = null;
+ }
+ is(endRow, aEndRow,
+ "Wrong 'endrow' of 'treeInvalidated' event on " + aMsg);
+
+ try {
+ var startCol = propBag.getPropertyAsInt32("startcolumn");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ startCol = null;
+ }
+ is(startCol, aStartCol,
+ "Wrong 'startcolumn' of 'treeInvalidated' event on " + aMsg);
+
+ try {
+ var endCol = propBag.getPropertyAsInt32("endcolumn");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ endCol = null;
+ }
+ is(endCol, aEndCol,
+ "Wrong 'endcolumn' of 'treeInvalidated' event on " + aMsg);
+ }
+ this.getID = function getID()
+ {
+ return "TreeInvalidated on " + aMsg;
+ }
+ }
+
+ /**
+ * Check name changed a11y event.
+ */
+ function nameChangeChecker(aMsg, aRow, aCol)
+ {
+ this.type = EVENT_NAME_CHANGE;
+
+ function targetGetter()
+ {
+ var acc = getAccessible(gTree);
+
+ var tableAcc = getAccessible(acc, [nsIAccessibleTable]);
+ return tableAcc.getCellAt(aRow, aCol);
+ }
+ Object.defineProperty(this, "target", { get: targetGetter });
+
+ this.getID = function getID()
+ {
+ return aMsg + "name changed";
+ }
+ }
+
+ /**
+ * Check name changed a11y event for a row.
+ */
+ function rowNameChangeChecker(aMsg, aRow)
+ {
+ this.type = EVENT_NAME_CHANGE;
+
+ function targetGetter()
+ {
+ var acc = getAccessible(gTree);
+ return acc.getChildAt(aRow + 1);
+ }
+ Object.defineProperty(this, "target", { get: targetGetter });
+
+ this.getID = function getID()
+ {
+ return aMsg + "name changed";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Set tree view.
+ */
+ function setTreeView()
+ {
+ this.invoke = function setTreeView_invoke()
+ {
+ gTree.view = gView;
+ }
+
+ this.getID = function setTreeView_getID() { return "set tree view"; }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, gTree)
+ ];
+ };
+
+ /**
+ * Insert row at 0 index and checks TreeRowCountChanged and TreeInvalidated
+ * event.
+ */
+ function insertRow()
+ {
+ this.invoke = function insertRow_invoke()
+ {
+ gView.appendItem("last");
+ gTree.rowCountChanged(0, 1);
+ }
+
+ this.eventSeq =
+ [
+ new rowCountChangedChecker("insertRow: ", 0, 1),
+ new treeInvalidatedChecker("insertRow", 0, 5, null, null)
+ ];
+
+ this.getID = function insertRow_getID()
+ {
+ return "insert row";
+ }
+ }
+
+ /**
+ * Invalidates first column and checks six name changed events for each
+ * treeitem plus TreeInvalidated event.
+ */
+ function invalidateColumn()
+ {
+ this.invoke = function invalidateColumn_invoke()
+ {
+ // Make sure accessible subtree of XUL tree is created otherwise no
+ // name change events for cell accessibles are emitted.
+ var tree = getAccessible(gTree);
+ var child = tree.firstChild;
+ var walkDown = true;
+ while (child != tree) {
+ if (walkDown) {
+ var grandChild = child.firstChild;
+ if (grandChild) {
+ child = grandChild;
+ continue;
+ }
+ }
+
+ var sibling = child.nextSibling;
+ if (sibling) {
+ child = sibling;
+ walkDown = true;
+ continue;
+ }
+
+ child = child.parent;
+ walkDown = false;
+ }
+
+ // Fire 'TreeInvalidated' event by InvalidateColumn()
+ var firstCol = gTree.columns.getFirstColumn();
+ for (var i = 0; i < gView.rowCount; i++)
+ gView.setCellText(i, firstCol, "hey " + String(i) + "x0");
+
+ gTree.invalidateColumn(firstCol);
+ }
+
+ this.eventSeq =
+ [
+ new nameChangeChecker("invalidateColumn: ", 0, 0),
+ new nameChangeChecker("invalidateColumn: ", 1, 0),
+ new nameChangeChecker("invalidateColumn: ", 2, 0),
+ new nameChangeChecker("invalidateColumn: ", 3, 0),
+ new nameChangeChecker("invalidateColumn: ", 4, 0),
+ new nameChangeChecker("invalidateColumn: ", 5, 0),
+ new treeInvalidatedChecker("invalidateColumn", null, null, 0, 0)
+ ];
+
+ this.getID = function invalidateColumn_getID()
+ {
+ return "invalidate column";
+ }
+ }
+
+ /**
+ * Invalidates second row and checks name changed event for first treeitem
+ * (note, there are two name changed events on linux due to different
+ * accessible tree for xul:tree element) plus TreeInvalidated event.
+ */
+ function invalidateRow()
+ {
+ this.invoke = function invalidateRow_invoke()
+ {
+ // Fire 'TreeInvalidated' event by InvalidateRow()
+ // eslint-disable-next-line no-unused-vars
+ var colCount = gTree.columns.count;
+ var column = gTree.columns.getFirstColumn();
+ while (column) {
+ gView.setCellText(1, column, "aloha 1x" + String(column.index));
+ column = column.getNext();
+ }
+
+ gTree.invalidateRow(1);
+ }
+
+ this.eventSeq =
+ [
+ new nameChangeChecker("invalidateRow: ", 1, 0),
+ new nameChangeChecker("invalidateRow: ", 1, 1),
+ new rowNameChangeChecker("invalidateRow: ", 1),
+ new treeInvalidatedChecker("invalidateRow", 1, 1, null, null)
+ ];
+
+ this.getID = function invalidateRow_getID()
+ {
+ return "invalidate row";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ var gTree = null;
+ var gTreeView = null;
+ var gQueue = null;
+
+ // gA11yEventDumpID = "debug";
+ gA11yEventDumpToConsole = true; // debuggin
+
+ function doTest()
+ {
+ // Initialize the tree
+ gTree = document.getElementById("tree");
+ gView = new nsTableTreeView(5);
+
+ // Perform actions
+ gQueue = new eventQueue();
+
+ gQueue.push(new setTreeView());
+ gQueue.push(new insertRow());
+ gQueue.push(new invalidateColumn());
+ gQueue.push(new invalidateRow());
+
+ gQueue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=368835"
+ title="Fire TreeViewChanged/TreeRowCountChanged events.">
+ Mozilla Bug 368835
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=308564"
+ title="No accessibility events when data in a tree row changes.">
+ Mozilla Bug 308564
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=739524"
+ title="replace TreeViewChanged DOM event on direct call from XUL tree.">
+ Mozilla Bug 739524
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=743568"
+ title="Thunderbird message list tree emitting incorrect focus signals after message deleted.">
+ Mozilla Bug 743568
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/events/test_valuechange.html b/accessible/tests/mochitest/events/test_valuechange.html
new file mode 100644
index 0000000000..1ad3c0359d
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_valuechange.html
@@ -0,0 +1,315 @@
+<html>
+
+<head>
+ <title>Accessible value change events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript"
+ src="../value.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // Value change invoker
+ function changeARIAValue(aNodeOrID, aValuenow, aValuetext) {
+ this.DOMNode = getNode(aNodeOrID);
+ this.eventSeq = [ new invokerChecker(aValuetext ?
+ EVENT_TEXT_VALUE_CHANGE :
+ EVENT_VALUE_CHANGE, this.DOMNode),
+ ];
+
+ this.invoke = function changeARIAValue_invoke() {
+ // Note: this should not fire an EVENT_VALUE_CHANGE when aria-valuetext
+ // is not empty
+ if (aValuenow != undefined)
+ this.DOMNode.setAttribute("aria-valuenow", aValuenow);
+
+ // Note: this should always fire an EVENT_VALUE_CHANGE
+ if (aValuetext != undefined)
+ this.DOMNode.setAttribute("aria-valuetext", aValuetext);
+ };
+
+ this.check = function changeARIAValue_check() {
+ var acc = getAccessible(aNodeOrID, [nsIAccessibleValue]);
+ if (!acc)
+ return;
+
+ // Note: always test against valuetext first because the existence of
+ // aria-valuetext takes precedence over aria-valuenow in gecko.
+ is(acc.value, (aValuetext != undefined) ? aValuetext : aValuenow,
+ "Wrong value of " + prettyName(aNodeOrID));
+ };
+
+ this.getID = function changeARIAValue_getID() {
+ return prettyName(aNodeOrID) + " value changed";
+ };
+ }
+
+ function changeValue(aID, aValue) {
+ this.DOMNode = getNode(aID);
+ this.eventSeq = [new invokerChecker(EVENT_TEXT_VALUE_CHANGE,
+ this.DOMNode),
+ ];
+
+ this.invoke = function changeValue_invoke() {
+ this.DOMNode.value = aValue;
+ };
+
+ this.check = function changeValue_check() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, aValue, "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeValue_getID() {
+ return prettyName(aID) + " value changed";
+ };
+ }
+
+ function changeProgressValue(aID, aValue) {
+ this.DOMNode = getNode(aID);
+ this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)];
+
+ this.invoke = function changeProgressValue_invoke() {
+ this.DOMNode.value = aValue;
+ };
+
+ this.check = function changeProgressValue_check() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, aValue + "%", "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeProgressValue_getID() {
+ return prettyName(aID) + " value changed";
+ };
+ }
+
+ function changeRangeValueWithMouse(aID) {
+ this.DOMNode = getNode(aID);
+ this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)];
+
+ this.invoke = function changeRangeValue_invoke() {
+ synthesizeMouse(getNode(aID), 5, 5, { });
+ };
+
+ this.finalCheck = function changeRangeValue_finalCheck() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, "0", "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeRangeValue_getID() {
+ return prettyName(aID) + " range value changed";
+ };
+ }
+
+ function changeRangeValueWithA11yAPI(aID) {
+ this.DOMNode = getNode(aID);
+ let inputChecker = new invokerChecker("input", this.DOMNode);
+ inputChecker.eventTarget = "element";
+
+ let changeChecker = new invokerChecker("change", this.DOMNode);
+ changeChecker.eventTarget = "element";
+
+ this.eventSeq = [
+ inputChecker,
+ changeChecker,
+ new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode),
+ ];
+
+ this.invoke = function changeRangeValue_invoke() {
+ this.DOMNode.focus();
+ let acc = getAccessible(this.DOMNode, [nsIAccessibleValue]);
+ acc.currentValue = 0;
+ this.DOMNode.blur();
+ };
+
+ this.finalCheck = function changeRangeValue_finalCheck() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, "0", "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeRangeValue_getID() {
+ return prettyName(aID) + " range value changed";
+ };
+ }
+
+ function changeSelectValue(aID, aKey, aValue) {
+ this.eventSeq =
+ [ new invokerChecker(EVENT_TEXT_VALUE_CHANGE, getAccessible(aID)) ];
+
+ this.invoke = function changeSelectValue_invoke() {
+ getAccessible(aID).takeFocus();
+ synthesizeKey(aKey, {}, window);
+ };
+
+ this.finalCheck = function changeSelectValue_finalCheck() {
+ is(getAccessible(aID).value, aValue, "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeSelectValue_getID() {
+ return `${prettyName(aID)} closed select value change on '${aKey}'' key press`;
+ };
+ }
+
+ // enableLogging("DOMEvents");
+ // gA11yEventDumpToConsole = true;
+ function doTests() {
+
+ // Test initial values
+ testValue("slider_vn", "5", 5, 0, 1000, 0);
+ testValue("slider_vnvt", "plain", 0, 0, 5, 0);
+ testValue("slider_vt", "hi", 1.5, 0, 3, 0);
+ testValue("scrollbar", "5", 5, 0, 1000, 0);
+ testValue("splitter", "5", 5, 0, 1000, 0);
+ testValue("progress", "22%", 22, 0, 100, 0);
+ testValue("range", "6", 6, 0, 10, 1);
+ testValue("range2", "6", 6, 0, 10, 1);
+
+ // Test that elements which should not expose values do not
+ let separatorVal = getAccessible("separator", [nsIAccessibleValue], null, DONOTFAIL_IF_NO_INTERFACE);
+ ok(!separatorVal, "value interface is not exposed for separator");
+ let separatorAcc = getAccessible("separator");
+ ok(!separatorAcc.value, "Value text is not exposed for separator");
+
+ // Test value change events
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeARIAValue("slider_vn", "6", undefined));
+ gQueue.push(new changeARIAValue("slider_vt", undefined, "hey!"));
+ gQueue.push(new changeARIAValue("slider_vnvt", "3", "sweet"));
+ gQueue.push(new changeARIAValue("scrollbar", "6", undefined));
+ gQueue.push(new changeARIAValue("splitter", "6", undefined));
+
+ gQueue.push(new changeValue("combobox", "hello"));
+
+ gQueue.push(new changeProgressValue("progress", "50"));
+ gQueue.push(new changeRangeValueWithMouse("range"));
+ gQueue.push(new changeRangeValueWithA11yAPI("range2"));
+
+ gQueue.push(new changeSelectValue("select", "VK_DOWN", "2nd"));
+ gQueue.push(new changeSelectValue("select", "3", "3rd"));
+
+ let iframeSelect = getAccessible("selectIframe").firstChild.firstChild;
+ gQueue.push(new changeSelectValue(iframeSelect, "VK_DOWN", "2"));
+
+ let shadowSelect = getAccessible("selectShadow").firstChild;
+ gQueue.push(new changeSelectValue(shadowSelect, "VK_DOWN", "2"));
+ gQueue.push(new changeValue("number", "2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=478032"
+ title=" Fire delayed value changed event for aria-valuetext changes">
+ Mozilla Bug 478032
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289"
+ title="We dont expose new aria role 'scrollbar' and property aria-orientation">
+ Mozilla Bug 529289
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764"
+ title="Make HTML5 input@type=range element accessible">
+ Mozilla Bug 559764
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=703202"
+ title="ARIA comboboxes don't fire value change events">
+ Mozilla Bug 703202
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=761901"
+ title=" HTML5 progress accessible should fire value change event">
+ Mozilla Bug 761901
+ </a>
+
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- ARIA sliders -->
+ <div id="slider_vn" role="slider" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">slider</div>
+
+ <div id="slider_vt" role="slider" aria-valuetext="hi"
+ aria-valuemin="0" aria-valuemax="3">greeting slider</div>
+
+ <div id="slider_vnvt" role="slider" aria-valuenow="0" aria-valuetext="plain"
+ aria-valuemin="0" aria-valuemax="5">sweetness slider</div>
+
+ <!-- ARIA scrollbar -->
+ <div id="scrollbar" role="scrollbar" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">slider</div>
+
+ <!-- ARIA separator which is focusable (i.e. a splitter) -->
+ <div id="splitter" role="separator" tabindex="0" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">splitter</div>
+
+ <!-- ARIA separator which is not focusable and should not expose values -->
+ <div id="separator" role="separator" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">splitter</div>
+
+ <!-- ARIA combobox -->
+ <input id="combobox" role="combobox" aria-autocomplete="inline">
+
+ <!-- progress bar -->
+ <progress id="progress" value="22" max="100"></progress>
+
+ <!-- input@type="range" -->
+ <input type="range" id="range" min="0" max="10" value="6">
+
+ <!-- input@type="range" -->
+ <input type="range" id="range2" min="0" max="10" value="6">
+
+ <select id="select">
+ <option>1st</option>
+ <option>2nd</option>
+ <option>3rd</option>
+ </select>
+
+ <iframe id="selectIframe"
+ src="data:text/html,<select id='iframeSelect'><option>1</option><option>2</option></select>">
+ </iframe>
+
+ <div id="selectShadow"></div>
+ <script>
+ let host = document.getElementById("selectShadow");
+ let shadow = host.attachShadow({mode: "open"});
+ let select = document.createElement("select");
+ select.id = "shadowSelect";
+ let option = document.createElement("option");
+ option.textContent = "1";
+ select.appendChild(option);
+ option = document.createElement("option");
+ option.textContent = "2";
+ select.appendChild(option);
+ shadow.appendChild(select);
+ </script>
+
+ <input type="number" id="number" value="1">
+</body>
+</html>