Bug 513213 - coalesce events when new event is appended to the queue, r=marcoz, ginn, davidb

This commit is contained in:
Alexander Surkov 2009-09-03 10:01:18 +08:00
parent 0a322165de
commit ad794d9985
8 changed files with 630 additions and 157 deletions

View File

@ -311,89 +311,89 @@ void
nsAccEvent::ApplyEventRules(nsCOMArray<nsIAccessibleEvent> &aEventsToFire) nsAccEvent::ApplyEventRules(nsCOMArray<nsIAccessibleEvent> &aEventsToFire)
{ {
PRUint32 numQueuedEvents = aEventsToFire.Count(); PRUint32 numQueuedEvents = aEventsToFire.Count();
for (PRInt32 tail = numQueuedEvents - 1; tail >= 0; tail --) { PRInt32 tail = numQueuedEvents - 1;
nsRefPtr<nsAccEvent> tailEvent = GetAccEventPtr(aEventsToFire[tail]);
switch(tailEvent->mEventRule) {
case nsAccEvent::eCoalesceFromSameSubtree:
{
for (PRInt32 index = 0; index < tail; index ++) {
nsRefPtr<nsAccEvent> thisEvent = GetAccEventPtr(aEventsToFire[index]);
if (thisEvent->mEventType != tailEvent->mEventType)
continue; // Different type
if (thisEvent->mEventRule == nsAccEvent::eAllowDupes || nsRefPtr<nsAccEvent> tailEvent = GetAccEventPtr(aEventsToFire[tail]);
thisEvent->mEventRule == nsAccEvent::eDoNotEmit) switch(tailEvent->mEventRule) {
continue; // Do not need to check case nsAccEvent::eCoalesceFromSameSubtree:
{
for (PRInt32 index = 0; index < tail; index ++) {
nsRefPtr<nsAccEvent> thisEvent = GetAccEventPtr(aEventsToFire[index]);
if (thisEvent->mEventType != tailEvent->mEventType)
continue; // Different type
if (thisEvent->mDOMNode == tailEvent->mDOMNode) { if (thisEvent->mEventRule == nsAccEvent::eAllowDupes ||
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) { thisEvent->mEventRule == nsAccEvent::eDoNotEmit)
CoalesceReorderEventsFromSameSource(thisEvent, tailEvent); continue; // Do not need to check
continue;
}
// Dupe if (thisEvent->mDOMNode == tailEvent->mDOMNode) {
thisEvent->mEventRule = nsAccEvent::eDoNotEmit; if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameSource(thisEvent, tailEvent);
continue; continue;
} }
if (nsCoreUtils::IsAncestorOf(tailEvent->mDOMNode,
thisEvent->mDOMNode)) {
// thisDOMNode is a descendant of tailDOMNode
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameTree(tailEvent, thisEvent);
continue;
}
// Do not emit thisEvent, also apply this result to sibling // Dupe
// nodes of thisDOMNode. thisEvent->mEventRule = nsAccEvent::eDoNotEmit;
thisEvent->mEventRule = nsAccEvent::eDoNotEmit; continue;
ApplyToSiblings(aEventsToFire, 0, index, thisEvent->mEventType, }
thisEvent->mDOMNode, nsAccEvent::eDoNotEmit); if (nsCoreUtils::IsAncestorOf(tailEvent->mDOMNode,
thisEvent->mDOMNode)) {
// thisDOMNode is a descendant of tailDOMNode
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameTree(tailEvent, thisEvent);
continue; continue;
} }
if (nsCoreUtils::IsAncestorOf(thisEvent->mDOMNode,
tailEvent->mDOMNode)) {
// tailDOMNode is a descendant of thisDOMNode
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameTree(thisEvent, tailEvent);
continue;
}
// Do not emit tailEvent, also apply this result to sibling // Do not emit thisEvent, also apply this result to sibling
// nodes of tailDOMNode. // nodes of thisDOMNode.
tailEvent->mEventRule = nsAccEvent::eDoNotEmit; thisEvent->mEventRule = nsAccEvent::eDoNotEmit;
ApplyToSiblings(aEventsToFire, 0, tail, tailEvent->mEventType, ApplyToSiblings(aEventsToFire, 0, index, thisEvent->mEventType,
tailEvent->mDOMNode, nsAccEvent::eDoNotEmit); thisEvent->mDOMNode, nsAccEvent::eDoNotEmit);
break; continue;
}
if (nsCoreUtils::IsAncestorOf(thisEvent->mDOMNode,
tailEvent->mDOMNode)) {
// tailDOMNode is a descendant of thisDOMNode
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameTree(thisEvent, tailEvent);
continue;
} }
} // for (index)
if (tailEvent->mEventRule != nsAccEvent::eDoNotEmit) { // Do not emit tailEvent, also apply this result to sibling
// Not in another event node's subtree, and no other event is in // nodes of tailDOMNode.
// this event node's subtree. tailEvent->mEventRule = nsAccEvent::eDoNotEmit;
// This event should be emitted
// Apply this result to sibling nodes of tailDOMNode
ApplyToSiblings(aEventsToFire, 0, tail, tailEvent->mEventType, ApplyToSiblings(aEventsToFire, 0, tail, tailEvent->mEventType,
tailEvent->mDOMNode, nsAccEvent::eAllowDupes); tailEvent->mDOMNode, nsAccEvent::eDoNotEmit);
break;
} }
} break; // case eCoalesceFromSameSubtree } // for (index)
case nsAccEvent::eRemoveDupes: if (tailEvent->mEventRule != nsAccEvent::eDoNotEmit) {
{ // Not in another event node's subtree, and no other event is in
// Check for repeat events. // this event node's subtree.
for (PRInt32 index = 0; index < tail; index ++) { // This event should be emitted
nsRefPtr<nsAccEvent> accEvent = GetAccEventPtr(aEventsToFire[index]); // Apply this result to sibling nodes of tailDOMNode
if (accEvent->mEventType == tailEvent->mEventType && ApplyToSiblings(aEventsToFire, 0, tail, tailEvent->mEventType,
accEvent->mEventRule == tailEvent->mEventRule && tailEvent->mDOMNode, nsAccEvent::eAllowDupes);
accEvent->mDOMNode == tailEvent->mDOMNode) { }
accEvent->mEventRule = nsAccEvent::eDoNotEmit; } break; // case eCoalesceFromSameSubtree
}
case nsAccEvent::eRemoveDupes:
{
// Check for repeat events.
for (PRInt32 index = 0; index < tail; index ++) {
nsRefPtr<nsAccEvent> accEvent = GetAccEventPtr(aEventsToFire[index]);
if (accEvent->mEventType == tailEvent->mEventType &&
accEvent->mEventRule == tailEvent->mEventRule &&
accEvent->mDOMNode == tailEvent->mDOMNode) {
accEvent->mEventRule = nsAccEvent::eDoNotEmit;
} }
} break; // case eRemoveDupes }
} break; // case eRemoveDupes
default: default:
break; // case eAllowDupes, eDoNotEmit break; // case eAllowDupes, eDoNotEmit
} // switch } // switch
} // for (tail)
} }
/* static */ /* static */

