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