Bug 810268 - there's no way to know unselected item when selection in single selection was changed, r=tbsaunde
authorAlexander Surkov <surkov.alexander@gmail.com>
Sun, 28 Jul 2013 14:33:57 -0400
changeset 140284 12eedfb87ed6213320c33374bf32c68ab78a42a3
parent 140283 980551965c2a463ad8760dcc00786979e91b3d5e
child 140285 c28a758324262fcda6df5b459ef50045803f203f
push id1954
push useremorley@mozilla.com
push dateMon, 29 Jul 2013 14:43:15 +0000
treeherderfx-team@b8c7acba4b40 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstbsaunde
bugs810268
milestone25.0a1
Bug 810268 - there's no way to know unselected item when selection in single selection was changed, r=tbsaunde
accessible/src/base/EventQueue.cpp
accessible/src/base/nsEventShell.h
accessible/tests/mochitest/events.js
accessible/tests/mochitest/events/test_selection.html
accessible/tests/mochitest/events/test_selection_aria.html
--- a/accessible/src/base/EventQueue.cpp
+++ b/accessible/src/base/EventQueue.cpp
@@ -298,17 +298,17 @@ EventQueue::CoalesceSelChangeEvents(AccS
       aTailEvent->mPackedEvent = aThisEvent;
       return;
     }
 
     if (aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd &&
         aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) {
       aTailEvent->mEventRule = AccEvent::eDoNotEmit;
       aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION;
-      aThisEvent->mPackedEvent = aThisEvent;
+      aThisEvent->mPackedEvent = aTailEvent;
       return;
     }
   }
 
   // Unpack the packed selection change event because we've got one
   // more selection add/remove.
   if (aThisEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) {
     if (aThisEvent->mPackedEvent) {
@@ -467,16 +467,39 @@ EventQueue::ProcessEventQueue()
           hyperText->GetSelectionCount(&selectionCount);
           if (selectionCount)
             nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED,
                                     hyperText);
         }
         continue;
       }
 
+      // Fire selected state change events in support to selection events.
+      if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) {
+        nsEventShell::FireEvent(event->mAccessible, states::SELECTED,
+                                true, event->mIsFromUserInput);
+
+      } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) {
+        nsEventShell::FireEvent(event->mAccessible, states::SELECTED,
+                                false, event->mIsFromUserInput);
+
+      } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION) {
+        AccSelChangeEvent* selChangeEvent = downcast_accEvent(event);
+        nsEventShell::FireEvent(event->mAccessible, states::SELECTED,
+                                (selChangeEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd),
+                                event->mIsFromUserInput);
+
+        if (selChangeEvent->mPackedEvent) {
+          nsEventShell::FireEvent(selChangeEvent->mPackedEvent->mAccessible,
+                                  states::SELECTED,
+                                  (selChangeEvent->mPackedEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd),
+                                  selChangeEvent->mPackedEvent->mIsFromUserInput);
+        }
+      }
+
       nsEventShell::FireEvent(event);
 
       // Fire text change events.
       AccMutationEvent* mutationEvent = downcast_accEvent(event);
       if (mutationEvent) {
         if (mutationEvent->mTextChangeEvent)
           nsEventShell::FireEvent(mutationEvent->mTextChangeEvent);
       }
--- a/accessible/src/base/nsEventShell.h
+++ b/accessible/src/base/nsEventShell.h
@@ -31,16 +31,30 @@ public:
    * @param  aEventType   [in] the event type
    * @param  aAccessible  [in] the event target
    */
   static void FireEvent(uint32_t aEventType,
                         mozilla::a11y::Accessible* aAccessible,
                         mozilla::a11y::EIsFromUserInput aIsFromUserInput = mozilla::a11y::eAutoDetect);
 
   /**
+   * Fire state change event.
+   */
+  static void FireEvent(mozilla::a11y::Accessible* aTarget, uint64_t aState,
+                        bool aIsEnabled, bool aIsFromUserInput)
+  {
+    nsRefPtr<mozilla::a11y::AccStateChangeEvent> stateChangeEvent =
+      new mozilla::a11y::AccStateChangeEvent(aTarget, aState, aIsEnabled,
+                                             (aIsFromUserInput ?
+                                               mozilla::a11y::eFromUserInput :
+                                               mozilla::a11y::eNoUserInput));
+    FireEvent(stateChangeEvent);
+  }
+
+  /**
    * Append 'event-from-input' object attribute if the accessible event has
    * been fired just now for the given node.
    *
    * @param  aNode        [in] the DOM node
    * @param  aAttributes  [in, out] the attributes
    */
   static void GetEventAttributes(nsINode *aNode,
                                  nsIPersistentProperties *aAttributes);