View File

@ -1185,8 +1185,8 @@ nsDocAccessible::AttributeChangedImpl(nsIContent* aContent, PRInt32 aNameSpaceID
aAttribute == nsAccessibilityAtoms::title || aAttribute == nsAccessibilityAtoms::title ||
aAttribute == nsAccessibilityAtoms::aria_label || aAttribute == nsAccessibilityAtoms::aria_label ||
aAttribute == nsAccessibilityAtoms::aria_labelledby) { aAttribute == nsAccessibilityAtoms::aria_labelledby) {
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE,
targetNode); targetNode);
return; return;
} }
@ -1208,21 +1208,21 @@ nsDocAccessible::AttributeChangedImpl(nsIContent* aContent, PRInt32 aNameSpaceID
nsCOMPtr<nsIDOMNode> multiSelectDOMNode; nsCOMPtr<nsIDOMNode> multiSelectDOMNode;
multiSelectAccessNode->GetDOMNode(getter_AddRefs(multiSelectDOMNode)); multiSelectAccessNode->GetDOMNode(getter_AddRefs(multiSelectDOMNode));
NS_ASSERTION(multiSelectDOMNode, "A new accessible without a DOM node!"); NS_ASSERTION(multiSelectDOMNode, "A new accessible without a DOM node!");
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN,
multiSelectDOMNode, multiSelectDOMNode,
nsAccEvent::eAllowDupes); nsAccEvent::eAllowDupes);
static nsIContent::AttrValuesArray strings[] = static nsIContent::AttrValuesArray strings[] =
{&nsAccessibilityAtoms::_empty, &nsAccessibilityAtoms::_false, nsnull}; {&nsAccessibilityAtoms::_empty, &nsAccessibilityAtoms::_false, nsnull};
if (aContent->FindAttrValueIn(kNameSpaceID_None, aAttribute, if (aContent->FindAttrValueIn(kNameSpaceID_None, aAttribute,
strings, eCaseMatters) >= 0) { strings, eCaseMatters) >= 0) {
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_SELECTION_REMOVE, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_SELECTION_REMOVE,
targetNode); targetNode);
return; return;
} }
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_SELECTION_ADD, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_SELECTION_ADD,
targetNode); targetNode);
} }
} }
@ -1340,7 +1340,8 @@ nsDocAccessible::ARIAAttributeChanged(nsIContent* aContent, nsIAtom* aAttribute)
aContent->AttrValueIs(kNameSpaceID_None, aContent->AttrValueIs(kNameSpaceID_None,
nsAccessibilityAtoms::aria_valuetext, nsAccessibilityAtoms::_empty, nsAccessibilityAtoms::aria_valuetext, nsAccessibilityAtoms::_empty,
eCaseMatters)))) { eCaseMatters)))) {
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, targetNode); FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE,
targetNode);
return; return;
} }
@ -1357,8 +1358,8 @@ nsDocAccessible::ARIAAttributeChanged(nsIContent* aContent, nsIAtom* aAttribute)
// at least until native API comes up with a more meaningful event. // at least until native API comes up with a more meaningful event.
if (aAttribute == nsAccessibilityAtoms::aria_grabbed || if (aAttribute == nsAccessibilityAtoms::aria_grabbed ||
aAttribute == nsAccessibilityAtoms::aria_dropeffect) { aAttribute == nsAccessibilityAtoms::aria_dropeffect) {
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED,
targetNode); targetNode);
} }
} }
@ -1592,13 +1593,14 @@ nsDocAccessible::CreateTextChangeEventForNode(nsIAccessible *aContainerAccessibl
return event; return event;
} }
nsresult nsDocAccessible::FireDelayedToolkitEvent(PRUint32 aEvent, nsresult
nsIDOMNode *aDOMNode, nsDocAccessible::FireDelayedAccessibleEvent(PRUint32 aEventType,
nsAccEvent::EEventRule aAllowDupes, nsIDOMNode *aDOMNode,
PRBool aIsAsynch) nsAccEvent::EEventRule aAllowDupes,
PRBool aIsAsynch)
{ {
nsCOMPtr<nsIAccessibleEvent> event = nsCOMPtr<nsIAccessibleEvent> event =
new nsAccEvent(aEvent, aDOMNode, aIsAsynch, aAllowDupes); new nsAccEvent(aEventType, aDOMNode, aIsAsynch, aAllowDupes);
NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY); NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
return FireDelayedAccessibleEvent(event); return FireDelayedAccessibleEvent(event);
@ -1616,6 +1618,10 @@ nsDocAccessible::FireDelayedAccessibleEvent(nsIAccessibleEvent *aEvent)
} }
mEventsToFire.AppendObject(aEvent); mEventsToFire.AppendObject(aEvent);
// Filter events.
nsAccEvent::ApplyEventRules(mEventsToFire);
if (mEventsToFire.Count() == 1) { if (mEventsToFire.Count() == 1) {
// This is be the first delayed event in queue, start timer // This is be the first delayed event in queue, start timer
// so that event gets fired via FlushEventsCallback // so that event gets fired via FlushEventsCallback
@ -1643,9 +1649,6 @@ nsDocAccessible::FlushPendingEvents()
// visibility. We don't flush the display because we don't care about // visibility. We don't flush the display because we don't care about
// painting. If no flush is necessary the method will simple return. // painting. If no flush is necessary the method will simple return.
presShell->FlushPendingNotifications(Flush_Layout); presShell->FlushPendingNotifications(Flush_Layout);
// filter events
nsAccEvent::ApplyEventRules(mEventsToFire);
} }
for (PRUint32 index = 0; index < length; index ++) { for (PRUint32 index = 0; index < length; index ++) {
@ -2105,17 +2108,17 @@ nsDocAccessible::InvalidateCacheSubtree(nsIContent *aChild,
// nsIAccessibleStates::STATE_INVISIBLE for the event's accessible object. // nsIAccessibleStates::STATE_INVISIBLE for the event's accessible object.
PRUint32 additionEvent = isAsynch ? nsIAccessibleEvent::EVENT_ASYNCH_SHOW : PRUint32 additionEvent = isAsynch ? nsIAccessibleEvent::EVENT_ASYNCH_SHOW :
nsIAccessibleEvent::EVENT_DOM_CREATE; nsIAccessibleEvent::EVENT_DOM_CREATE;
FireDelayedToolkitEvent(additionEvent, childNode, FireDelayedAccessibleEvent(additionEvent, childNode,
nsAccEvent::eCoalesceFromSameSubtree, nsAccEvent::eCoalesceFromSameSubtree,
isAsynch); isAsynch);
// Check to see change occured in an ARIA menu, and fire // Check to see change occured in an ARIA menu, and fire
// an EVENT_MENUPOPUP_START if it did. // an EVENT_MENUPOPUP_START if it did.
nsRoleMapEntry *roleMapEntry = nsAccUtils::GetRoleMapEntry(childNode); nsRoleMapEntry *roleMapEntry = nsAccUtils::GetRoleMapEntry(childNode);
if (roleMapEntry && roleMapEntry->role == nsIAccessibleRole::ROLE_MENUPOPUP) { if (roleMapEntry && roleMapEntry->role == nsIAccessibleRole::ROLE_MENUPOPUP) {
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START,
childNode, nsAccEvent::eRemoveDupes, childNode, nsAccEvent::eRemoveDupes,
isAsynch); isAsynch);
} }
// Check to see if change occured inside an alert, and fire an EVENT_ALERT if it did // Check to see if change occured inside an alert, and fire an EVENT_ALERT if it did
@ -2123,8 +2126,8 @@ nsDocAccessible::InvalidateCacheSubtree(nsIContent *aChild,
while (PR_TRUE) { while (PR_TRUE) {
if (roleMapEntry && roleMapEntry->role == nsIAccessibleRole::ROLE_ALERT) { if (roleMapEntry && roleMapEntry->role == nsIAccessibleRole::ROLE_ALERT) {
nsCOMPtr<nsIDOMNode> alertNode(do_QueryInterface(ancestor)); nsCOMPtr<nsIDOMNode> alertNode(do_QueryInterface(ancestor));
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_ALERT, alertNode, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_ALERT, alertNode,
nsAccEvent::eRemoveDupes, isAsynch); nsAccEvent::eRemoveDupes, isAsynch);
break; break;
} }
ancestor = ancestor->GetParent(); ancestor = ancestor->GetParent();

View File

@ -121,15 +121,15 @@ public:
/** /**
* Non-virtual method to fire a delayed event after a 0 length timeout. * Non-virtual method to fire a delayed event after a 0 length timeout.
* *
* @param aEvent [in] the nsIAccessibleEvent event type * @param aEventType [in] the nsIAccessibleEvent event type
* @param aDOMNode [in] DOM node the accesible event should be fired for * @param aDOMNode [in] DOM node the accesible event should be fired for
* @param aAllowDupes [in] rule to process an event (see EEventRule constants) * @param aAllowDupes [in] rule to process an event (see EEventRule constants)
* @param aIsAsynch [in] set to PR_TRUE if this is not being called from * @param aIsAsynch [in] set to PR_TRUE if this is not being called from
* code synchronous with a DOM event * code synchronous with a DOM event
*/ */
nsresult FireDelayedToolkitEvent(PRUint32 aEvent, nsIDOMNode *aDOMNode, nsresult FireDelayedAccessibleEvent(PRUint32 aEventType, nsIDOMNode *aDOMNode,
nsAccEvent::EEventRule aAllowDupes = nsAccEvent::eRemoveDupes, nsAccEvent::EEventRule aAllowDupes = nsAccEvent::eRemoveDupes,
PRBool aIsAsynch = PR_FALSE); PRBool aIsAsynch = PR_FALSE);
/** /**
* Fire accessible event after timeout. * Fire accessible event after timeout.

View File

@ -396,7 +396,8 @@ void nsRootAccessible::TryFireEarlyLoadEvent(nsIDOMNode *aDocNode)
NS_ASSERTION(rootContentTreeItem, "No root content tree item"); NS_ASSERTION(rootContentTreeItem, "No root content tree item");
if (rootContentTreeItem == treeItem) { if (rootContentTreeItem == treeItem) {
// No frames or iframes, so we can fire the doc load finished event early // No frames or iframes, so we can fire the doc load finished event early
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_INTERNAL_LOAD, aDocNode); FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_INTERNAL_LOAD,
aDocNode);
} }
} }
@ -538,9 +539,9 @@ PRBool nsRootAccessible::FireAccessibleFocusEvent(nsIAccessible *aAccessible,
} }
} }
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_FOCUS, FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_FOCUS,
finalFocusNode, nsAccEvent::eRemoveDupes, finalFocusNode, nsAccEvent::eRemoveDupes,
aIsAsynch); aIsAsynch);
return PR_TRUE; return PR_TRUE;
} }
@ -893,7 +894,8 @@ nsresult nsRootAccessible::HandleEventWithTarget(nsIDOMEvent* aEvent,
FireCurrentFocusEvent(); FireCurrentFocusEvent();
} }
else if (eventType.EqualsLiteral("ValueChange")) { else if (eventType.EqualsLiteral("ValueChange")) {
FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aTargetNode, nsAccEvent::eRemoveDupes); FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE,
aTargetNode, nsAccEvent::eRemoveDupes);
} }
#ifdef DEBUG #ifdef DEBUG
else if (eventType.EqualsLiteral("mouseover")) { else if (eventType.EqualsLiteral("mouseover")) {

View File

@ -98,6 +98,7 @@ _TEST_FILES =\
test_events_draganddrop.html \ test_events_draganddrop.html \
test_events_focus.xul \ test_events_focus.xul \
test_events_mutation.html \ test_events_mutation.html \
test_events_mutation_coalesce.html \
test_events_tree.xul \ test_events_tree.xul \
test_events_valuechange.html \ test_events_valuechange.html \
test_groupattrs.xul \ test_groupattrs.xul \

View File

@ -1,8 +1,11 @@
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Constants // Constants
const EVENT_ASYNCH_HIDE = nsIAccessibleEvent.EVENT_ASYNCH_HIDE;
const EVENT_ASYNCH_SHOW = nsIAccessibleEvent.EVENT_ASYNCH_SHOW;
const EVENT_DOCUMENT_LOAD_COMPLETE = const EVENT_DOCUMENT_LOAD_COMPLETE =
nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE;
const EVENT_DOM_CREATE = nsIAccessibleEvent.EVENT_DOM_CREATE;
const EVENT_DOM_DESTROY = nsIAccessibleEvent.EVENT_DOM_DESTROY; const EVENT_DOM_DESTROY = nsIAccessibleEvent.EVENT_DOM_DESTROY;
const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS;
const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE;
@ -123,15 +126,10 @@ function invokerChecker(aEventType, aTarget)
* // (used in the case when invoker expects single event). * // (used in the case when invoker expects single event).
* DOMNode getter: function() {}, * DOMNode getter: function() {},
* *
* // Array of items defining events expected (or not expected, see * // Array of checker objects defining expected events on invoker's action.
* // 'doNotExpectEvents' property) on invoker's action.
* // * //
* // Every array item should be either * // Checker object interface:
* // 1) an array consisted from two elements, the first element is DOM or
* // a11y event type, second element is event target (DOM node or
* // accessible).
* // * //
* // 2) object (invoker's checker object) like
* // var checker = { * // var checker = {
* // type getter: function() {}, // DOM or a11y event type * // type getter: function() {}, // DOM or a11y event type
* // target getter: function() {}, // DOM node or accessible * // target getter: function() {}, // DOM node or accessible
@ -141,9 +139,9 @@ function invokerChecker(aEventType, aTarget)
* // }; * // };
* eventSeq getter() {}, * eventSeq getter() {},
* *
* // [optional, used together with 'eventSeq'] Boolean indicates if events * // Array of checker objects defining unexpected events on invoker's
* // specified by 'eventSeq' property shouldn't be triggerd by invoker. * // action.
* doNotExpectEvents getter() {}, * unexpectedEventSeq getter() {},
* *
* // The ID of invoker. * // The ID of invoker.
* getID: function(){} // returns invoker ID * getID: function(){} // returns invoker ID
@ -174,8 +172,7 @@ function eventQueue(aEventType)
// XXX: Intermittent test_events_caretmove.html fails withouth timeout, // XXX: Intermittent test_events_caretmove.html fails withouth timeout,
// see bug 474952. // see bug 474952.
window.setTimeout(function(aQueue) { aQueue.processNextInvoker(); }, 500, this.processNextInvokerInTimeout(true);
this);
} }
/** /**
@ -198,15 +195,20 @@ function eventQueue(aEventType)
var invoker = this.getInvoker(); var invoker = this.getInvoker();
if (invoker) { if (invoker) {
if ("finalCheck" in invoker)
invoker.finalCheck();
if (invoker.wasCaught) { if (invoker.wasCaught) {
for (var idx = 0; idx < invoker.wasCaught.length; idx++) { for (var idx = 0; idx < invoker.wasCaught.length; idx++) {
var id = this.getEventID(idx); var id = this.getEventID(idx);
var type = this.getEventType(idx); var type = this.getEventType(idx);
var unexpected = this.mEventSeq[idx].unexpected;
var typeStr = (typeof type == "string") ? var typeStr = (typeof type == "string") ?
type : gAccRetrieval.getStringEventType(type); type : gAccRetrieval.getStringEventType(type);
var msg = "test with ID = '" + id + "' failed. "; var msg = "test with ID = '" + id + "' failed. ";
if (invoker.doNotExpectEvents) { if (unexpected) {
var wasCaught = invoker.wasCaught[idx]; var wasCaught = invoker.wasCaught[idx];
if (!testFailed) if (!testFailed)
testFailed = wasCaught; testFailed = wasCaught;
@ -256,11 +258,22 @@ function eventQueue(aEventType)
return; return;
} }
if (invoker.doNotExpectEvents) { if (this.areAllEventsUnexpected())
// Check in timeout invoker didn't fire registered events. this.processNextInvokerInTimeout(true);
window.setTimeout(function(aQueue) { aQueue.processNextInvoker(); }, 500, }
this);
this.processNextInvokerInTimeout = function eventQueue_processNextInvokerInTimeout(aUncondProcess)
{
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(); }, 500,
this);
} }
/** /**
@ -282,33 +295,38 @@ function eventQueue(aEventType)
if ("debugCheck" in invoker) if ("debugCheck" in invoker)
invoker.debugCheck(aEvent); invoker.debugCheck(aEvent);
if (invoker.doNotExpectEvents) { // Search through unexpected events to ensure no one of them was handled.
// Search through event sequence to ensure it doesn't contain handled for (var idx = 0; idx < this.mEventSeq.length; idx++) {
// event. if (this.mEventSeq[idx].unexpected && this.compareEvents(idx, aEvent))
for (var idx = 0; idx < this.mEventSeq.length; idx++) {
if (this.compareEvents(idx, aEvent))
invoker.wasCaught[idx] = true;
}
} else {
// We wait for events in order specified by eventSeq variable.
var idx = this.mEventSeqIdx + 1;
var matched = this.compareEvents(idx, aEvent);
this.dumpEventToDOM(aEvent, idx, matched);
if (matched) {
this.checkEvent(idx, aEvent);
invoker.wasCaught[idx] = true; invoker.wasCaught[idx] = true;
}
if (idx == this.mEventSeq.length - 1) { // Wait for next expected event in an order specified by event sequence.
// We need delay to avoid events coalesce from different invokers.
var queue = this;
SimpleTest.executeSoon(function() { queue.processNextInvoker(); });
return;
}
this.mEventSeqIdx = idx; // Compute next expected event index.
for (var idx = this.mEventSeqIdx + 1;
idx < this.mEventSeq.length && this.mEventSeq[idx].unexpected; idx++);
if (idx == this.mEventSeq.length) {
// There is no expected events in the sequence.
this.processNextInvokerInTimeout();
return;
}
var matched = this.compareEvents(idx, aEvent);
this.dumpEventToDOM(aEvent, idx, matched);
if (matched) {
this.checkEvent(idx, aEvent);
invoker.wasCaught[idx] = true;
// The last event is expected and was handled, proceed next invoker.
if (idx == this.mEventSeq.length - 1) {
this.processNextInvokerInTimeout();
return;
} }
this.mEventSeqIdx = idx;
} }
} }
@ -325,12 +343,26 @@ function eventQueue(aEventType)
this.setEventHandler = function eventQueue_setEventHandler(aInvoker) this.setEventHandler = function eventQueue_setEventHandler(aInvoker)
{ {
// Create unique event sequence concatenating expected and unexpected
// events.
this.mEventSeq = ("eventSeq" in aInvoker) ? this.mEventSeq = ("eventSeq" in aInvoker) ?
aInvoker.eventSeq : aInvoker.eventSeq :
[ new invokerChecker(this.mDefEventType, aInvoker.DOMNode) ]; [ new invokerChecker(this.mDefEventType, aInvoker.DOMNode) ];
for (var idx = 0; idx < this.mEventSeq.length; idx++)
this.mEventSeq[idx].unexpected = false;
var unexpectedSeq = aInvoker.unexpectedEventSeq;
if (unexpectedSeq) {
for (var idx = 0; idx < unexpectedSeq.length; idx++)
unexpectedSeq[idx].unexpected = true;
this.mEventSeq = this.mEventSeq.concat(unexpectedSeq);
}
this.mEventSeqIdx = -1; this.mEventSeqIdx = -1;
// Register event listeners
if (this.mEventSeq) { if (this.mEventSeq) {
aInvoker.wasCaught = new Array(this.mEventSeq.length); aInvoker.wasCaught = new Array(this.mEventSeq.length);
@ -390,6 +422,16 @@ function eventQueue(aEventType)
return true; return true;
} }
this.getEventID = function eventQueue_getEventID(aIdx)
{
var eventItem = this.mEventSeq[aIdx];
if ("getID" in eventItem)
return eventItem.getID();
var invoker = this.getInvoker();
return invoker.getID();
}
this.compareEvents = function eventQueue_compareEvents(aIdx, aEvent) this.compareEvents = function eventQueue_compareEvents(aIdx, aEvent)
{ {
var eventType1 = this.getEventType(aIdx); var eventType1 = this.getEventType(aIdx);
@ -426,14 +468,24 @@ function eventQueue(aEventType)
invoker.check(aEvent); invoker.check(aEvent);
} }
this.getEventID = function eventQueue_getEventID(aIdx) this.areAllEventsExpected = function eventQueue_areAllEventsExpected()
{ {
var eventItem = this.mEventSeq[aIdx]; for (var idx = 0; idx < this.mEventSeq.length; idx++) {
if ("getID" in eventItem) if (this.mEventSeq[idx].unexpected)
return eventItem.getID(); return false;
}
var invoker = this.getInvoker(); return true;
return invoker.getID(); }
this.areAllEventsUnexpected = function eventQueue_areAllEventsUnxpected()
{
for (var idx = 0; idx < this.mEventSeq.length; idx++) {
if (!this.mEventSeq[idx].unexpected)
return false;
}
return true;
} }
this.dumpEventToDOM = function eventQueue_dumpEventToDOM(aOrigEvent, this.dumpEventToDOM = function eventQueue_dumpEventToDOM(aOrigEvent,

View File

@ -52,6 +52,7 @@
this.DOMNode = getNode(aNodeOrID); this.DOMNode = getNode(aNodeOrID);
this.doNotExpectEvents = aDoNotExpectEvents; this.doNotExpectEvents = aDoNotExpectEvents;
this.eventSeq = []; this.eventSeq = [];
this.unexpectedEventSeq = [];
/** /**
* Change default target (aNodeOrID) registered for the given event type. * Change default target (aNodeOrID) registered for the given event type.
@ -59,9 +60,9 @@
this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget) this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget)
{ {
var type = this.getA11yEventType(aEventType); var type = this.getA11yEventType(aEventType);
for (var idx = 0; idx < this.eventSeq.length; idx++) { for (var idx = 0; idx < this.getEventSeq().length; idx++) {
if (this.eventSeq[idx].type == type) { if (this.getEventSeq()[idx].type == type) {
this.eventSeq[idx].target = aTarget; this.getEventSeq()[idx].target = aTarget;
return idx; return idx;
} }
} }
@ -78,7 +79,7 @@
var type = this.getA11yEventType(aEventType); var type = this.getA11yEventType(aEventType);
for (var i = 1; i < aTargets.length; i++) { for (var i = 1; i < aTargets.length; i++) {
var checker = new invokerChecker(type, aTargets[i]); var checker = new invokerChecker(type, aTargets[i]);
this.eventSeq.splice(++targetIdx, 0, checker); this.getEventSeq().splice(++targetIdx, 0, checker);
} }
} }
@ -103,24 +104,29 @@
} }
} }
this.getEventSeq = function mutateA11yTree_getEventSeq()
{
return this.doNotExpectEvents ? this.unexpectedEventSeq : this.eventSeq;
}
this.mIsDOMChange = aIsDOMChange; this.mIsDOMChange = aIsDOMChange;
if (aEventTypes & kHideEvent) { if (aEventTypes & kHideEvent) {
var checker = new invokerChecker(this.getA11yEventType(kHideEvent), var checker = new invokerChecker(this.getA11yEventType(kHideEvent),
this.DOMNode); this.DOMNode);
this.eventSeq.push(checker); this.getEventSeq().push(checker);
} }
if (aEventTypes & kShowEvent) { if (aEventTypes & kShowEvent) {
var checker = new invokerChecker(this.getA11yEventType(kShowEvent), var checker = new invokerChecker(this.getA11yEventType(kShowEvent),
this.DOMNode); this.DOMNode);
this.eventSeq.push(checker); this.getEventSeq().push(checker);
} }
if (aEventTypes & kReorderEvent) { if (aEventTypes & kReorderEvent) {
var checker = new invokerChecker(this.getA11yEventType(kReorderEvent), var checker = new invokerChecker(this.getA11yEventType(kReorderEvent),
this.DOMNode.parentNode); this.DOMNode.parentNode);
this.eventSeq.push(checker); this.getEventSeq().push(checker);
} }
} }

View File

@ -0,0 +1,409 @@
<html>
<head>
<title>Accessible mutation events coalescence testing</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script type="application/javascript"
src="chrome://mochikit/content/MochiKit/packed.js"></script>
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="application/javascript"
src="chrome://mochikit/content/a11y/accessible/common.js"></script>
<script type="application/javascript"
src="chrome://mochikit/content/a11y/accessible/events.js"></script>
<script type="application/javascript">
////////////////////////////////////////////////////////////////////////////
// Invoker base classes
const kRemoveElm = 1;
const kHideElm = 2;
const kAddElm = 3;
const kShowElm = 4;
/**
* Base class to test of mutation events coalescence.
*/
function coalescenceBase(aChildAction, aParentAction,
aPerformActionOnChildInTheFirstPlace,
aIsChildsToDo)
{
// Invoker interface
this.invoke = function coalescenceBase_invoke()
{
if (aPerformActionOnChildInTheFirstPlace) {
this.invokeAction(this.childNode, aChildAction);
this.invokeAction(this.parentNode, aParentAction);
} else {
this.invokeAction(this.parentNode, aParentAction);
this.invokeAction(this.childNode, aChildAction);
}
}
this.getID = function coalescenceBase_getID()
{
var childAction = this.getActionName(aChildAction) + " child";
var parentAction = this.getActionName(aParentAction) + " parent";
if (aPerformActionOnChildInTheFirstPlace)
return childAction + " and then " + parentAction;
return parentAction + " and then " + childAction;
}
this.finalCheck = function coalescenceBase_check()
{
if (!aIsChildsToDo)
return;
todo(false,
"Unexpected event " + this.getEventType(aChildAction) +
" for child in the test '" + this.getID() + "'");
}
// Implementation details
this.invokeAction = function coalescenceBase_invokeAction(aNode, aAction)
{
switch (aAction) {
case kRemoveElm:
aNode.parentNode.removeChild(aNode);
break;
case kHideElm:
aNode.style.display = "none";
break;
case kAddElm:
if (aNode == this.parentNode)
this.hostNode.appendChild(this.parentNode);
else
this.parentNode.appendChild(this.childNode);
break;
case kShowElm:
aNode.style.display = "block";
default:
return INVOKER_ACTION_FAILED;
}
}
this.getEventType = function coalescenceBase_getEventType(aAction)
{
switch (aAction) {
case kRemoveElm:
return EVENT_DOM_DESTROY;
case kHideElm:
return EVENT_ASYNCH_HIDE;
case kAddElm:
return EVENT_DOM_CREATE;
case kShowElm:
return EVENT_ASYNCH_SHOW;
}
}
this.getActionName = function coalescenceBase_getActionName(aAction)
{
switch (aAction) {
case kRemoveElm:
return "remove";
case kHideElm:
return "hide";
case kAddElm:
return "add";
case kShowElm:
return "show";
default:
return "??";
}
}
this.initSequence = function coalescenceBase_initSequence()
{
// expected events
var eventType = this.getEventType(aParentAction);
this.eventSeq = [
new invokerChecker(eventType, this.parentNode),
new invokerChecker(EVENT_REORDER, this.hostNode)
];
// unexpected events
this.unexpectedEventSeq = [
new invokerChecker(EVENT_REORDER, this.parentNode)
];
if (!aIsChildsToDo) {
var eventType = this.getEventType(aChildAction);
var checker = new invokerChecker(eventType, this.childNode);
this.unexpectedEventSeq.unshift(checker);
}
}
}
/**
* Remove or hide mutation events coalescence testing.
*/
function removeOrHidecoalescenceBase(aChildID, aParentID,
aChildAction, aParentAction,
aPerformActionOnChildInTheFirstPlace,
aIsChildsToDo)
{
this.__proto__ = new coalescenceBase(aChildAction, aParentAction,
aPerformActionOnChildInTheFirstPlace,
aIsChildsToDo);
this.init = function removeOrHidecoalescenceBase_init()
{
this.childNode = getNode(aChildID);
this.parentNode = getNode(aParentID);
this.hostNode = this.parentNode.parentNode;
// ensure child accessible is created
getAccessible(this.childNode);
}
// Initalization
this.init();
this.initSequence();
}
////////////////////////////////////////////////////////////////////////////
// Invokers
/**
* Remove child node and then its parent node from DOM tree.
*/
function removeChildNParent(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kRemoveElm, kRemoveElm,
true, true);
}
/**
* Remove parent node and then its child node from DOM tree.
*/
function removeParentNChild(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kRemoveElm, kRemoveElm,
false);
}
/**
* Hide child node and then its parent node.
*/
function hideChildNParent(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kHideElm, kHideElm,
true);
}
/**
* Hide parent node and then its child node.
*/
function hideParentNChild(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kHideElm, kHideElm,
false);
}
/**
* Hide child node and then remove its parent node.
*/
function hideChildNRemoveParent(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kHideElm, kRemoveElm,
true);
}
/**
* Hide parent node and then remove its child node.
*/
function hideParentNRemoveChild(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kRemoveElm, kHideElm,
false, true);
}
/**
* Remove child node and then hide its parent node.
*/
function removeChildNHideParent(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kRemoveElm, kHideElm,
true, true);
}
/**
* Remove parent node and then hide its child node.
*/
function removeParentNHideChild(aChildID, aParentID)
{
this.__proto__ = new removeOrHidecoalescenceBase(aChildID, aParentID,
kHideElm, kRemoveElm,
false);
}
/**
* Create and append parent node and create and append child node to it.
*/
function addParentNChild(aHostID, aPerformActionOnChildInTheFirstPlace)
{
this.init = function addParentNChild_init()
{
this.hostNode = getNode(aHostID);
this.parentNode = document.createElement("select");
this.childNode = document.createElement("option");
this.childNode.textContent = "testing";
}
this.__proto__ = new coalescenceBase(kAddElm, kAddElm,
aPerformActionOnChildInTheFirstPlace);
this.init();
this.initSequence();
}
/**
* Show parent node and show child node to it.
*/
function showParentNChild(aParentID, aChildID,
aPerformActionOnChildInTheFirstPlace)
{
this.init = function showParentNChild_init()
{
this.parentNode = getNode(aParentID);
this.hostNode = this.parentNode.parentNode;
this.childNode = getNode(aChildID);
}
this.__proto__ = new coalescenceBase(kShowElm, kShowElm,
aPerformActionOnChildInTheFirstPlace);
this.init();
this.initSequence();
}
/**
* Create and append child node to the DOM and then show parent node.
*/
function showParentNAddChild(aParentID,
aPerformActionOnChildInTheFirstPlace)
{
this.init = function showParentNAddChild_init()
{
this.parentNode = getNode(aParentID);
this.hostNode = this.parentNode.parentNode;
this.childNode = document.createElement("option");
this.childNode.textContent = "testing";
}
this.__proto__ = new coalescenceBase(kAddElm, kShowElm,
aPerformActionOnChildInTheFirstPlace,
true);
this.init();
this.initSequence();
}
////////////////////////////////////////////////////////////////////////////
// Do tests.
var gQueue = null;
// var gA11yEventDumpID = "eventdump"; // debug stuff
function doTests()
{
gQueue = new eventQueue();
gQueue.push(new removeChildNParent("option1", "select1"));
gQueue.push(new removeParentNChild("option2", "select2"));
gQueue.push(new hideChildNParent("option3", "select3"));
gQueue.push(new hideParentNChild("option4", "select4"));
gQueue.push(new hideChildNRemoveParent("option5", "select5"));
gQueue.push(new hideParentNRemoveChild("option6", "select6"));
gQueue.push(new removeChildNHideParent("option7", "select7"));
gQueue.push(new removeParentNHideChild("option8", "select8"));
gQueue.push(new addParentNChild("testContainer", false));
gQueue.push(new addParentNChild("testContainer", true));
gQueue.push(new showParentNChild("select9", "option9", false));
gQueue.push(new showParentNChild("select10", "option10", true));
gQueue.push(new showParentNAddChild("select11", false));
gQueue.push(new showParentNAddChild("select12", true));
gQueue.invoke(); // Will call SimpleTest.finish();
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTests);
</script>
</head>
<body>
<a target="_blank"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=513213"
title="coalesce events when new event is appended to the queue">
Mozilla Bug 513213
</a>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
<div id="eventdump"></div>
<div id="testContainer">
<select id="select1">
<option id="option1">option</option>
</select>
<select id="select2">
<option id="option2">option</option>
</select>
<select id="select3">
<option id="option3">option</option>
</select>
<select id="select4">
<option id="option4">option</option>
</select>
<select id="select5">
<option id="option5">option</option>
</select>
<select id="select6">
<option id="option6">option</option>
</select>
<select id="select7">
<option id="option7">option</option>
</select>
<select id="select8">
<option id="option8">option</option>
</select>
<select id="select9" style="display: none">
<option id="option9" style="display: none">testing</option>
</select>
<select id="select10" style="display: none">
<option id="option10" style="display: none">testing</option>
</select>
<select id="select11" style="display: none"></select>
<select id="select12" style="display: none"></select>
</div>
</body>
</html>