michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Constants michael@0: michael@0: const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; michael@0: const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; michael@0: const EVENT_DOCUMENT_LOAD_COMPLETE = nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; michael@0: const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; michael@0: const EVENT_DOCUMENT_LOAD_STOPPED = nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED; michael@0: const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; michael@0: const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; michael@0: const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; michael@0: const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; michael@0: const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; michael@0: const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; michael@0: const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; michael@0: const EVENT_OBJECT_ATTRIBUTE_CHANGED = nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; michael@0: const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; michael@0: const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; michael@0: const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; michael@0: const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; michael@0: const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; michael@0: const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; michael@0: const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; michael@0: const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; michael@0: const EVENT_TEXT_ATTRIBUTE_CHANGED = nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; michael@0: const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; michael@0: const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; michael@0: const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; michael@0: const EVENT_TEXT_SELECTION_CHANGED = nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; michael@0: const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; michael@0: const EVENT_VIRTUALCURSOR_CHANGED = nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; michael@0: michael@0: const kNotFromUserInput = 0; michael@0: const kFromUserInput = 1; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // General michael@0: michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: /** michael@0: * Set up this variable to dump events into DOM. michael@0: */ michael@0: var gA11yEventDumpID = ""; michael@0: michael@0: /** michael@0: * Set up this variable to dump event processing into console. michael@0: */ michael@0: var gA11yEventDumpToConsole = false; michael@0: michael@0: /** michael@0: * Set up this variable to dump event processing into error console. michael@0: */ michael@0: var gA11yEventDumpToAppConsole = false; michael@0: michael@0: /** michael@0: * Semicolon separated set of logging features. michael@0: */ michael@0: var gA11yEventDumpFeature = ""; michael@0: michael@0: /** michael@0: * Executes the function when requested event is handled. michael@0: * michael@0: * @param aEventType [in] event type michael@0: * @param aTarget [in] event target michael@0: * @param aFunc [in] function to call when event is handled michael@0: * @param aContext [in, optional] object in which context the function is michael@0: * called michael@0: * @param aArg1 [in, optional] argument passed into the function michael@0: * @param aArg2 [in, optional] argument passed into the function michael@0: */ michael@0: function waitForEvent(aEventType, aTargetOrFunc, aFunc, aContext, aArg1, aArg2) michael@0: { michael@0: var handler = { michael@0: handleEvent: function handleEvent(aEvent) { michael@0: michael@0: var target = aTargetOrFunc; michael@0: if (typeof aTargetOrFunc == "function") michael@0: target = aTargetOrFunc.call(); michael@0: michael@0: if (target) { michael@0: if (target instanceof nsIAccessible && michael@0: target != aEvent.accessible) michael@0: return; michael@0: michael@0: if (target instanceof nsIDOMNode && michael@0: target != aEvent.DOMNode) michael@0: return; michael@0: } michael@0: michael@0: unregisterA11yEventListener(aEventType, this); michael@0: michael@0: window.setTimeout( michael@0: function () michael@0: { michael@0: aFunc.call(aContext, aArg1, aArg2); michael@0: }, michael@0: 0 michael@0: ); michael@0: } michael@0: }; michael@0: michael@0: registerA11yEventListener(aEventType, handler); michael@0: } michael@0: michael@0: /** michael@0: * Generate mouse move over image map what creates image map accessible (async). michael@0: * See waitForImageMap() function. michael@0: */ michael@0: function waveOverImageMap(aImageMapID) michael@0: { michael@0: var imageMapNode = getNode(aImageMapID); michael@0: synthesizeMouse(imageMapNode, 10, 10, { type: "mousemove" }, michael@0: imageMapNode.ownerDocument.defaultView); michael@0: } michael@0: michael@0: /** michael@0: * Call the given function when the tree of the given image map is built. michael@0: */ michael@0: function waitForImageMap(aImageMapID, aTestFunc) michael@0: { michael@0: waveOverImageMap(aImageMapID); michael@0: michael@0: var imageMapAcc = getAccessible(aImageMapID); michael@0: if (imageMapAcc.firstChild) michael@0: return aTestFunc(); michael@0: michael@0: waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc); michael@0: } michael@0: michael@0: /** michael@0: * Register accessibility event listener. michael@0: * michael@0: * @param aEventType the accessible event type (see nsIAccessibleEvent for michael@0: * available constants). michael@0: * @param aEventHandler event listener object, when accessible event of the michael@0: * given type is handled then 'handleEvent' method of michael@0: * this object is invoked with nsIAccessibleEvent object michael@0: * as the first argument. michael@0: */ michael@0: function registerA11yEventListener(aEventType, aEventHandler) michael@0: { michael@0: listenA11yEvents(true); michael@0: addA11yEventListener(aEventType, aEventHandler); michael@0: } michael@0: michael@0: /** michael@0: * Unregister accessibility event listener. Must be called for every registered michael@0: * event listener (see registerA11yEventListener() function) when the listener michael@0: * is not needed. michael@0: */ michael@0: function unregisterA11yEventListener(aEventType, aEventHandler) michael@0: { michael@0: removeA11yEventListener(aEventType, aEventHandler); michael@0: listenA11yEvents(false); michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Event queue michael@0: michael@0: /** michael@0: * Return value of invoke method of invoker object. Indicates invoker was unable michael@0: * to prepare action. michael@0: */ michael@0: const INVOKER_ACTION_FAILED = 1; michael@0: michael@0: /** michael@0: * Return value of eventQueue.onFinish. Indicates eventQueue should not finish michael@0: * tests. michael@0: */ michael@0: const DO_NOT_FINISH_TEST = 1; michael@0: michael@0: /** michael@0: * Creates event queue for the given event type. The queue consists of invoker michael@0: * objects, each of them generates the event of the event type. When queue is michael@0: * started then every invoker object is asked to generate event after timeout. michael@0: * When event is caught then current invoker object is asked to check whether michael@0: * event was handled correctly. michael@0: * michael@0: * Invoker interface is: michael@0: * michael@0: * var invoker = { michael@0: * // Generates accessible event or event sequence. If returns michael@0: * // INVOKER_ACTION_FAILED constant then stop tests. michael@0: * invoke: function(){}, michael@0: * michael@0: * // [optional] Invoker's check of handled event for correctness. michael@0: * check: function(aEvent){}, michael@0: * michael@0: * // [optional] Invoker's check before the next invoker is proceeded. michael@0: * finalCheck: function(aEvent){}, michael@0: * michael@0: * // [optional] Is called when event of any registered type is handled. michael@0: * debugCheck: function(aEvent){}, michael@0: * michael@0: * // [ignored if 'eventSeq' is defined] DOM node event is generated for michael@0: * // (used in the case when invoker expects single event). michael@0: * DOMNode getter: function() {}, michael@0: * michael@0: * // [optional] if true then event sequences are ignored (no failure if michael@0: * // sequences are empty). Use you need to invoke an action, do some check michael@0: * // after timeout and proceed a next invoker. michael@0: * noEventsOnAction getter: function() {}, michael@0: * michael@0: * // Array of checker objects defining expected events on invoker's action. michael@0: * // michael@0: * // Checker object interface: michael@0: * // michael@0: * // var checker = { michael@0: * // * DOM or a11y event type. * michael@0: * // type getter: function() {}, michael@0: * // michael@0: * // * DOM node or accessible. * michael@0: * // target getter: function() {}, michael@0: * // michael@0: * // * DOM event phase (false - bubbling). * michael@0: * // phase getter: function() {}, michael@0: * // michael@0: * // * Callback, called to match handled event. * michael@0: * // match : function(aEvent) {}, michael@0: * // michael@0: * // * Callback, called when event is handled michael@0: * // check: function(aEvent) {}, michael@0: * // michael@0: * // * Checker ID * michael@0: * // getID: function() {}, michael@0: * // michael@0: * // * Event that don't have predefined order relative other events. * michael@0: * // async getter: function() {}, michael@0: * // michael@0: * // * Event that is not expected. * michael@0: * // unexpected getter: function() {}, michael@0: * // michael@0: * // * No other event of the same type is not allowed. * michael@0: * // unique getter: function() {} michael@0: * // }; michael@0: * eventSeq getter() {}, michael@0: * michael@0: * // Array of checker objects defining unexpected events on invoker's michael@0: * // action. michael@0: * unexpectedEventSeq getter() {}, michael@0: * michael@0: * // The ID of invoker. michael@0: * getID: function(){} // returns invoker ID michael@0: * }; michael@0: * michael@0: * // Used to add a possible scenario of expected/unexpected events on michael@0: * // invoker's action. michael@0: * defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq) michael@0: * michael@0: * michael@0: * @param aEventType [in, optional] the default event type (isn't used if michael@0: * invoker defines eventSeq property). michael@0: */ michael@0: function eventQueue(aEventType) michael@0: { michael@0: // public michael@0: michael@0: /** michael@0: * Add invoker object into queue. michael@0: */ michael@0: this.push = function eventQueue_push(aEventInvoker) michael@0: { michael@0: this.mInvokers.push(aEventInvoker); michael@0: } michael@0: michael@0: /** michael@0: * Start the queue processing. michael@0: */ michael@0: this.invoke = function eventQueue_invoke() michael@0: { michael@0: listenA11yEvents(true); michael@0: michael@0: // XXX: Intermittent test_events_caretmove.html fails withouth timeout, michael@0: // see bug 474952. michael@0: this.processNextInvokerInTimeout(true); michael@0: } michael@0: michael@0: /** michael@0: * This function is called when all events in the queue were handled. michael@0: * Override it if you need to be notified of this. michael@0: */ michael@0: this.onFinish = function eventQueue_finish() michael@0: { michael@0: } michael@0: michael@0: // private michael@0: michael@0: /** michael@0: * Process next invoker. michael@0: */ michael@0: this.processNextInvoker = function eventQueue_processNextInvoker() michael@0: { michael@0: // Some scenario was matched, we wait on next invoker processing. michael@0: if (this.mNextInvokerStatus == kInvokerCanceled) { michael@0: this.setInvokerStatus(kInvokerNotScheduled, michael@0: "scenario was matched, wait for next invoker activation"); michael@0: return; michael@0: } michael@0: michael@0: this.setInvokerStatus(kInvokerNotScheduled, "the next invoker is processed now"); michael@0: michael@0: // Finish processing of the current invoker if any. michael@0: var testFailed = false; michael@0: michael@0: var invoker = this.getInvoker(); michael@0: if (invoker) { michael@0: if ("finalCheck" in invoker) michael@0: invoker.finalCheck(); michael@0: michael@0: if (this.mScenarios && this.mScenarios.length) { michael@0: var matchIdx = -1; michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: if (!this.areExpectedEventsLeft(eventSeq)) { michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: var checker = eventSeq[idx]; michael@0: if (checker.unexpected && checker.wasCaught || michael@0: !checker.unexpected && checker.wasCaught != 1) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Ok, we have matched scenario. Report it was completed ok. In michael@0: // case of empty scenario guess it was matched but if later we michael@0: // find out that non empty scenario was matched then it will be michael@0: // a final match. michael@0: if (idx == eventSeq.length) { michael@0: if (matchIdx != -1 && eventSeq.length > 0 && michael@0: this.mScenarios[matchIdx].length > 0) { michael@0: ok(false, michael@0: "We have a matched scenario at index " + matchIdx + " already."); michael@0: } michael@0: michael@0: if (matchIdx == -1 || eventSeq.length > 0) michael@0: matchIdx = scnIdx; michael@0: michael@0: // Report everythign is ok. michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: var checker = eventSeq[idx]; michael@0: michael@0: var typeStr = eventQueue.getEventTypeAsString(checker); michael@0: var msg = "Test with ID = '" + this.getEventID(checker) + michael@0: "' succeed. "; michael@0: michael@0: if (checker.unexpected) michael@0: ok(true, msg + "There's no unexpected " + typeStr + " event."); michael@0: else michael@0: ok(true, msg + "Event " + typeStr + " was handled."); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // We don't have completely matched scenario. Report each failure/success michael@0: // for every scenario. michael@0: if (matchIdx == -1) { michael@0: testFailed = true; michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: var checker = eventSeq[idx]; michael@0: michael@0: var typeStr = eventQueue.getEventTypeAsString(checker); michael@0: var msg = "Scenario #" + scnIdx + " of test with ID = '" + michael@0: this.getEventID(checker) + "' failed. "; michael@0: michael@0: if (checker.wasCaught > 1) michael@0: ok(false, msg + "Dupe " + typeStr + " event."); michael@0: michael@0: if (checker.unexpected) { michael@0: if (checker.wasCaught) michael@0: ok(false, msg + "There's unexpected " + typeStr + " event."); michael@0: } else if (!checker.wasCaught) { michael@0: ok(false, msg + typeStr + " event was missed."); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.clearEventHandler(); michael@0: michael@0: // Check if need to stop the test. michael@0: if (testFailed || this.mIndex == this.mInvokers.length - 1) { michael@0: listenA11yEvents(false); michael@0: michael@0: var res = this.onFinish(); michael@0: if (res != DO_NOT_FINISH_TEST) michael@0: SimpleTest.finish(); michael@0: michael@0: return; michael@0: } michael@0: michael@0: // Start processing of next invoker. michael@0: invoker = this.getNextInvoker(); michael@0: michael@0: // Set up event listeners. Process a next invoker if no events were added. michael@0: if (!this.setEventHandler(invoker)) { michael@0: this.processNextInvoker(); michael@0: return; michael@0: } michael@0: michael@0: if (gLogger.isEnabled()) { michael@0: gLogger.logToConsole("Event queue: \n invoke: " + invoker.getID()); michael@0: gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true); michael@0: } michael@0: michael@0: var infoText = "Invoke the '" + invoker.getID() + "' test { "; michael@0: var scnCount = this.mScenarios ? this.mScenarios.length : 0; michael@0: for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) { michael@0: infoText += "scenario #" + scnIdx + ": "; michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: infoText += eventSeq[idx].unexpected ? "un" : "" + michael@0: "expected '" + eventQueue.getEventTypeAsString(eventSeq[idx]) + michael@0: "' event; "; michael@0: } michael@0: } michael@0: infoText += " }"; michael@0: info(infoText); michael@0: michael@0: if (invoker.invoke() == INVOKER_ACTION_FAILED) { michael@0: // Invoker failed to prepare action, fail and finish tests. michael@0: this.processNextInvoker(); michael@0: return; michael@0: } michael@0: michael@0: if (this.hasUnexpectedEventsScenario()) michael@0: this.processNextInvokerInTimeout(true); michael@0: } michael@0: michael@0: this.processNextInvokerInTimeout = michael@0: function eventQueue_processNextInvokerInTimeout(aUncondProcess) michael@0: { michael@0: this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout"); michael@0: michael@0: // No need to wait extra timeout when a) we know we don't need to do that michael@0: // and b) there's no any single unexpected event. michael@0: if (!aUncondProcess && this.areAllEventsExpected()) { michael@0: // We need delay to avoid events coalesce from different invokers. michael@0: var queue = this; michael@0: SimpleTest.executeSoon(function() { queue.processNextInvoker(); }); michael@0: return; michael@0: } michael@0: michael@0: // Check in timeout invoker didn't fire registered events. michael@0: window.setTimeout(function(aQueue) { aQueue.processNextInvoker(); }, 300, michael@0: this); michael@0: } michael@0: michael@0: /** michael@0: * Handle events for the current invoker. michael@0: */ michael@0: this.handleEvent = function eventQueue_handleEvent(aEvent) michael@0: { michael@0: var invoker = this.getInvoker(); michael@0: if (!invoker) // skip events before test was started michael@0: return; michael@0: michael@0: if (!this.mScenarios) { michael@0: // Bad invoker object, error will be reported before processing of next michael@0: // invoker in the queue. michael@0: this.processNextInvoker(); michael@0: return; michael@0: } michael@0: michael@0: if ("debugCheck" in invoker) michael@0: invoker.debugCheck(aEvent); michael@0: michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: var checker = eventSeq[idx]; michael@0: michael@0: // Search through handled expected events to report error if one of them michael@0: // is handled for a second time. michael@0: if (!checker.unexpected && (checker.wasCaught > 0) && michael@0: eventQueue.isSameEvent(checker, aEvent)) { michael@0: checker.wasCaught++; michael@0: continue; michael@0: } michael@0: michael@0: // Search through unexpected events, any match results in error report michael@0: // after this invoker processing (in case of matched scenario only). michael@0: if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) { michael@0: checker.wasCaught++; michael@0: continue; michael@0: } michael@0: michael@0: // Report an error if we hanlded not expected event of unique type michael@0: // (i.e. event types are matched, targets differs). michael@0: if (!checker.unexpected && checker.unique && michael@0: eventQueue.compareEventTypes(checker, aEvent)) { michael@0: var isExppected = false; michael@0: for (var jdx = 0; jdx < eventSeq.length; jdx++) { michael@0: isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent); michael@0: if (isExpected) michael@0: break; michael@0: } michael@0: michael@0: if (!isExpected) { michael@0: ok(false, michael@0: "Unique type " + michael@0: eventQueue.getEventTypeAsString(checker) + " event was handled."); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: var hasMatchedCheckers = false; michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: michael@0: // Check if handled event matches expected sync event. michael@0: var nextChecker = this.getNextExpectedEvent(eventSeq); michael@0: if (nextChecker) { michael@0: if (eventQueue.compareEvents(nextChecker, aEvent)) { michael@0: this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); michael@0: hasMatchedCheckers = true; michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: // Check if handled event matches any expected async events. michael@0: for (idx = 0; idx < eventSeq.length; idx++) { michael@0: if (!eventSeq[idx].unexpected && eventSeq[idx].async) { michael@0: if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { michael@0: this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); michael@0: hasMatchedCheckers = true; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (hasMatchedCheckers) { michael@0: var invoker = this.getInvoker(); michael@0: if ("check" in invoker) michael@0: invoker.check(aEvent); michael@0: } michael@0: michael@0: // If we don't have more events to wait then schedule next invoker. michael@0: if (this.hasMatchedScenario()) { michael@0: if (this.mNextInvokerStatus == kInvokerNotScheduled) { michael@0: this.processNextInvokerInTimeout(); michael@0: michael@0: } else if (this.mNextInvokerStatus == kInvokerCanceled) { michael@0: this.setInvokerStatus(kInvokerPending, michael@0: "Full match. Void the cancelation of next invoker processing"); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // If we have scheduled a next invoker then cancel in case of match. michael@0: if ((this.mNextInvokerStatus == kInvokerPending) && hasMatchedCheckers) { michael@0: this.setInvokerStatus(kInvokerCanceled, michael@0: "Cancel the scheduled invoker in case of match"); michael@0: } michael@0: } michael@0: michael@0: // Helpers michael@0: this.processMatchedChecker = michael@0: function eventQueue_function(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx) michael@0: { michael@0: aMatchedChecker.wasCaught++; michael@0: michael@0: if ("check" in aMatchedChecker) michael@0: aMatchedChecker.check(aEvent); michael@0: michael@0: eventQueue.logEvent(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx, michael@0: this.areExpectedEventsLeft(), michael@0: this.mNextInvokerStatus); michael@0: } michael@0: michael@0: this.getNextExpectedEvent = michael@0: function eventQueue_getNextExpectedEvent(aEventSeq) michael@0: { michael@0: if (!("idx" in aEventSeq)) michael@0: aEventSeq.idx = 0; michael@0: michael@0: while (aEventSeq.idx < aEventSeq.length && michael@0: (aEventSeq[aEventSeq.idx].unexpected || michael@0: aEventSeq[aEventSeq.idx].async || michael@0: aEventSeq[aEventSeq.idx].wasCaught > 0)) { michael@0: aEventSeq.idx++; michael@0: } michael@0: michael@0: return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null; michael@0: } michael@0: michael@0: this.areExpectedEventsLeft = michael@0: function eventQueue_areExpectedEventsLeft(aScenario) michael@0: { michael@0: function scenarioHasUnhandledExpectedEvent(aEventSeq) michael@0: { michael@0: // Check if we have unhandled async (can be anywhere in the sequance) or michael@0: // sync expcected events yet. michael@0: for (var idx = 0; idx < aEventSeq.length; idx++) { michael@0: if (!aEventSeq[idx].unexpected && !aEventSeq[idx].wasCaught) michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: if (aScenario) michael@0: return scenarioHasUnhandledExpectedEvent(aScenario); michael@0: michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: if (scenarioHasUnhandledExpectedEvent(eventSeq)) michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: this.areAllEventsExpected = michael@0: function eventQueue_areAllEventsExpected() michael@0: { michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: if (eventSeq[idx].unexpected) michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: this.isUnexpectedEventScenario = michael@0: function eventQueue_isUnexpectedEventsScenario(aScenario) michael@0: { michael@0: for (var idx = 0; idx < aScenario.length; idx++) { michael@0: if (!aScenario[idx].unexpected) michael@0: break; michael@0: } michael@0: michael@0: return idx == aScenario.length; michael@0: } michael@0: michael@0: this.hasUnexpectedEventsScenario = michael@0: function eventQueue_hasUnexpectedEventsScenario() michael@0: { michael@0: if (this.getInvoker().noEventsOnAction) michael@0: return true; michael@0: michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: this.hasMatchedScenario = michael@0: function eventQueue_hasMatchedScenario() michael@0: { michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var scn = this.mScenarios[scnIdx]; michael@0: if (!this.isUnexpectedEventScenario(scn) && !this.areExpectedEventsLeft(scn)) michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: this.getInvoker = function eventQueue_getInvoker() michael@0: { michael@0: return this.mInvokers[this.mIndex]; michael@0: } michael@0: michael@0: this.getNextInvoker = function eventQueue_getNextInvoker() michael@0: { michael@0: return this.mInvokers[++this.mIndex]; michael@0: } michael@0: michael@0: this.setEventHandler = function eventQueue_setEventHandler(aInvoker) michael@0: { michael@0: if (!("scenarios" in aInvoker) || aInvoker.scenarios.length == 0) { michael@0: var eventSeq = aInvoker.eventSeq; michael@0: var unexpectedEventSeq = aInvoker.unexpectedEventSeq; michael@0: if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) michael@0: eventSeq = [ new invokerChecker(this.mDefEventType, aInvoker.DOMNode) ]; michael@0: michael@0: if (eventSeq || unexpectedEventSeq) michael@0: defineScenario(aInvoker, eventSeq, unexpectedEventSeq); michael@0: } michael@0: michael@0: if (aInvoker.noEventsOnAction) michael@0: return true; michael@0: michael@0: this.mScenarios = aInvoker.scenarios; michael@0: if (!this.mScenarios || !this.mScenarios.length) { michael@0: ok(false, "Broken invoker '" + aInvoker.getID() + "'"); michael@0: return false; michael@0: } michael@0: michael@0: // Register event listeners. michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: michael@0: if (gLogger.isEnabled()) { michael@0: var msg = "scenario #" + scnIdx + michael@0: ", registered events number: " + eventSeq.length; michael@0: gLogger.logToConsole(msg); michael@0: gLogger.logToDOM(msg, true); michael@0: } michael@0: michael@0: // Do not warn about empty event sequances when more than one scenario michael@0: // was registered. michael@0: if (this.mScenarios.length == 1 && eventSeq.length == 0) { michael@0: ok(false, michael@0: "Broken scenario #" + scnIdx + " of invoker '" + aInvoker.getID() + michael@0: "'. No registered events"); michael@0: return false; michael@0: } michael@0: michael@0: for (var idx = 0; idx < eventSeq.length; idx++) michael@0: eventSeq[idx].wasCaught = 0; michael@0: michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: if (gLogger.isEnabled()) { michael@0: var msg = "registered"; michael@0: if (eventSeq[idx].unexpected) michael@0: msg += " unexpected"; michael@0: if (eventSeq[idx].async) michael@0: msg += " async"; michael@0: michael@0: msg += ": event type: " + michael@0: eventQueue.getEventTypeAsString(eventSeq[idx]) + michael@0: ", target: " + eventQueue.getEventTargetDescr(eventSeq[idx], true); michael@0: michael@0: gLogger.logToConsole(msg); michael@0: gLogger.logToDOM(msg, true); michael@0: } michael@0: michael@0: var eventType = eventSeq[idx].type; michael@0: if (typeof eventType == "string") { michael@0: // DOM event michael@0: var target = eventSeq[idx].target; michael@0: if (!target) { michael@0: ok(false, "no target for DOM event!"); michael@0: return false; michael@0: } michael@0: var phase = eventQueue.getEventPhase(eventSeq[idx]); michael@0: target.ownerDocument.addEventListener(eventType, this, phase); michael@0: michael@0: } else { michael@0: // A11y event michael@0: addA11yEventListener(eventType, this); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: this.clearEventHandler = function eventQueue_clearEventHandler() michael@0: { michael@0: if (!this.mScenarios) michael@0: return; michael@0: michael@0: for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { michael@0: var eventSeq = this.mScenarios[scnIdx]; michael@0: for (var idx = 0; idx < eventSeq.length; idx++) { michael@0: var eventType = eventSeq[idx].type; michael@0: if (typeof eventType == "string") { michael@0: // DOM event michael@0: var target = eventSeq[idx].target; michael@0: var phase = eventQueue.getEventPhase(eventSeq[idx]); michael@0: target.ownerDocument.removeEventListener(eventType, this, phase); michael@0: michael@0: } else { michael@0: // A11y event michael@0: removeA11yEventListener(eventType, this); michael@0: } michael@0: } michael@0: } michael@0: this.mScenarios = null; michael@0: } michael@0: michael@0: this.getEventID = function eventQueue_getEventID(aChecker) michael@0: { michael@0: if ("getID" in aChecker) michael@0: return aChecker.getID(); michael@0: michael@0: var invoker = this.getInvoker(); michael@0: return invoker.getID(); michael@0: } michael@0: michael@0: this.setInvokerStatus = function eventQueue_setInvokerStatus(aStatus, aLogMsg) michael@0: { michael@0: this.mNextInvokerStatus = aStatus; michael@0: michael@0: // Uncomment it to debug invoker processing logic. michael@0: //gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg)); michael@0: } michael@0: michael@0: this.mDefEventType = aEventType; michael@0: michael@0: this.mInvokers = new Array(); michael@0: this.mIndex = -1; michael@0: this.mScenarios = null; michael@0: michael@0: this.mNextInvokerStatus = kInvokerNotScheduled; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // eventQueue static members and constants michael@0: michael@0: const kInvokerNotScheduled = 0; michael@0: const kInvokerPending = 1; michael@0: const kInvokerCanceled = 2; michael@0: michael@0: eventQueue.getEventTypeAsString = michael@0: function eventQueue_getEventTypeAsString(aEventOrChecker) michael@0: { michael@0: if (aEventOrChecker instanceof nsIDOMEvent) michael@0: return aEventOrChecker.type; michael@0: michael@0: if (aEventOrChecker instanceof nsIAccessibleEvent) michael@0: return eventTypeToString(aEventOrChecker.eventType); michael@0: michael@0: return (typeof aEventOrChecker.type == "string") ? michael@0: aEventOrChecker.type : eventTypeToString(aEventOrChecker.type); michael@0: } michael@0: michael@0: eventQueue.getEventTargetDescr = michael@0: function eventQueue_getEventTargetDescr(aEventOrChecker, aDontForceTarget) michael@0: { michael@0: if (aEventOrChecker instanceof nsIDOMEvent) michael@0: return prettyName(aEventOrChecker.originalTarget); michael@0: michael@0: if (aEventOrChecker instanceof nsIDOMEvent) michael@0: return prettyName(aEventOrChecker.accessible); michael@0: michael@0: var descr = aEventOrChecker.targetDescr; michael@0: if (descr) michael@0: return descr; michael@0: michael@0: if (aDontForceTarget) michael@0: return "no target description"; michael@0: michael@0: var target = ("target" in aEventOrChecker) ? aEventOrChecker.target : null; michael@0: return prettyName(target); michael@0: } michael@0: michael@0: eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) michael@0: { michael@0: return ("phase" in aChecker) ? aChecker.phase : true; michael@0: } michael@0: michael@0: eventQueue.compareEventTypes = michael@0: function eventQueue_compareEventTypes(aChecker, aEvent) michael@0: { michael@0: var eventType = (aEvent instanceof nsIDOMEvent) ? michael@0: aEvent.type : aEvent.eventType; michael@0: return aChecker.type == eventType; michael@0: } michael@0: michael@0: eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) michael@0: { michael@0: if (!eventQueue.compareEventTypes(aChecker, aEvent)) michael@0: return false; michael@0: michael@0: // If checker provides "match" function then allow the checker to decide michael@0: // whether event is matched. michael@0: if ("match" in aChecker) michael@0: return aChecker.match(aEvent); michael@0: michael@0: var target1 = aChecker.target; michael@0: if (target1 instanceof nsIAccessible) { michael@0: var target2 = (aEvent instanceof nsIDOMEvent) ? michael@0: getAccessible(aEvent.target) : aEvent.accessible; michael@0: michael@0: return target1 == target2; michael@0: } michael@0: michael@0: // If original target isn't suitable then extend interface to support target michael@0: // (original target is used in test_elm_media.html). michael@0: var target2 = (aEvent instanceof nsIDOMEvent) ? michael@0: aEvent.originalTarget : aEvent.DOMNode; michael@0: return target1 == target2; michael@0: } michael@0: michael@0: eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) michael@0: { michael@0: // We don't have stored info about handled event other than its type and michael@0: // target, thus we should filter text change and state change events since michael@0: // they may occur on the same element because of complex changes. michael@0: return this.compareEvents(aChecker, aEvent) && michael@0: !(aEvent instanceof nsIAccessibleTextChangeEvent) && michael@0: !(aEvent instanceof nsIAccessibleStateChangeEvent); michael@0: } michael@0: michael@0: eventQueue.invokerStatusToMsg = michael@0: function eventQueue_invokerStatusToMsg(aInvokerStatus, aMsg) michael@0: { michael@0: var msg = "invoker status: "; michael@0: switch (aInvokerStatus) { michael@0: case kInvokerNotScheduled: michael@0: msg += "not scheduled"; michael@0: break; michael@0: case kInvokerPending: michael@0: msg += "pending"; michael@0: break; michael@0: case kInvokerCanceled: michael@0: msg += "canceled"; michael@0: break; michael@0: } michael@0: michael@0: if (aMsg) michael@0: msg += " (" + aMsg + ")"; michael@0: michael@0: return msg; michael@0: } michael@0: michael@0: eventQueue.logEvent = function eventQueue_logEvent(aOrigEvent, aMatchedChecker, michael@0: aScenarioIdx, aEventIdx, michael@0: aAreExpectedEventsLeft, michael@0: aInvokerStatus) michael@0: { michael@0: // Dump DOM event information. Skip a11y event since it is dumped by michael@0: // gA11yEventObserver. michael@0: if (aOrigEvent instanceof nsIDOMEvent) { michael@0: var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent); michael@0: info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent); michael@0: gLogger.logToDOM(info); michael@0: } michael@0: michael@0: var infoMsg = "unhandled expected events: " + aAreExpectedEventsLeft + michael@0: ", " + eventQueue.invokerStatusToMsg(aInvokerStatus); michael@0: michael@0: var currType = eventQueue.getEventTypeAsString(aMatchedChecker); michael@0: var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); michael@0: var consoleMsg = "*****\nScenario " + aScenarioIdx + michael@0: ", event " + aEventIdx + " matched: " + currType + "\n" + infoMsg + "\n*****"; michael@0: gLogger.logToConsole(consoleMsg); michael@0: michael@0: var emphText = "matched "; michael@0: var msg = "EQ event, type: " + currType + ", target: " + currTargetDescr + michael@0: ", " + infoMsg; michael@0: gLogger.logToDOM(msg, true, emphText); michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Action sequence michael@0: michael@0: /** michael@0: * Deal with action sequence. Used when you need to execute couple of actions michael@0: * each after other one. michael@0: */ michael@0: function sequence() michael@0: { michael@0: /** michael@0: * Append new sequence item. michael@0: * michael@0: * @param aProcessor [in] object implementing interface michael@0: * { michael@0: * // execute item action michael@0: * process: function() {}, michael@0: * // callback, is called when item was processed michael@0: * onProcessed: function() {} michael@0: * }; michael@0: * @param aEventType [in] event type of expected event on item action michael@0: * @param aTarget [in] event target of expected event on item action michael@0: * @param aItemID [in] identifier of item michael@0: */ michael@0: this.append = function sequence_append(aProcessor, aEventType, aTarget, michael@0: aItemID) michael@0: { michael@0: var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID); michael@0: this.items.push(item); michael@0: } michael@0: michael@0: /** michael@0: * Process next sequence item. michael@0: */ michael@0: this.processNext = function sequence_processNext() michael@0: { michael@0: this.idx++; michael@0: if (this.idx >= this.items.length) { michael@0: ok(false, "End of sequence: nothing to process!"); michael@0: SimpleTest.finish(); michael@0: return; michael@0: } michael@0: michael@0: this.items[this.idx].startProcess(); michael@0: } michael@0: michael@0: this.items = new Array(); michael@0: this.idx = -1; michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Event queue invokers michael@0: michael@0: /** michael@0: * Defines a scenario of expected/unexpected events. Each invoker can have michael@0: * one or more scenarios of events. Only one scenario must be completed. michael@0: */ michael@0: function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) michael@0: { michael@0: if (!("scenarios" in aInvoker)) michael@0: aInvoker.scenarios = new Array(); michael@0: michael@0: // Create unified event sequence concatenating expected and unexpected michael@0: // events. michael@0: if (!aEventSeq) michael@0: aEventSeq = []; michael@0: michael@0: for (var idx = 0; idx < aEventSeq.length; idx++) { michael@0: aEventSeq[idx].unexpected |= false; michael@0: aEventSeq[idx].async |= false; michael@0: } michael@0: michael@0: if (aUnexpectedEventSeq) { michael@0: for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) { michael@0: aUnexpectedEventSeq[idx].unexpected = true; michael@0: aUnexpectedEventSeq[idx].async = false; michael@0: } michael@0: michael@0: aEventSeq = aEventSeq.concat(aUnexpectedEventSeq); michael@0: } michael@0: michael@0: aInvoker.scenarios.push(aEventSeq); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Invokers defined below take a checker object (or array of checker objects). michael@0: * An invoker listens for default event type registered in event queue object michael@0: * until its checker is provided. michael@0: * michael@0: * Note, checker object or array of checker objects is optional. michael@0: */ michael@0: michael@0: /** michael@0: * Click invoker. michael@0: */ michael@0: function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) michael@0: { michael@0: this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); michael@0: michael@0: this.invoke = function synthClick_invoke() michael@0: { michael@0: var targetNode = this.DOMNode; michael@0: if (targetNode instanceof nsIDOMDocument) { michael@0: targetNode = michael@0: this.DOMNode.body ? this.DOMNode.body : this.DOMNode.documentElement; michael@0: } michael@0: michael@0: // Scroll the node into view, otherwise synth click may fail. michael@0: if (targetNode instanceof nsIDOMHTMLElement) { michael@0: targetNode.scrollIntoView(true); michael@0: } else if (targetNode instanceof nsIDOMXULElement) { michael@0: var targetAcc = getAccessible(targetNode); michael@0: targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); michael@0: } michael@0: michael@0: var x = 1, y = 1; michael@0: if (aArgs && ("where" in aArgs) && aArgs.where == "right") { michael@0: if (targetNode instanceof nsIDOMHTMLElement) michael@0: x = targetNode.offsetWidth - 1; michael@0: else if (targetNode instanceof nsIDOMXULElement) michael@0: x = targetNode.boxObject.width - 1; michael@0: } michael@0: synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {}); michael@0: } michael@0: michael@0: this.finalCheck = function synthClick_finalCheck() michael@0: { michael@0: // Scroll top window back. michael@0: window.top.scrollTo(0, 0); michael@0: } michael@0: michael@0: this.getID = function synthClick_getID() michael@0: { michael@0: return prettyName(aNodeOrID) + " click"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Mouse move invoker. michael@0: */ michael@0: function synthMouseMove(aID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); michael@0: michael@0: this.invoke = function synthMouseMove_invoke() michael@0: { michael@0: synthesizeMouse(this.DOMNode, 1, 1, { type: "mousemove" }); michael@0: synthesizeMouse(this.DOMNode, 2, 2, { type: "mousemove" }); michael@0: } michael@0: michael@0: this.getID = function synthMouseMove_getID() michael@0: { michael@0: return prettyName(aID) + " mouse move"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * General key press invoker. michael@0: */ michael@0: function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); michael@0: michael@0: this.invoke = function synthKey_invoke() michael@0: { michael@0: synthesizeKey(this.mKey, this.mArgs, this.mWindow); michael@0: } michael@0: michael@0: this.getID = function synthKey_getID() michael@0: { michael@0: var key = this.mKey; michael@0: switch (this.mKey) { michael@0: case "VK_TAB": michael@0: key = "tab"; michael@0: break; michael@0: case "VK_DOWN": michael@0: key = "down"; michael@0: break; michael@0: case "VK_UP": michael@0: key = "up"; michael@0: break; michael@0: case "VK_LEFT": michael@0: key = "left"; michael@0: break; michael@0: case "VK_RIGHT": michael@0: key = "right"; michael@0: break; michael@0: case "VK_HOME": michael@0: key = "home"; michael@0: break; michael@0: case "VK_END": michael@0: key = "end"; michael@0: break; michael@0: case "VK_ESCAPE": michael@0: key = "escape"; michael@0: break; michael@0: case "VK_RETURN": michael@0: key = "enter"; michael@0: break; michael@0: } michael@0: if (aArgs) { michael@0: if (aArgs.shiftKey) michael@0: key += " shift"; michael@0: if (aArgs.ctrlKey) michael@0: key += " ctrl"; michael@0: if (aArgs.altKey) michael@0: key += " alt"; michael@0: } michael@0: return prettyName(aNodeOrID) + " '" + key + " ' key"; michael@0: } michael@0: michael@0: this.mKey = aKey; michael@0: this.mArgs = aArgs ? aArgs : {}; michael@0: this.mWindow = aArgs ? aArgs.window : null; michael@0: } michael@0: michael@0: /** michael@0: * Tab key invoker. michael@0: */ michael@0: function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_TAB", michael@0: { shiftKey: false, window: aWindow }, michael@0: aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Shift tab key invoker. michael@0: */ michael@0: function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_TAB", { shiftKey: true }, michael@0: aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Escape key invoker. michael@0: */ michael@0: function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_ESCAPE", null, michael@0: aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Down arrow key invoker. michael@0: */ michael@0: function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_DOWN", aArgs, michael@0: aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Up arrow key invoker. michael@0: */ michael@0: function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, michael@0: aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Left arrow key invoker. michael@0: */ michael@0: function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_LEFT", aArgs, aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Right arrow key invoker. michael@0: */ michael@0: function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_RIGHT", aArgs, aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Home key invoker. michael@0: */ michael@0: function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * End key invoker. michael@0: */ michael@0: function synthEndKey(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Enter key invoker michael@0: */ michael@0: function synthEnterKey(aID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq); michael@0: } michael@0: michael@0: /** michael@0: * Synth alt + down arrow to open combobox. michael@0: */ michael@0: function synthOpenComboboxKey(aID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true }); michael@0: michael@0: this.getID = function synthOpenComboboxKey_getID() michael@0: { michael@0: return "open combobox (atl + down arrow) " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Focus invoker. michael@0: */ michael@0: function synthFocus(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: var checkerOfEventSeq = michael@0: aCheckerOrEventSeq ? aCheckerOrEventSeq : new focusChecker(aNodeOrID); michael@0: this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq); michael@0: michael@0: this.invoke = function synthFocus_invoke() michael@0: { michael@0: if (this.DOMNode instanceof Components.interfaces.nsIDOMNSEditableElement && michael@0: this.DOMNode.editor || michael@0: this.DOMNode instanceof Components.interfaces.nsIDOMXULTextBoxElement) { michael@0: this.DOMNode.selectionStart = this.DOMNode.selectionEnd = this.DOMNode.value.length; michael@0: } michael@0: this.DOMNode.focus(); michael@0: } michael@0: michael@0: this.getID = function synthFocus_getID() michael@0: { michael@0: return prettyName(aNodeOrID) + " focus"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Focus invoker. Focus the HTML body of content document of iframe. michael@0: */ michael@0: function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: var frameDoc = getNode(aNodeOrID).contentDocument; michael@0: var checkerOrEventSeq = michael@0: aCheckerOrEventSeq ? aCheckerOrEventSeq : new focusChecker(frameDoc); michael@0: this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq); michael@0: michael@0: this.invoke = function synthFocus_invoke() michael@0: { michael@0: this.DOMNode.body.focus(); michael@0: } michael@0: michael@0: this.getID = function synthFocus_getID() michael@0: { michael@0: return prettyName(aNodeOrID) + " frame document focus"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Change the current item when the widget doesn't have a focus. michael@0: */ michael@0: function changeCurrentItem(aID, aItemID) michael@0: { michael@0: this.eventSeq = [ new nofocusChecker() ]; michael@0: michael@0: this.invoke = function changeCurrentItem_invoke() michael@0: { michael@0: var controlNode = getNode(aID); michael@0: var itemNode = getNode(aItemID); michael@0: michael@0: // HTML michael@0: if (controlNode.localName == "input") { michael@0: if (controlNode.checked) michael@0: this.reportError(); michael@0: michael@0: controlNode.checked = true; michael@0: return; michael@0: } michael@0: michael@0: if (controlNode.localName == "select") { michael@0: if (controlNode.selectedIndex == itemNode.index) michael@0: this.reportError(); michael@0: michael@0: controlNode.selectedIndex = itemNode.index; michael@0: return; michael@0: } michael@0: michael@0: // XUL michael@0: if (controlNode.localName == "tree") { michael@0: if (controlNode.currentIndex == aItemID) michael@0: this.reportError(); michael@0: michael@0: controlNode.currentIndex = aItemID; michael@0: return; michael@0: } michael@0: michael@0: if (controlNode.localName == "menulist") { michael@0: if (controlNode.selectedItem == itemNode) michael@0: this.reportError(); michael@0: michael@0: controlNode.selectedItem = itemNode; michael@0: return; michael@0: } michael@0: michael@0: if (controlNode.currentItem == itemNode) michael@0: ok(false, "Error in test: proposed current item is already current" + prettyName(aID)); michael@0: michael@0: controlNode.currentItem = itemNode; michael@0: } michael@0: michael@0: this.getID = function changeCurrentItem_getID() michael@0: { michael@0: return "current item change for " + prettyName(aID); michael@0: } michael@0: michael@0: this.reportError = function changeCurrentItem_reportError() michael@0: { michael@0: ok(false, michael@0: "Error in test: proposed current item '" + aItemID + "' is already current"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Toggle top menu invoker. michael@0: */ michael@0: function toggleTopMenu(aID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthKey(aID, "VK_ALT", null, michael@0: aCheckerOrEventSeq); michael@0: michael@0: this.getID = function toggleTopMenu_getID() michael@0: { michael@0: return "toggle top menu on " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Context menu invoker. michael@0: */ michael@0: function synthContextMenu(aID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, michael@0: { button: 0, type: "contextmenu" }); michael@0: michael@0: this.getID = function synthContextMenu_getID() michael@0: { michael@0: return "context menu on " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Open combobox, autocomplete and etc popup, check expandable states. michael@0: */ michael@0: function openCombobox(aComboboxID) michael@0: { michael@0: this.eventSeq = [ michael@0: new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID) michael@0: ]; michael@0: michael@0: this.invoke = function openCombobox_invoke() michael@0: { michael@0: getNode(aComboboxID).focus(); michael@0: synthesizeKey("VK_DOWN", { altKey: true }); michael@0: } michael@0: michael@0: this.getID = function openCombobox_getID() michael@0: { michael@0: return "open combobox " + prettyName(aComboboxID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Close combobox, autocomplete and etc popup, check expandable states. michael@0: */ michael@0: function closeCombobox(aComboboxID) michael@0: { michael@0: this.eventSeq = [ michael@0: new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID) michael@0: ]; michael@0: michael@0: this.invoke = function closeCombobox_invoke() michael@0: { michael@0: synthesizeKey("VK_ESCAPE", { }); michael@0: } michael@0: michael@0: this.getID = function closeCombobox_getID() michael@0: { michael@0: return "close combobox " + prettyName(aComboboxID); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Select all invoker. michael@0: */ michael@0: function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) michael@0: { michael@0: this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); michael@0: michael@0: this.invoke = function synthSelectAll_invoke() michael@0: { michael@0: if (this.DOMNode instanceof Components.interfaces.nsIDOMHTMLInputElement || michael@0: this.DOMNode instanceof Components.interfaces.nsIDOMXULTextBoxElement) { michael@0: this.DOMNode.select(); michael@0: michael@0: } else { michael@0: window.getSelection().selectAllChildren(this.DOMNode); michael@0: } michael@0: } michael@0: michael@0: this.getID = function synthSelectAll_getID() michael@0: { michael@0: return aNodeOrID + " selectall"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Move the caret to the end of line. michael@0: */ michael@0: function moveToLineEnd(aID, aCaretOffset) michael@0: { michael@0: if (MAC) { michael@0: this.__proto__ = new synthKey(aID, "VK_RIGHT", { metaKey: true }, michael@0: new caretMoveChecker(aCaretOffset, aID)); michael@0: } else { michael@0: this.__proto__ = new synthEndKey(aID, michael@0: new caretMoveChecker(aCaretOffset, aID)); michael@0: } michael@0: michael@0: this.getID = function moveToLineEnd_getID() michael@0: { michael@0: return "move to line end in " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Move the caret to the end of previous line if any. michael@0: */ michael@0: function moveToPrevLineEnd(aID, aCaretOffset) michael@0: { michael@0: this.__proto__ = new synthAction(aID, new caretMoveChecker(aCaretOffset, aID)); michael@0: michael@0: this.invoke = function moveToPrevLineEnd_invoke() michael@0: { michael@0: synthesizeKey("VK_UP", { }); michael@0: michael@0: if (MAC) michael@0: synthesizeKey("VK_RIGHT", { metaKey: true }); michael@0: else michael@0: synthesizeKey("VK_END", { }); michael@0: } michael@0: michael@0: this.getID = function moveToPrevLineEnd_getID() michael@0: { michael@0: return "move to previous line end in " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Move the caret to begining of the line. michael@0: */ michael@0: function moveToLineStart(aID, aCaretOffset) michael@0: { michael@0: if (MAC) { michael@0: this.__proto__ = new synthKey(aID, "VK_LEFT", { metaKey: true }, michael@0: new caretMoveChecker(aCaretOffset, aID)); michael@0: } else { michael@0: this.__proto__ = new synthHomeKey(aID, michael@0: new caretMoveChecker(aCaretOffset, aID)); michael@0: } michael@0: michael@0: this.getID = function moveToLineEnd_getID() michael@0: { michael@0: return "move to line start in " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Move the caret to begining of the text. michael@0: */ michael@0: function moveToTextStart(aID) michael@0: { michael@0: if (MAC) { michael@0: this.__proto__ = new synthKey(aID, "VK_UP", { metaKey: true }, michael@0: new caretMoveChecker(0, aID)); michael@0: } else { michael@0: this.__proto__ = new synthKey(aID, "VK_HOME", { ctrlKey: true }, michael@0: new caretMoveChecker(0, aID)); michael@0: } michael@0: michael@0: this.getID = function moveToTextStart_getID() michael@0: { michael@0: return "move to text start in " + prettyName(aID); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Move the caret in text accessible. michael@0: */ michael@0: function moveCaretToDOMPoint(aID, aDOMPointNodeID, aDOMPointOffset, michael@0: aExpectedOffset, aFocusTargetID, michael@0: aCheckFunc) michael@0: { michael@0: this.target = getAccessible(aID, [nsIAccessibleText]); michael@0: this.DOMPointNode = getNode(aDOMPointNodeID); michael@0: this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; michael@0: this.focusNode = this.focus ? this.focus.DOMNode : null; michael@0: michael@0: this.invoke = function moveCaretToDOMPoint_invoke() michael@0: { michael@0: if (this.focusNode) michael@0: this.focusNode.focus(); michael@0: michael@0: var selection = this.DOMPointNode.ownerDocument.defaultView.getSelection(); michael@0: var selRange = selection.getRangeAt(0); michael@0: selRange.setStart(this.DOMPointNode, aDOMPointOffset); michael@0: selRange.collapse(true); michael@0: michael@0: selection.removeRange(selRange); michael@0: selection.addRange(selRange); michael@0: } michael@0: michael@0: this.getID = function moveCaretToDOMPoint_getID() michael@0: { michael@0: return "Set caret on " + prettyName(aID) + " at point: " + michael@0: prettyName(aDOMPointNodeID) + " node with offset " + aDOMPointOffset; michael@0: } michael@0: michael@0: this.finalCheck = function moveCaretToDOMPoint_finalCheck() michael@0: { michael@0: if (aCheckFunc) michael@0: aCheckFunc.call(); michael@0: } michael@0: michael@0: this.eventSeq = [ michael@0: new caretMoveChecker(aExpectedOffset, this.target) michael@0: ]; michael@0: michael@0: if (this.focus) michael@0: this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); michael@0: } michael@0: michael@0: /** michael@0: * Set caret offset in text accessible. michael@0: */ michael@0: function setCaretOffset(aID, aOffset, aFocusTargetID) michael@0: { michael@0: this.target = getAccessible(aID, [nsIAccessibleText]); michael@0: this.offset = aOffset == -1 ? this.target.characterCount: aOffset; michael@0: this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; michael@0: michael@0: this.invoke = function setCaretOffset_invoke() michael@0: { michael@0: this.target.caretOffset = this.offset; michael@0: } michael@0: michael@0: this.getID = function setCaretOffset_getID() michael@0: { michael@0: return "Set caretOffset on " + prettyName(aID) + " at " + this.offset; michael@0: } michael@0: michael@0: this.eventSeq = [ michael@0: new caretMoveChecker(this.offset, this.target) michael@0: ]; michael@0: michael@0: if (this.focus) michael@0: this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Event queue checkers michael@0: michael@0: /** michael@0: * Common invoker checker (see eventSeq of eventQueue). michael@0: */ michael@0: function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) michael@0: { michael@0: this.type = aEventType; michael@0: this.async = aIsAsync; michael@0: michael@0: this.__defineGetter__("target", invokerChecker_targetGetter); michael@0: this.__defineSetter__("target", invokerChecker_targetSetter); michael@0: michael@0: // implementation details michael@0: function invokerChecker_targetGetter() michael@0: { michael@0: if (typeof this.mTarget == "function") michael@0: return this.mTarget.call(null, this.mTargetFuncArg); michael@0: if (typeof this.mTarget == "string") michael@0: return getNode(this.mTarget); michael@0: michael@0: return this.mTarget; michael@0: } michael@0: michael@0: function invokerChecker_targetSetter(aValue) michael@0: { michael@0: this.mTarget = aValue; michael@0: return this.mTarget; michael@0: } michael@0: michael@0: this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter); michael@0: michael@0: function invokerChecker_targetDescrGetter() michael@0: { michael@0: if (typeof this.mTarget == "function") michael@0: return this.mTarget.name + ", arg: " + this.mTargetFuncArg; michael@0: michael@0: return prettyName(this.mTarget); michael@0: } michael@0: michael@0: this.mTarget = aTargetOrFunc; michael@0: this.mTargetFuncArg = aTargetFuncArg; michael@0: } michael@0: michael@0: /** michael@0: * Generic invoker checker for unexpected events. michael@0: */ michael@0: function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) michael@0: { michael@0: this.__proto__ = new invokerChecker(aEventType, aTargetOrFunc, michael@0: aTargetFuncArg, true); michael@0: michael@0: this.unexpected = true; michael@0: } michael@0: michael@0: /** michael@0: * Common invoker checker for async events. michael@0: */ michael@0: function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) michael@0: { michael@0: this.__proto__ = new invokerChecker(aEventType, aTargetOrFunc, michael@0: aTargetFuncArg, true); michael@0: } michael@0: michael@0: function focusChecker(aTargetOrFunc, aTargetFuncArg) michael@0: { michael@0: this.__proto__ = new invokerChecker(EVENT_FOCUS, aTargetOrFunc, michael@0: aTargetFuncArg, false); michael@0: michael@0: this.unique = true; // focus event must be unique for invoker action michael@0: michael@0: this.check = function focusChecker_check(aEvent) michael@0: { michael@0: testStates(aEvent.accessible, STATE_FOCUSED); michael@0: } michael@0: } michael@0: michael@0: function nofocusChecker(aID) michael@0: { michael@0: this.__proto__ = new focusChecker(aID); michael@0: this.unexpected = true; michael@0: } michael@0: michael@0: /** michael@0: * Text inserted/removed events checker. michael@0: * @param aFromUser [in, optional] kNotFromUserInput or kFromUserInput michael@0: */ michael@0: function textChangeChecker(aID, aStart, aEnd, aTextOrFunc, aIsInserted, aFromUser) michael@0: { michael@0: this.target = getNode(aID); michael@0: this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; michael@0: this.startOffset = aStart; michael@0: this.endOffset = aEnd; michael@0: this.textOrFunc = aTextOrFunc; michael@0: michael@0: this.check = function textChangeChecker_check(aEvent) michael@0: { michael@0: aEvent.QueryInterface(nsIAccessibleTextChangeEvent); michael@0: michael@0: var modifiedText = (typeof this.textOrFunc == "function") ? michael@0: this.textOrFunc() : this.textOrFunc; michael@0: var modifiedTextLen = michael@0: (this.endOffset == -1) ? modifiedText.length : aEnd - aStart; michael@0: michael@0: is(aEvent.start, this.startOffset, michael@0: "Wrong start offset for " + prettyName(aID)); michael@0: is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID)); michael@0: var changeInfo = (aIsInserted ? "inserted" : "removed"); michael@0: is(aEvent.isInserted, aIsInserted, michael@0: "Text was " + changeInfo + " for " + prettyName(aID)); michael@0: is(aEvent.modifiedText, modifiedText, michael@0: "Wrong " + changeInfo + " text for " + prettyName(aID)); michael@0: if (typeof aFromUser != "undefined") michael@0: is(aEvent.isFromUserInput, aFromUser, michael@0: "wrong value of isFromUserInput() for " + prettyName(aID)); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Caret move events checker. michael@0: */ michael@0: function caretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) michael@0: { michael@0: this.__proto__ = new invokerChecker(EVENT_TEXT_CARET_MOVED, michael@0: aTargetOrFunc, aTargetFuncArg); michael@0: michael@0: this.check = function caretMoveChecker_check(aEvent) michael@0: { michael@0: is(aEvent.QueryInterface(nsIAccessibleCaretMoveEvent).caretOffset, michael@0: aCaretOffset, michael@0: "Wrong caret offset for " + prettyName(aEvent.accessible)); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Text selection change checker. michael@0: */ michael@0: function textSelectionChecker(aID, aStartOffset, aEndOffset) michael@0: { michael@0: this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID); michael@0: michael@0: this.check = function textSelectionChecker_check(aEvent) michael@0: { michael@0: if (aStartOffset == aEndOffset) { michael@0: is(getAccessible(aID, [nsIAccessibleText]).caretOffset, aStartOffset, michael@0: "Wrong collapsed selection!"); michael@0: } else { michael@0: testTextGetSelection(aID, aStartOffset, aEndOffset, 0); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * State change checker. michael@0: */ michael@0: function stateChangeChecker(aState, aIsExtraState, aIsEnabled, michael@0: aTargetOrFunc, aTargetFuncArg, aIsAsync, michael@0: aSkipCurrentStateCheck) michael@0: { michael@0: this.__proto__ = new invokerChecker(EVENT_STATE_CHANGE, aTargetOrFunc, michael@0: aTargetFuncArg, aIsAsync); michael@0: michael@0: this.check = function stateChangeChecker_check(aEvent) michael@0: { michael@0: var event = null; michael@0: try { michael@0: var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); michael@0: } catch (e) { michael@0: ok(false, "State change event was expected"); michael@0: } michael@0: michael@0: if (!event) michael@0: return; michael@0: michael@0: is(event.isExtraState, aIsExtraState, michael@0: "Wrong extra state bit of the statechange event."); michael@0: isState(event.state, aState, aIsExtraState, michael@0: "Wrong state of the statechange event."); michael@0: is(event.isEnabled, aIsEnabled, michael@0: "Wrong state of statechange event state"); michael@0: michael@0: if (aSkipCurrentStateCheck) { michael@0: todo(false, "State checking was skipped!"); michael@0: return; michael@0: } michael@0: michael@0: var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0; michael@0: var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0; michael@0: var unxpdState = aIsEnabled ? 0 : (aIsExtraState ? 0 : aState); michael@0: var unxpdExtraState = aIsEnabled ? 0 : (aIsExtraState ? aState : 0); michael@0: testStates(event.accessible, state, extraState, unxpdState, unxpdExtraState); michael@0: } michael@0: michael@0: this.match = function stateChangeChecker_match(aEvent) michael@0: { michael@0: if (aEvent instanceof nsIAccessibleStateChangeEvent) { michael@0: var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); michael@0: return (aEvent.accessible == getAccessible(this.target)) && michael@0: (scEvent.state == aState); michael@0: } michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: function asyncStateChangeChecker(aState, aIsExtraState, aIsEnabled, michael@0: aTargetOrFunc, aTargetFuncArg) michael@0: { michael@0: this.__proto__ = new stateChangeChecker(aState, aIsExtraState, aIsEnabled, michael@0: aTargetOrFunc, aTargetFuncArg, true); michael@0: } michael@0: michael@0: /** michael@0: * Expanded state change checker. michael@0: */ michael@0: function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) michael@0: { michael@0: this.__proto__ = new invokerChecker(EVENT_STATE_CHANGE, aTargetOrFunc, michael@0: aTargetFuncArg); michael@0: michael@0: this.check = function expandedStateChecker_check(aEvent) michael@0: { michael@0: var event = null; michael@0: try { michael@0: var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); michael@0: } catch (e) { michael@0: ok(false, "State change event was expected"); michael@0: } michael@0: michael@0: if (!event) michael@0: return; michael@0: michael@0: is(event.state, STATE_EXPANDED, "Wrong state of the statechange event."); michael@0: is(event.isExtraState, false, michael@0: "Wrong extra state bit of the statechange event."); michael@0: is(event.isEnabled, aIsEnabled, michael@0: "Wrong state of statechange event state"); michael@0: michael@0: testStates(event.accessible, michael@0: (aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED)); michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Event sequances (array of predefined checkers) michael@0: michael@0: /** michael@0: * Event seq for single selection change. michael@0: */ michael@0: function selChangeSeq(aUnselectedID, aSelectedID) michael@0: { michael@0: if (!aUnselectedID) { michael@0: return [ michael@0: new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), michael@0: new invokerChecker(EVENT_SELECTION, aSelectedID) michael@0: ]; michael@0: } michael@0: michael@0: // Return two possible scenarios: depending on widget type when selection is michael@0: // moved the the order of items that get selected and unselected may vary. michael@0: return [ michael@0: [ michael@0: new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), michael@0: new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), michael@0: new invokerChecker(EVENT_SELECTION, aSelectedID) michael@0: ], michael@0: [ michael@0: new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), michael@0: new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), michael@0: new invokerChecker(EVENT_SELECTION, aSelectedID) michael@0: ] michael@0: ]; michael@0: } michael@0: michael@0: /** michael@0: * Event seq for item removed form the selection. michael@0: */ michael@0: function selRemoveSeq(aUnselectedID) michael@0: { michael@0: return [ michael@0: new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), michael@0: new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID) michael@0: ]; michael@0: } michael@0: michael@0: /** michael@0: * Event seq for item added to the selection. michael@0: */ michael@0: function selAddSeq(aSelectedID) michael@0: { michael@0: return [ michael@0: new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), michael@0: new invokerChecker(EVENT_SELECTION_ADD, aSelectedID) michael@0: ]; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Private implementation details. michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // General michael@0: michael@0: var gA11yEventListeners = {}; michael@0: var gA11yEventApplicantsCount = 0; michael@0: michael@0: var gA11yEventObserver = michael@0: { michael@0: observe: function observe(aSubject, aTopic, aData) michael@0: { michael@0: if (aTopic != "accessible-event") michael@0: return; michael@0: michael@0: var event; michael@0: try { michael@0: event = aSubject.QueryInterface(nsIAccessibleEvent); michael@0: } catch (ex) { michael@0: // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered. michael@0: // Remove the leftover observer, otherwise it "leaks" to all the following tests. michael@0: Services.obs.removeObserver(this, "accessible-event"); michael@0: // Forward the exception, with added explanation. michael@0: throw "[accessible/events.js, gA11yEventObserver.observe] This is expected if a previous test has been aborted... Initial exception was: [ " + ex + " ]"; michael@0: } michael@0: var listenersArray = gA11yEventListeners[event.eventType]; michael@0: michael@0: var eventFromDumpArea = false; michael@0: if (gLogger.isEnabled()) { // debug stuff michael@0: eventFromDumpArea = true; michael@0: michael@0: var target = event.DOMNode; michael@0: var dumpElm = gA11yEventDumpID ? michael@0: document.getElementById(gA11yEventDumpID) : null; michael@0: michael@0: if (dumpElm) { michael@0: var parent = target; michael@0: while (parent && parent != dumpElm) michael@0: parent = parent.parentNode; michael@0: } michael@0: michael@0: if (!dumpElm || parent != dumpElm) { michael@0: var type = eventTypeToString(event.eventType); michael@0: var info = "Event type: " + type; michael@0: michael@0: if (event instanceof nsIAccessibleStateChangeEvent) { michael@0: var stateStr = statesToString(event.isExtraState ? 0 : event.state, michael@0: event.isExtraState ? event.state : 0); michael@0: info += ", state: " + stateStr + ", is enabled: " + event.isEnabled; michael@0: michael@0: } else if (event instanceof nsIAccessibleTextChangeEvent) { michael@0: info += ", start: " + event.start + ", length: " + event.length + michael@0: ", " + (event.isInserted ? "inserted" : "removed") + michael@0: " text: " + event.modifiedText; michael@0: } michael@0: michael@0: info += ". Target: " + prettyName(event.accessible); michael@0: michael@0: if (listenersArray) michael@0: info += ". Listeners count: " + listenersArray.length; michael@0: michael@0: if (gLogger.hasFeature("parentchain:" + type)) { michael@0: info += "\nParent chain:\n"; michael@0: var acc = event.accessible; michael@0: while (acc) { michael@0: info += " " + prettyName(acc) + "\n"; michael@0: acc = acc.parent; michael@0: } michael@0: } michael@0: michael@0: eventFromDumpArea = false; michael@0: gLogger.log(info); michael@0: } michael@0: } michael@0: michael@0: // Do not notify listeners if event is result of event log changes. michael@0: if (!listenersArray || eventFromDumpArea) michael@0: return; michael@0: michael@0: for (var index = 0; index < listenersArray.length; index++) michael@0: listenersArray[index].handleEvent(event); michael@0: } michael@0: }; michael@0: michael@0: function listenA11yEvents(aStartToListen) michael@0: { michael@0: if (aStartToListen) { michael@0: // Add observer when adding the first applicant only. michael@0: if (!(gA11yEventApplicantsCount++)) michael@0: Services.obs.addObserver(gA11yEventObserver, "accessible-event", false); michael@0: } else { michael@0: // Remove observer when there are no more applicants only. michael@0: // '< 0' case should not happen, but just in case: removeObserver() will throw. michael@0: if (--gA11yEventApplicantsCount <= 0) michael@0: Services.obs.removeObserver(gA11yEventObserver, "accessible-event"); michael@0: } michael@0: } michael@0: michael@0: function addA11yEventListener(aEventType, aEventHandler) michael@0: { michael@0: if (!(aEventType in gA11yEventListeners)) michael@0: gA11yEventListeners[aEventType] = new Array(); michael@0: michael@0: var listenersArray = gA11yEventListeners[aEventType]; michael@0: var index = listenersArray.indexOf(aEventHandler); michael@0: if (index == -1) michael@0: listenersArray.push(aEventHandler); michael@0: } michael@0: michael@0: function removeA11yEventListener(aEventType, aEventHandler) michael@0: { michael@0: var listenersArray = gA11yEventListeners[aEventType]; michael@0: if (!listenersArray) michael@0: return false; michael@0: michael@0: var index = listenersArray.indexOf(aEventHandler); michael@0: if (index == -1) michael@0: return false; michael@0: michael@0: listenersArray.splice(index, 1); michael@0: michael@0: if (!listenersArray.length) { michael@0: gA11yEventListeners[aEventType] = null; michael@0: delete gA11yEventListeners[aEventType]; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Used to dump debug information. michael@0: */ michael@0: var gLogger = michael@0: { michael@0: /** michael@0: * Return true if dump is enabled. michael@0: */ michael@0: isEnabled: function debugOutput_isEnabled() michael@0: { michael@0: return gA11yEventDumpID || gA11yEventDumpToConsole || michael@0: gA11yEventDumpToAppConsole; michael@0: }, michael@0: michael@0: /** michael@0: * Dump information into DOM and console if applicable. michael@0: */ michael@0: log: function logger_log(aMsg) michael@0: { michael@0: this.logToConsole(aMsg); michael@0: this.logToAppConsole(aMsg); michael@0: this.logToDOM(aMsg); michael@0: }, michael@0: michael@0: /** michael@0: * Log message to DOM. michael@0: * michael@0: * @param aMsg [in] the primary message michael@0: * @param aHasIndent [in, optional] if specified the message has an indent michael@0: * @param aPreEmphText [in, optional] the text is colored and appended prior michael@0: * primary message michael@0: */ michael@0: logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) michael@0: { michael@0: if (gA11yEventDumpID == "") michael@0: return; michael@0: michael@0: var dumpElm = document.getElementById(gA11yEventDumpID); michael@0: if (!dumpElm) { michael@0: ok(false, michael@0: "No dump element '" + gA11yEventDumpID + "' within the document!"); michael@0: return; michael@0: } michael@0: michael@0: var containerTagName = document instanceof nsIDOMHTMLDocument ? michael@0: "div" : "description"; michael@0: michael@0: var container = document.createElement(containerTagName); michael@0: if (aHasIndent) michael@0: container.setAttribute("style", "padding-left: 10px;"); michael@0: michael@0: if (aPreEmphText) { michael@0: var inlineTagName = document instanceof nsIDOMHTMLDocument ? michael@0: "span" : "description"; michael@0: var emphElm = document.createElement(inlineTagName); michael@0: emphElm.setAttribute("style", "color: blue;"); michael@0: emphElm.textContent = aPreEmphText; michael@0: michael@0: container.appendChild(emphElm); michael@0: } michael@0: michael@0: var textNode = document.createTextNode(aMsg); michael@0: container.appendChild(textNode); michael@0: michael@0: dumpElm.appendChild(container); michael@0: }, michael@0: michael@0: /** michael@0: * Log message to console. michael@0: */ michael@0: logToConsole: function logger_logToConsole(aMsg) michael@0: { michael@0: if (gA11yEventDumpToConsole) michael@0: dump("\n" + aMsg + "\n"); michael@0: }, michael@0: michael@0: /** michael@0: * Log message to error console. michael@0: */ michael@0: logToAppConsole: function logger_logToAppConsole(aMsg) michael@0: { michael@0: if (gA11yEventDumpToAppConsole) michael@0: Services.console.logStringMessage("events: " + aMsg); michael@0: }, michael@0: michael@0: /** michael@0: * Return true if logging feature is enabled. michael@0: */ michael@0: hasFeature: function logger_hasFeature(aFeature) michael@0: { michael@0: var startIdx = gA11yEventDumpFeature.indexOf(aFeature); michael@0: if (startIdx == - 1) michael@0: return false; michael@0: michael@0: var endIdx = startIdx + aFeature.length; michael@0: return endIdx == gA11yEventDumpFeature.length || michael@0: gA11yEventDumpFeature[endIdx] == ";"; michael@0: } michael@0: }; michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Sequence michael@0: michael@0: /** michael@0: * Base class of sequence item. michael@0: */ michael@0: function sequenceItem(aProcessor, aEventType, aTarget, aItemID) michael@0: { michael@0: // private michael@0: michael@0: this.startProcess = function sequenceItem_startProcess() michael@0: { michael@0: this.queue.invoke(); michael@0: } michael@0: michael@0: var item = this; michael@0: michael@0: this.queue = new eventQueue(); michael@0: this.queue.onFinish = function() michael@0: { michael@0: aProcessor.onProcessed(); michael@0: return DO_NOT_FINISH_TEST; michael@0: } michael@0: michael@0: var invoker = { michael@0: invoke: function invoker_invoke() { michael@0: return aProcessor.process(); michael@0: }, michael@0: getID: function invoker_getID() michael@0: { michael@0: return aItemID; michael@0: }, michael@0: eventSeq: [ new invokerChecker(aEventType, aTarget) ] michael@0: }; michael@0: michael@0: this.queue.push(invoker); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: // Event queue invokers michael@0: michael@0: /** michael@0: * Invoker base class for prepare an action. michael@0: */ michael@0: function synthAction(aNodeOrID, aEventsObj) michael@0: { michael@0: this.DOMNode = getNode(aNodeOrID); michael@0: michael@0: if (aEventsObj) { michael@0: var scenarios = null; michael@0: if (aEventsObj instanceof Array) { michael@0: if (aEventsObj[0] instanceof Array) michael@0: scenarios = aEventsObj; // scenarios michael@0: else michael@0: scenarios = [ aEventsObj ]; // event sequance michael@0: } else { michael@0: scenarios = [ [ aEventsObj ] ]; // a single checker object michael@0: } michael@0: michael@0: for (var i = 0; i < scenarios.length; i++) michael@0: defineScenario(this, scenarios[i]); michael@0: } michael@0: michael@0: this.getID = function synthAction_getID() michael@0: { return prettyName(aNodeOrID) + " action"; } michael@0: }