--- a/accessible/tests/mochitest/events.js
+++ b/accessible/tests/mochitest/events.js
@@ -503,69 +503,74 @@ function eventQueue(aEventType)
             ok(false,
                "Unique type " +
                eventQueue.getEventTypeAsString(checker) + " event was handled.");
           }
         }
       }
     }
 
-    var matchedChecker = null;
+    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)) {
-          matchedChecker = nextChecker;
-          matchedChecker.wasCaught++;
-          break;
+          this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx);
+          hasMatchedCheckers = true;
+          continue;
         }
       }
 
       // Check if handled event matches any expected async events.
       for (idx = 0; idx < eventSeq.length; idx++) {
         if (!eventSeq[idx].unexpected && eventSeq[idx].async) {
           if (eventQueue.compareEvents(eventSeq[idx], aEvent)) {
-            matchedChecker = eventSeq[idx];
-            matchedChecker.wasCaught++;
+            this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx);
+            hasMatchedCheckers = true;
             break;
           }
         }
       }
     }
 
-    // Call 'check' functions on invoker's side.
-    if (matchedChecker) {
-      if ("check" in matchedChecker)
-        matchedChecker.check(aEvent);
-
+    if (hasMatchedCheckers) {
       var invoker = this.getInvoker();
       if ("check" in invoker)
         invoker.check(aEvent);
     }
 
-    // Dump handled event.
-    eventQueue.logEvent(aEvent, matchedChecker, this.areExpectedEventsLeft(),
-                        this.mNextInvokerStatus);
-
     // If we don't have more events to wait then schedule next invoker.
-    if (!this.areExpectedEventsLeft() &&
+    if (this.hasMatchedScenario() &&
         (this.mNextInvokerStatus == kInvokerNotScheduled)) {
       this.processNextInvokerInTimeout();
       return;
     }
 
     // If we have scheduled a next invoker then cancel in case of match.
-    if ((this.mNextInvokerStatus == kInvokerPending) && matchedChecker)
+    if ((this.mNextInvokerStatus == kInvokerPending) && hasMatchedCheckers)
       this.mNextInvokerStatus = kInvokerCanceled;
   }
 
   // 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 ||
@@ -630,16 +635,26 @@ function eventQueue(aEventType)
           break;
       }
       if (idx == eventSeq.length)
         return true;
     }
 
     return false;
   }
+  
+  this.hasMatchedScenario =
+    function eventQueue_hasMatchedScenario()
+  {
+    for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) {
+      if (!this.areExpectedEventsLeft(this.mScenarios[scnIdx]))
+        return true;
+    }
+    return false;
+  }
 
   this.getInvoker = function eventQueue_getInvoker()
   {
     return this.mInvokers[this.mIndex];
   }
 
   this.getNextInvoker = function eventQueue_getNextInvoker()
   {
@@ -853,16 +868,17 @@ eventQueue.isSameEvent = function eventQ
   // 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.logEvent = function eventQueue_logEvent(aOrigEvent, aMatchedChecker,
+                                                   aScenarioIdx, aEventIdx,
                                                    aAreExpectedEventsLeft,
                                                    aInvokerStatus)
 {
   if (!gLogger.isEnabled()) // debug stuff
     return;
 
   // Dump DOM event information. Skip a11y event since it is dumped by
   // gA11yEventObserver.
@@ -892,17 +908,18 @@ eventQueue.logEvent = function eventQueu
   if (!aMatchedChecker)
     return;
 
   var msg = "EQ: ";
   var emphText = "matched ";
 
   var currType = eventQueue.getEventTypeAsString(aMatchedChecker);
   var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker);
-  var consoleMsg = "*****\nEQ matched: " + currType + "\n*****";
+  var consoleMsg = "*****\nScenario " + aScenarioIdx + 
+    ", event " + aEventIdx + " matched: " + currType + "\n*****";
   gLogger.logToConsole(consoleMsg);
 
   msg += " event, type: " + currType + ", target: " + currTargetDescr;
 
   gLogger.logToDOM(msg, true, emphText);
 }
 
 
