gecko-dev/accessible/tests/mochitest/events.js

2287 lines
65 KiB
JavaScript

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