//////////////////////////////////////////////////////////////////////////////// // Interfaces const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval; const nsIAccessibleEvent = Components.interfaces.nsIAccessibleEvent; const nsIAccessibleStateChangeEvent = Components.interfaces.nsIAccessibleStateChangeEvent; const nsIAccessibleCaretMoveEvent = Components.interfaces.nsIAccessibleCaretMoveEvent; const nsIAccessibleStates = Components.interfaces.nsIAccessibleStates; const nsIAccessibleRole = Components.interfaces.nsIAccessibleRole; const nsIAccessibleTypes = Components.interfaces.nsIAccessibleTypes; const nsIAccessibleRelation = Components.interfaces.nsIAccessibleRelation; const nsIAccessNode = Components.interfaces.nsIAccessNode; const nsIAccessible = Components.interfaces.nsIAccessible; const nsIAccessibleDocument = Components.interfaces.nsIAccessibleDocument; const nsIAccessibleText = Components.interfaces.nsIAccessibleText; const nsIAccessibleEditableText = Components.interfaces.nsIAccessibleEditableText; const nsIAccessibleHyperLink = Components.interfaces.nsIAccessibleHyperLink; const nsIAccessibleHyperText = Components.interfaces.nsIAccessibleHyperText; const nsIAccessibleImage = Components.interfaces.nsIAccessibleImage; const nsIAccessibleSelectable = Components.interfaces.nsIAccessibleSelectable; const nsIAccessibleTable = Components.interfaces.nsIAccessibleTable; const nsIAccessibleValue = Components.interfaces.nsIAccessibleValue; const nsIObserverService = Components.interfaces.nsIObserverService; const nsIDOMNode = Components.interfaces.nsIDOMNode; const nsIPropertyElement = Components.interfaces.nsIPropertyElement; //////////////////////////////////////////////////////////////////////////////// // Roles const ROLE_PUSHBUTTON = nsIAccessibleRole.ROLE_PUSHBUTTON; const ROLE_CELL = nsIAccessibleRole.ROLE_CELL; const ROLE_COMBOBOX = nsIAccessibleRole.ROLE_COMBOBOX; const ROLE_COMBOBOX_LIST = nsIAccessibleRole.ROLE_COMBOBOX_LIST; const ROLE_COMBOBOX_OPTION = nsIAccessibleRole.ROLE_COMBOBOX_OPTION; const ROLE_DOCUMENT = nsIAccessibleRole.ROLE_DOCUMENT; const ROLE_ENTRY = nsIAccessibleRole.ROLE_ENTRY; const ROLE_FLAT_EQUATION = nsIAccessibleRole.ROLE_FLAT_EQUATION; const ROLE_FORM = nsIAccessibleRole.ROLE_FORM; const ROLE_GRID_CELL = nsIAccessibleRole.ROLE_GRID_CELL; const ROLE_HEADING = nsIAccessibleRole.ROLE_HEADING; const ROLE_LABEL = nsIAccessibleRole.ROLE_LABEL; const ROLE_LIST = nsIAccessibleRole.ROLE_LIST; const ROLE_OPTION = nsIAccessibleRole.ROLE_OPTION; const ROLE_PARAGRAPH = nsIAccessibleRole.ROLE_PARAGRAPH; const ROLE_PASSWORD_TEXT = nsIAccessibleRole.ROLE_PASSWORD_TEXT; const ROLE_SECTION = nsIAccessibleRole.ROLE_SECTION; const ROLE_TEXT_CONTAINER = nsIAccessibleRole.ROLE_TEXT_CONTAINER; const ROLE_TEXT_LEAF = nsIAccessibleRole.ROLE_TEXT_LEAF; const ROLE_TOGGLE_BUTTON = nsIAccessibleRole.ROLE_TOGGLE_BUTTON; //////////////////////////////////////////////////////////////////////////////// // States const STATE_CHECKED = nsIAccessibleStates.STATE_CHECKED; const STATE_CHECKABLE = nsIAccessibleStates.STATE_CHECKABLE; const STATE_COLLAPSED = nsIAccessibleStates.STATE_COLLAPSED; const STATE_EXPANDED = nsIAccessibleStates.STATE_EXPANDED; const STATE_EXTSELECTABLE = nsIAccessibleStates.STATE_EXTSELECTABLE; const STATE_FOCUSABLE = nsIAccessibleStates.STATE_FOCUSABLE; const STATE_FOCUSED = nsIAccessibleStates.STATE_FOCUSED; const STATE_HASPOPUP = nsIAccessibleStates.STATE_HASPOPUP; const STATE_MULTISELECTABLE = nsIAccessibleStates.STATE_MULTISELECTABLE; const STATE_PRESSED = nsIAccessibleStates.STATE_PRESSED; const STATE_READONLY = nsIAccessibleStates.STATE_READONLY; const STATE_SELECTABLE = nsIAccessibleStates.STATE_SELECTABLE; const STATE_SELECTED = nsIAccessibleStates.STATE_SELECTED; const EXT_STATE_EDITABLE = nsIAccessibleStates.EXT_STATE_EDITABLE; const EXT_STATE_EXPANDABLE = nsIAccessibleStates.EXT_STATE_EXPANDABLE; const EXT_STATE_INVALID = nsIAccessibleStates.STATE_INVALID; const EXT_STATE_MULTI_LINE = nsIAccessibleStates.EXT_STATE_MULTI_LINE; const EXT_STATE_REQUIRED = nsIAccessibleStates.STATE_REQUIRED; const EXT_STATE_SUPPORTS_AUTOCOMPLETION = nsIAccessibleStates.EXT_STATE_SUPPORTS_AUTOCOMPLETION; //////////////////////////////////////////////////////////////////////////////// // Accessible general /** * nsIAccessibleRetrieval, initialized when test is loaded. */ var gAccRetrieval = null; /** * Invokes the given function when document is loaded. Preferable to mochitests * 'addLoadEvent' function -- additionally ensures state of the document * accessible is not busy. * * @param aFunc the function to invoke */ function addA11yLoadEvent(aFunc) { function waitForDocLoad() { window.setTimeout( function() { var accDoc = getAccessible(document); var state = {}; accDoc.getState(state, {}); if (state.value & nsIAccessibleStates.STATE_BUSY) return waitForDocLoad(); aFunc.call(); }, 200 ); } addLoadEvent(waitForDocLoad); } //////////////////////////////////////////////////////////////////////////////// // Get DOM node/accesible helpers /** * Return the DOM node. */ function getNode(aNodeOrID) { if (!aNodeOrID) return null; var node = aNodeOrID; if (!(aNodeOrID instanceof nsIDOMNode)) { node = document.getElementById(aNodeOrID); if (!node) { ok(false, "Can't get DOM element for " + aNodeOrID); return null; } } return node; } /** * Return accessible for the given ID attribute or DOM element or accessible. * * @param aAccOrElmOrID [in] DOM element or ID attribute to get an accessible * for or an accessible to query additional interfaces. * @param aInterfaces [in, optional] the accessible interface or the array of * accessible interfaces to query it/them from obtained * accessible * @param aElmObj [out, optional] object to store DOM element which * accessible is obtained for */ function getAccessible(aAccOrElmOrID, aInterfaces, aElmObj) { var elm = null; if (aAccOrElmOrID instanceof nsIAccessible) { aAccOrElmOrID.QueryInterface(nsIAccessNode); elm = aAccOrElmOrID.DOMNode; } else if (aAccOrElmOrID instanceof nsIDOMNode) { elm = aAccOrElmOrID; } else { var elm = document.getElementById(aAccOrElmOrID); if (!elm) { ok(false, "Can't get DOM element for " + aAccOrElmOrID); return null; } } if (aElmObj && (typeof aElmObj == "object")) aElmObj.value = elm; var acc = (aAccOrElmOrID instanceof nsIAccessible) ? aAccOrElmOrID : null; if (!acc) { try { acc = gAccRetrieval.getAccessibleFor(elm); } catch (e) { } if (!acc) { ok(false, "Can't get accessible for " + aAccOrElmOrID); return null; } } if (!aInterfaces) return acc; if (aInterfaces instanceof Array) { for (var index = 0; index < aInterfaces.length; index++) { try { acc.QueryInterface(aInterfaces[index]); } catch (e) { ok(false, "Can't query " + aInterfaces[index] + " for " + aID); return null; } } return acc; } try { acc.QueryInterface(aInterfaces); } catch (e) { ok(false, "Can't query " + aInterfaces + " for " + aID); return null; } return acc; } //////////////////////////////////////////////////////////////////////////////// // Accessible Events /** * 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); gA11yEventApplicantsCount++; 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); gA11yEventApplicantsCount--; listenA11yEvents(false); } /** * 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 wether * event was handled correctly. * * Invoker interface is: * * var invoker = { * // Generates accessible event or event sequence. * invoke: function(){}, * * // Invoker's check of handled event for correctness [optional]. * check: function(aEvent){}, * * // Is called when event of registered type is handled. * debugCheck: function(aEvent){}, * * // DOM node event is generated for (the case when invoker generates * // single event, see 'eventSeq' property). * DOMNode getter() {}, * * // Array of items defining handled events. Every item is array with two * // elements, first element is event type, second element is event target * // (DOM node). * eventSeq getter() {}, * * // The ID of invoker. * getID: function(){} // returns invoker ID * }; * * @param aEventType [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); gA11yEventApplicantsCount++; window.setTimeout(function(aQueue) { aQueue.processInvoker(); }, 200, this); } // private this.processInvoker = function eventQueue_processInvoker() { this.clearEventHandler(); if (this.mIndex == this.mInvokers.length - 1) { gA11yEventApplicantsCount--; listenA11yEvents(false); for (var idx = 0; idx < this.mInvokers.length; idx++) { var invoker = this.mInvokers[idx]; var id = invoker.getID(); if (invoker.wasCaught) { for (var jdx = 0; jdx < invoker.wasCaught.length; jdx++) { var seq = this.getEventSequence(invoker); var type = seq[jdx][0]; var typeStr = gAccRetrieval.getStringEventType(type); var msg = "test with ID = '" + id + "' failed. "; if (invoker.doNotExpectEvents) { ok(!invoker.wasCaught[jdx], msg + "There is unexpected " + typeStr + " event."); } else { ok(invoker.wasCaught[jdx], msg + "No " + typeStr + " event."); } } } else { ok(false, "test with ID = '" + id + "' failed. No events were registered."); } } SimpleTest.finish(); return; } this.mIndex++; this.setEventHandler(); this.mInvokers[this.mIndex].invoke(); window.setTimeout(function(aQueue) { aQueue.processInvoker(); }, 200, this); } this.getInvoker = function eventQueue_getInvoker() { return this.mInvokers[this.mIndex]; } this.getEventSequence = function eventQueue_getEventSeq(aInvoker) { if (!aInvoker) // no invoker, return cached event sequence return this.mEventSeq; return ("eventSeq" in aInvoker) ? aInvoker.eventSeq : [[this.mDefEventType, aInvoker.DOMNode]]; } this.setEventHandler = function eventQueue_setEventHandler() { var invoker = this.getInvoker(); this.mEventSeq = this.getEventSequence(invoker); if (this.mEventSeq) { invoker.wasCaught = new Array(this.mEventSeq.length); for (var idx = 0; idx < this.mEventSeq.length; idx++) addA11yEventListener(this.mEventSeq[idx][0], this.mEventHandler); } } this.clearEventHandler = function eventQueue_clearEventHandler() { if (this.mEventSeq) { for (var idx = 0; idx < this.mEventSeq.length; idx++) removeA11yEventListener(this.mEventSeq[idx][0], this.mEventHandler); this.mEventSeq = null; } } this.mDefEventType = aEventType; this.mEventHandler = new eventHandlerForEventQueue(this); this.mInvokers = new Array(); this.mIndex = -1; this.mEventSeq = null; } //////////////////////////////////////////////////////////////////////////////// // Private //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Accessible general function initialize() { gAccRetrieval = Components.classes["@mozilla.org/accessibleRetrieval;1"]. getService(nsIAccessibleRetrieval); } addLoadEvent(initialize); //////////////////////////////////////////////////////////////////////////////// // Accessible Events var gObserverService = null; var gA11yEventListeners = {}; var gA11yEventApplicantsCount = 0; var gA11yEventDumpID = ""; // set up this variable to dump events into DOM. var gA11yEventObserver = { observe: function observe(aSubject, aTopic, aData) { if (aTopic != "accessible-event") return; var event = aSubject.QueryInterface(nsIAccessibleEvent); var listenersArray = gA11yEventListeners[event.eventType]; if (gA11yEventDumpID) { // debug stuff var type = gAccRetrieval.getStringEventType(event.eventType); var target = event.DOMNode; var info = "event type: " + type + ", target: " + target.localName; if (listenersArray) info += ", registered listeners count is " + listenersArray.length; var div = document.createElement("div"); div.textContent = info; var dumpElm = document.getElementById(gA11yEventDumpID); dumpElm.appendChild(div); } if (!listenersArray) return; for (var index = 0; index < listenersArray.length; index++) listenersArray[index].handleEvent(event); } }; function listenA11yEvents(aStartToListen) { if (aStartToListen && !gObserverService) { gObserverService = Components.classes["@mozilla.org/observer-service;1"]. getService(nsIObserverService); gObserverService.addObserver(gA11yEventObserver, "accessible-event", false); } else if (!gA11yEventApplicantsCount) { gObserverService.removeObserver(gA11yEventObserver, "accessible-event"); gObserverService = null; } } function addA11yEventListener(aEventType, aEventHandler) { if (!(aEventType in gA11yEventListeners)) gA11yEventListeners[aEventType] = new Array(); gA11yEventListeners[aEventType].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; } function eventHandlerForEventQueue(aQueue) { this.handleEvent = function eventHandlerForEventQueue_handleEvent(aEvent) { var invoker = this.mQueue.getInvoker(); var eventSeq = this.mQueue.getEventSequence(); if (!invoker && !eventSeq) // skip events before test was started return; if (eventSeq != this.mEventSeq) { this.mEventSeqIdx = -1; this.mEventSeq = eventSeq; } if ("debugCheck" in invoker) invoker.debugCheck(aEvent); if (invoker.doNotExpectEvents) { // Search through event sequence to ensure it doesn't containt handled // event. for (var idx = 0; idx < this.mEventSeq.length; idx++) { if (aEvent.eventType == eventSeq[idx][0] && aEvent.DOMNode == eventSeq[idx][1]) { invoker.wasCaught[idx] = true; } } } else { // We wait for events in order specified by evenSeq variable. var idx = this.mEventSeqIdx + 1; if (aEvent.eventType == eventSeq[idx][0] && aEvent.DOMNode == eventSeq[idx][1]) { if ("check" in invoker) invoker.check(aEvent); invoker.wasCaught[idx] = true; this.mEventSeqIdx++; } } } this.mQueue = aQueue; this.mEventSeq = null; this.mEventSeqIdx = -1; }