@@ -1722,17 +1739,18 @@ function stateChangeChecker(aState, aIsE
     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 = this.target && scEvent.state == aState;
+      return (aEvent.accessible == getAccessible(this.target)) &&
+        (scEvent.state == aState);
     }
     return false;
   }
 }
 
 function asyncStateChangeChecker(aState, aIsExtraState, aIsEnabled,
                                  aTargetOrFunc, aTargetFuncArg)
 {
@@ -1767,16 +1785,69 @@ function expandedStateChecker(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 = {};
@@ -2038,23 +2109,30 @@ function sequenceItem(aProcessor, aEvent
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Event queue invokers
 
 /**
  * Invoker base class for prepare an action.
  */
-function synthAction(aNodeOrID, aCheckerOrEventSeq)
+function synthAction(aNodeOrID, aEventsObj)
 {
   this.DOMNode = getNode(aNodeOrID);
 
-  if (aCheckerOrEventSeq) {
-    if (aCheckerOrEventSeq instanceof Array) {
-      this.eventSeq = aCheckerOrEventSeq;
+  if (aEventsObj) {
+    var scenarios = null;
+    if (aEventsObj instanceof Array) {
+      if (aEventsObj[0] instanceof Array)
+        scenarios = aEventsObj; // scenarios
+      else
+        scenarios = [ aEventsObj ]; // event sequance
     } else {
-      this.eventSeq = [ aCheckerOrEventSeq ];
+      scenarios = [ [ aEventsObj ] ]; // a single checker object
     }
+
+    for (var i = 0; i < scenarios.length; i++)
+      defineScenario(this, scenarios[i]);
   }
 
   this.getID = function synthAction_getID()
     { return prettyName(aNodeOrID) + " action"; }
 }
--- a/accessible/tests/mochitest/events/test_selection.html
+++ b/accessible/tests/mochitest/events/test_selection.html
@@ -33,56 +33,61 @@
     function doTests()
     {
       gQueue = new eventQueue();
 
       // open combobox
       gQueue.push(new synthClick("combobox",
                                  new invokerChecker(EVENT_FOCUS, "cb1_item1")));
       gQueue.push(new synthDownKey("cb1_item1",
-                                   new invokerChecker(EVENT_SELECTION, "cb1_item2")));
+                                   selChangeSeq("cb1_item1", "cb1_item2")));
 
       // closed combobox
       gQueue.push(new synthEscapeKey("combobox",
                                      new invokerChecker(EVENT_FOCUS, "combobox")));
       gQueue.push(new synthDownKey("cb1_item2",
-                                   new invokerChecker(EVENT_SELECTION, "cb1_item3")));
+                                   selChangeSeq("cb1_item2", "cb1_item3")));
 
       // listbox
       gQueue.push(new synthClick("lb1_item1",
                                  new invokerChecker(EVENT_SELECTION, "lb1_item1")));
       gQueue.push(new synthDownKey("lb1_item1",
-                                   new invokerChecker(EVENT_SELECTION, "lb1_item2")));
+                                   selChangeSeq("lb1_item1", "lb1_item2")));
 
       // multiselectable listbox
       gQueue.push(new synthClick("lb2_item1",
-                                 new invokerChecker(EVENT_SELECTION, "lb2_item1")));
+                                 selChangeSeq(null, "lb2_item1")));
       gQueue.push(new synthDownKey("lb2_item1",
-                                   new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"),
+                                   selAddSeq("lb2_item2"),
                                    { shiftKey: true }));
       gQueue.push(new synthUpKey("lb2_item2",
-                                 new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"),
+                                 selRemoveSeq("lb2_item2"),
                                  { shiftKey: true }));
       gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true },
-                               new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1")));
+                               selRemoveSeq("lb2_item1")));
 
       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=414302"
      title="Incorrect selection events in HTML, XUL and ARIA">
-    Mozilla Bug 414302
+    Bug 414302
+  </a>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=810268"
+     title="There's no way to know unselected item when selection in single selection was changed">
+    Bug 810268
   </a>
 
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <select id="combobox">
--- a/accessible/tests/mochitest/events/test_selection_aria.html
+++ b/accessible/tests/mochitest/events/test_selection_aria.html
@@ -31,17 +31,17 @@
 
       this.eventSeq = [
         new invokerChecker(EVENT_SELECTION, aItemID)
       ];
 
       this.invoke = function selectItem_invoke() {
         var itemNode = this.selectNode.querySelector("*[aria-selected='true']");
         if (itemNode)
-          itemNode.removeAttribute("aria-selected", "true");
+          itemNode.removeAttribute("aria-selected");
 
         this.itemNode.setAttribute("aria-selected", "true");
       }
 
       this.getID = function selectItem_getID()
       {
         return "select item " + prettyName(aItemID);
       }