Bug 467143 - mixed state change event is fired for focused accessible only, r=tbsaunde
authorAlexander Surkov <surkov.alexander@gmail.com>
Thu, 11 Aug 2011 20:45:36 +0900
changeset 74262 c25eeb46c2a4eb7824316ca5d398580d4722b01e
parent 74261 73351129648ba23724ec373f010d5c5ccf8ea335
child 74263 6a43079a1c0f17fbad026311cccf25caaa1455c5
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewerstbsaunde
bugs467143
milestone8.0a1
Bug 467143 - mixed state change event is fired for focused accessible only, r=tbsaunde
accessible/src/base/AccEvent.cpp
accessible/src/base/nsAccUtils.cpp
accessible/src/base/nsAccUtils.h
accessible/src/base/nsAccessibilityAtomList.h
accessible/src/base/nsDocAccessible.cpp
accessible/src/base/nsDocAccessible.h
accessible/src/base/nsRootAccessible.cpp
accessible/tests/mochitest/events.js
accessible/tests/mochitest/events/test_aria_statechange.html
accessible/tests/mochitest/states.js
--- a/accessible/src/base/AccEvent.cpp
+++ b/accessible/src/base/AccEvent.cpp
@@ -226,24 +226,27 @@ AccStateChangeEvent::
   AccEvent(nsIAccessibleEvent::EVENT_STATE_CHANGE, aAccessible,
            aIsFromUserInput, eAllowDupes),
   mState(aState), mIsEnabled(aIsEnabled)
 {
 }
 
 AccStateChangeEvent::
   AccStateChangeEvent(nsINode* aNode, PRUint64 aState, PRBool aIsEnabled):
-  AccEvent(::nsIAccessibleEvent::EVENT_STATE_CHANGE, aNode),
+  AccEvent(::nsIAccessibleEvent::EVENT_STATE_CHANGE, aNode,
+           eAutoDetect, eAllowDupes),
   mState(aState), mIsEnabled(aIsEnabled)
 {
 }
 
 AccStateChangeEvent::
   AccStateChangeEvent(nsINode* aNode, PRUint64 aState) :
-  AccEvent(::nsIAccessibleEvent::EVENT_STATE_CHANGE, aNode), mState(aState)
+  AccEvent(::nsIAccessibleEvent::EVENT_STATE_CHANGE, aNode,
+           eAutoDetect, eAllowDupes),
+  mState(aState)
 {
   // Use GetAccessibleForNode() because we do not want to store an accessible
   // since it leads to problems with delayed events in the case when
   // an accessible gets reorder event before delayed event is processed.
   nsAccessible *accessible = GetAccessibleForNode();
   mIsEnabled = accessible && ((accessible->State() & mState) != 0);
 }
 
--- a/accessible/src/base/nsAccUtils.cpp
+++ b/accessible/src/base/nsAccUtils.cpp
@@ -52,16 +52,17 @@
 #include "nsXULTreeGridAccessible.h"
 
 #include "nsIDOMXULContainerElement.h"
 #include "nsIDOMXULSelectCntrlEl.h"
 #include "nsIDOMXULSelectCntrlItemEl.h"
 #include "nsWhitespaceTokenizer.h"
 #include "nsComponentManagerUtils.h"
 
+namespace dom = mozilla::dom;
 using namespace mozilla::a11y;
 
 void
 nsAccUtils::GetAccAttr(nsIPersistentProperties *aAttributes,
                        nsIAtom *aAttrName, nsAString& aAttrValue)
 {
   aAttrValue.Truncate();
 
@@ -330,16 +331,34 @@ nsAccUtils::HasDefinedARIAToken(nsIConte
                             nsAccessibilityAtoms::_empty, eCaseMatters) ||
       aContent->AttrValueIs(kNameSpaceID_None, aAtom,
                             nsAccessibilityAtoms::_undefined, eCaseMatters)) {
         return PR_FALSE;
   }
   return PR_TRUE;
 }
 
+nsIAtom*
+nsAccUtils::GetARIAToken(dom::Element* aElement, nsIAtom* aAttr)
+{
+  if (!nsAccUtils::HasDefinedARIAToken(aElement, aAttr))
+    return nsAccessibilityAtoms::_empty;
+
+  static nsIContent::AttrValuesArray tokens[] =
+    { &nsAccessibilityAtoms::_false, &nsAccessibilityAtoms::_true,
+      &nsAccessibilityAtoms::mixed, nsnull};
+
+  PRInt32 idx = aElement->FindAttrValueIn(kNameSpaceID_None,
+                                          aAttr, tokens, eCaseMatters);
+  if (idx >= 0)
+    return *(tokens[idx]);
+
+  return nsnull;
+}
+
 nsAccessible *
 nsAccUtils::GetAncestorWithRole(nsAccessible *aDescendant, PRUint32 aRole)
 {
   nsAccessible *document = aDescendant->GetDocAccessible();
   nsAccessible *parent = aDescendant;
   while ((parent = parent->Parent())) {
     PRUint32 testRole = parent->Role();
     if (testRole == aRole)
--- a/accessible/src/base/nsAccUtils.h
+++ b/accessible/src/base/nsAccUtils.h
@@ -45,17 +45,17 @@
 #include "nsIAccessibleRole.h"
 #include "nsIAccessibleText.h"
 #include "nsIAccessibleTable.h"
 
 #include "nsARIAMap.h"
 #include "nsAccessibilityService.h"
 #include "nsCoreUtils.h"
 
-#include "nsIContent.h"
+#include "mozilla/dom/Element.h"
 #include "nsIDocShell.h"
 #include "nsIDOMNode.h"
 #include "nsIPersistentProperties2.h"
 #include "nsIPresShell.h"
 #include "nsPoint.h"
 
 class nsAccessNode;
 class nsAccessible;
@@ -146,16 +146,21 @@ public:
    * property is not present, or is "" or "undefined". Do not call 
    * this method for properties of type string, decimal, IDREF or IDREFS.
    * 
    * Return PR_TRUE if the ARIA property is defined, otherwise PR_FALSE
    */
   static PRBool HasDefinedARIAToken(nsIContent *aContent, nsIAtom *aAtom);
 
   /**
+   * Return atomic value of ARIA attribute of boolean or NMTOKEN type.
+   */
+  static nsIAtom* GetARIAToken(mozilla::dom::Element* aElement, nsIAtom* aAttr);
+
+  /**
    * Return document accessible for the given presshell.
    */
   static nsDocAccessible *GetDocAccessibleFor(nsIWeakReference *aWeakShell)
   {
     nsCOMPtr<nsIPresShell> presShell(do_QueryReferent(aWeakShell));
     return presShell ?
       GetAccService()->GetDocAccessible(presShell->GetDocument()) : nsnull;
   }
--- a/accessible/src/base/nsAccessibilityAtomList.h
+++ b/accessible/src/base/nsAccessibilityAtomList.h
@@ -60,16 +60,17 @@ ACCESSIBILITY_ATOM(button, "button")
 ACCESSIBILITY_ATOM(checkbox, "checkbox")
 ACCESSIBILITY_ATOM(col, "col")
 ACCESSIBILITY_ATOM(_empty, "")
 ACCESSIBILITY_ATOM(_false, "false")
 ACCESSIBILITY_ATOM(image, "image")
 ACCESSIBILITY_ATOM(menu, "menu")
 ACCESSIBILITY_ATOM(menuButton, "menu-button")
 ACCESSIBILITY_ATOM(multiple, "multiple")
+ACCESSIBILITY_ATOM(mixed, "mixed")
 ACCESSIBILITY_ATOM(open, "open")
 ACCESSIBILITY_ATOM(password, "password")
 ACCESSIBILITY_ATOM(radio, "radio")
 ACCESSIBILITY_ATOM(reset, "reset")
 ACCESSIBILITY_ATOM(row, "row")
 ACCESSIBILITY_ATOM(submit, "submit")
 ACCESSIBILITY_ATOM(_true, "true")
 ACCESSIBILITY_ATOM(_undefined, "undefined")
--- a/accessible/src/base/nsDocAccessible.cpp
+++ b/accessible/src/base/nsDocAccessible.cpp
@@ -79,18 +79,16 @@
 #endif
 
 namespace dom = mozilla::dom;
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
 // Static member initialization
 
-PRUint64 nsDocAccessible::gLastFocusedAccessiblesState = 0;
-
 static nsIAtom** kRelationAttrs[] =
 {
   &nsAccessibilityAtoms::aria_labelledby,
   &nsAccessibilityAtoms::aria_describedby,
   &nsAccessibilityAtoms::aria_owns,
   &nsAccessibilityAtoms::aria_controls,
   &nsAccessibilityAtoms::aria_flowto,
   &nsAccessibilityAtoms::_for,
@@ -913,31 +911,43 @@ NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(ns
 NS_IMPL_NSIDOCUMENTOBSERVER_STYLE_STUB(nsDocAccessible)
 
 void
 nsDocAccessible::AttributeWillChange(nsIDocument *aDocument,
                                      dom::Element* aElement,
                                      PRInt32 aNameSpaceID,
                                      nsIAtom* aAttribute, PRInt32 aModType)
 {
-  // XXX TODO: bugs 467143, 472142, 472143.
-  // Here we will want to cache whatever state we are potentially interested in,
-  // such as the existence of aria-pressed for button (so we know if we need to
-  // newly expose it as a toggle button) etc.
+  nsAccessible* accessible = GetAccessible(aElement);
+  if (!accessible) {
+    if (aElement != mContent)
+      return;
+
+    accessible = this;
+  }
 
   // Update dependent IDs cache. Take care of elements that are accessible
   // because dependent IDs cache doesn't contain IDs from non accessible
   // elements.
-  if (aModType == nsIDOMMutationEvent::MODIFICATION ||
-      aModType == nsIDOMMutationEvent::REMOVAL) {
-    nsAccessible* accessible = GetAccessible(aElement);
-    if (accessible)
-      RemoveDependentIDsFor(accessible, aAttribute);
-    else if (aElement == mContent)
-      RemoveDependentIDsFor(this, aAttribute);
+  if (aModType != nsIDOMMutationEvent::ADDITION)
+    RemoveDependentIDsFor(accessible, aAttribute);
+
+  // Store the ARIA attribute old value so that it can be used after
+  // attribute change. Note, we assume there's no nested ARIA attribute
+  // changes. If this happens then we should end up with keeping a stack of
+  // old values.
+
+  // XXX TODO: bugs 472142, 472143.
+  // Here we will want to cache whatever attribute values we are interested
+  // in, such as the existence of aria-pressed for button (so we know if we
+  // need to newly expose it as a toggle button) etc.
+  if (aAttribute == nsAccessibilityAtoms::aria_checked ||
+      aAttribute == nsAccessibilityAtoms::aria_pressed) {
+    mARIAAttrOldValue = (aModType != nsIDOMMutationEvent::ADDITION) ?
+      nsAccUtils::GetARIAToken(aElement, aAttribute) : nsnull;
   }
 }
 
 void
 nsDocAccessible::AttributeChanged(nsIDocument *aDocument,
                                   dom::Element* aElement,
                                   PRInt32 aNameSpaceID, nsIAtom* aAttribute,
                                   PRInt32 aModType)
@@ -971,20 +981,16 @@ nsDocAccessible::AttributeChanged(nsIDoc
   // accessible element means either the element is not accessible at all or
   // its accessible will be created later. It doesn't make sense to keep
   // dependent IDs for non accessible elements. For the second case we'll update
   // dependent IDs cache when its accessible is created.
   if (aModType == nsIDOMMutationEvent::MODIFICATION ||
       aModType == nsIDOMMutationEvent::ADDITION) {
     AddDependentIDsFor(accessible, aAttribute);
   }
-
-  // If it was the focused node, cache the new state.
-  if (aElement == gLastFocusedNode)
-    gLastFocusedAccessiblesState = accessible->State();
 }
 
 // nsDocAccessible protected member
 void
 nsDocAccessible::AttributeChangedImpl(nsIContent* aContent, PRInt32 aNameSpaceID, nsIAtom* aAttribute)
 {
   // Fire accessible event after short timer, because we need to wait for
   // DOM attribute & resulting layout to actually change. Otherwise,
@@ -1040,18 +1046,18 @@ nsDocAccessible::AttributeChangedImpl(ns
       aAttribute == nsAccessibilityAtoms::aria_label ||
       aAttribute == nsAccessibilityAtoms::aria_labelledby) {
     FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE,
                                aContent);
     return;
   }
 
   if (aAttribute == nsAccessibilityAtoms::aria_busy) {
-    PRBool isOn = !aContent->AttrValueIs(aNameSpaceID, aAttribute,
-                                         nsAccessibilityAtoms::_true, eCaseMatters);
+    PRBool isOn = aContent->AttrValueIs(aNameSpaceID, aAttribute,
+                                        nsAccessibilityAtoms::_true, eCaseMatters);
     nsRefPtr<AccEvent> event = new AccStateChangeEvent(aContent, states::BUSY, isOn);
     FireDelayedAccessibleEvent(event);
     return;
   }
 
   if (aAttribute == nsAccessibilityAtoms::selected ||
       aAttribute == nsAccessibilityAtoms::aria_selected) {
     // ARIA or XUL selection
@@ -1153,34 +1159,28 @@ nsDocAccessible::ARIAAttributeChanged(ns
     return;
   }
 
   // The following ARIA attributes only take affect when dynamic content role is present
   if (aAttribute == nsAccessibilityAtoms::aria_checked ||
       aAttribute == nsAccessibilityAtoms::aria_pressed) {
     const PRUint32 kState = (aAttribute == nsAccessibilityAtoms::aria_checked) ?
                             states::CHECKED : states::PRESSED;
-    nsRefPtr<AccEvent> event =
-      new AccStateChangeEvent(aContent, kState);
+    nsRefPtr<AccEvent> event = new AccStateChangeEvent(aContent, kState);
     FireDelayedAccessibleEvent(event);
-    if (aContent == gLastFocusedNode) {
-      // State changes for MIXED state currently only supported for focused item, because
-      // otherwise we would need access to the old attribute value in this listener.
-      // This is because we don't know if the previous value of aria-checked or aria-pressed was "mixed"
-      // without caching that info.
-      nsAccessible *accessible = event->GetAccessible();
-      if (accessible) {
-        PRBool wasMixed = (gLastFocusedAccessiblesState & states::MIXED) != 0;
-        PRBool isMixed  =
-          (accessible->State() & states::MIXED) != 0;
-        if (wasMixed != isMixed) {
-          nsRefPtr<AccEvent> event =
-            new AccStateChangeEvent(aContent, states::MIXED, isMixed);
-          FireDelayedAccessibleEvent(event);
-        }
+
+    nsAccessible* accessible = event->GetAccessible();
+    if (accessible) {
+      bool wasMixed = (mARIAAttrOldValue == nsAccessibilityAtoms::mixed);
+      bool isMixed = aContent->AttrValueIs(kNameSpaceID_None, aAttribute,
+                                           nsAccessibilityAtoms::mixed, eCaseMatters);
+      if (isMixed != wasMixed) {
+        nsRefPtr<AccEvent> event =
+          new AccStateChangeEvent(aContent, states::MIXED, isMixed);
+        FireDelayedAccessibleEvent(event);
       }
     }
     return;
   }
 
   if (aAttribute == nsAccessibilityAtoms::aria_readonly) {
     nsRefPtr<AccEvent> event =
       new AccStateChangeEvent(aContent, states::READONLY);
--- a/accessible/src/base/nsDocAccessible.h
+++ b/accessible/src/base/nsDocAccessible.h
@@ -562,17 +562,21 @@ protected:
    */
   PRUint32 mLoadState;
 
   /**
    * Type of document load event fired after the document is loaded completely.
    */
   PRUint32 mLoadEventType;
 
-  static PRUint64 gLastFocusedAccessiblesState;
+  /**
+   * Keep the ARIA attribute old value that is initialized by
+   * AttributeWillChange and used by AttributeChanged notifications.
+   */
+  nsIAtom* mARIAAttrOldValue;
 
   nsTArray<nsRefPtr<nsDocAccessible> > mChildDocuments;
 
   /**
    * A storage class for pairing content with one of its relation attributes.
    */
   class AttrRelProvider
   {
--- a/accessible/src/base/nsRootAccessible.cpp
+++ b/accessible/src/base/nsRootAccessible.cpp
@@ -350,18 +350,16 @@ nsRootAccessible::FireAccessibleFocusEve
   // aForceEvent == PR_TRUE
   nsINode* focusNode = focusAccessible->GetNode();
   if (gLastFocusedNode == focusNode && !aForceEvent)
     return;
 
   nsDocAccessible* focusDocument = focusAccessible->GetDocAccessible();
   NS_ASSERTION(focusDocument, "No document while accessible is in document?!");
 
-  gLastFocusedAccessiblesState = focusAccessible->State();
-
   // Fire menu start/end events for ARIA menus.
   if (focusAccessible->ARIARole() == nsIAccessibleRole::ROLE_MENUITEM) {
     // The focus is inside a menu.
     if (!mCurrentARIAMenubar) {
       // Entering ARIA menu. Fire menu start event.
       nsAccessible* menuBarAccessible =
         nsAccUtils::GetAncestorWithRole(focusAccessible,
                                         nsIAccessibleRole::ROLE_MENUBAR);
@@ -663,17 +661,16 @@ nsRootAccessible::ProcessDOMEvent(nsIDOM
             return;
         }
       }
     }
     FireAccessibleFocusEvent(accessible, origTargetContent);
   }
   else if (eventType.EqualsLiteral("blur")) {
     NS_IF_RELEASE(gLastFocusedNode);
-    gLastFocusedAccessiblesState = 0;
   }
   else if (eventType.EqualsLiteral("AlertActive")) { 
     nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_ALERT, accessible);
   }
   else if (eventType.EqualsLiteral("popupshown")) {
     HandlePopupShownEvent(accessible);
   }
   else if (eventType.EqualsLiteral("DOMMenuInactive")) {
--- a/accessible/tests/mochitest/events.js
+++ b/accessible/tests/mochitest/events.js
@@ -597,20 +597,21 @@ function eventQueue(aEventType)
     var target2 = (aEvent instanceof nsIDOMEvent) ?
       aEvent.originalTarget : aEvent.DOMNode;
     return target1 == target2;
   }
 
   this.isAlreadyCaught = function eventQueue_isAlreadyCaught(aIdx, aEvent)
   {
     // We don't have stored info about handled event other than its type and
-    // target, thus we should filter text change events since they may occur
-    // on the same element because of complex changes.
+    // 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(aIdx, aEvent) &&
-      !(aEvent instanceof nsIAccessibleTextChangeEvent);
+      !(aEvent instanceof nsIAccessibleTextChangeEvent) &&
+      !(aEvent instanceof nsIAccessibleStateChangeEvent);
   }
 
   this.checkEvent = function eventQueue_checkEvent(aIdx, aEvent)
   {
     var eventItem = this.mEventSeq[aIdx];
     if ("check" in eventItem)
       eventItem.check(aEvent);
 
@@ -1036,16 +1037,82 @@ function caretMoveChecker(aCaretOffset)
   this.check = function caretMoveChecker_check(aEvent)
   {
     is(aEvent.QueryInterface(nsIAccessibleCaretMoveEvent).caretOffset,
        aCaretOffset,
        "Wrong caret offset for " + prettyName(aEvent.accessible));
   }
 }
 
+/**
+ * State change checker.
+ */
+function stateChangeChecker(aState, aIsExtraState, aIsEnabled,
+                            aTargetOrFunc, aTargetFuncArg)
+{
+  this.__proto__ = new invokerChecker(EVENT_STATE_CHANGE, aTargetOrFunc,
+                                      aTargetFuncArg);
+
+  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.state, aState, "Wrong state of the statechange event.");
+    is(event.isExtraState(), aIsExtraState,
+       "Wrong extra state bit of the statechange event.");
+    is(event.isEnabled(), aIsEnabled,
+      "Wrong state of statechange event state");
+
+    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);
+  }
+}
+
+/**
+ * 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));
+  }
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Private implementation details.
 ////////////////////////////////////////////////////////////////////////////////
 
 
 ////////////////////////////////////////////////////////////////////////////////
 // General
 
--- a/accessible/tests/mochitest/events/test_aria_statechange.html
+++ b/accessible/tests/mochitest/events/test_aria_statechange.html
@@ -21,97 +21,163 @@
   <script type="application/javascript">
 
 
     /**
      * Do tests.
      */
     var gQueue = null;
 
-    // Debug stuff.
-    //gA11yEventDumpID = "eventdump";
-    // gA11yEventDumpToConsole = true;
+    //gA11yEventDumpID = "eventdump"; // debugging
+    //gA11yEventDumpToConsole = true; // debugging
 
-    function expandNode(aNodeOrID, bExpand)
+    function expandNode(aID, aIsExpanded)
     {
-      this.DOMNode = getNode(aNodeOrID);
+      this.DOMNode = getNode(aID);
 
-      this.invoke = function expandNode_invoke() {
-        // Note: this should fire an EVENT_STATE_CHANGE
-        this.DOMNode.setAttribute("aria-expanded", bExpand);
+      this.eventSeq = [
+        new expandedStateChecker(aIsExpanded, this.DOMNode)
+      ];
+
+      this.invoke = function expandNode_invoke()
+      {
+        this.DOMNode.setAttribute("aria-expanded",
+                                  (aIsExpanded ? "true" : "false"));
       };
 
-      this.check = function expandNode_check() {
-        testStates(aNodeOrID,
-                   bExpand ? STATE_EXPANDED : STATE_COLLAPSED,
-                   EXT_STATE_EXPANDABLE);
+      this.getID = function expandNode_getID()
+      {
+        return prettyName(aID) + " aria-expanded changed to '" + aIsExpanded + "'";
+      };
+    }
+
+    function busyify(aID, aIsBusy)
+    {
+      this.DOMNode = getNode(aID);
+
+      this.eventSeq = [
+        new stateChangeChecker(STATE_BUSY, kOrdinalState, aIsBusy, this.DOMNode)
+      ];
+
+      this.invoke = function busyify_invoke()
+      {
+        this.DOMNode.setAttribute("aria-busy", (aIsBusy ? "true" : "false"));
       };
 
-      this.getID = function expandNode_getID() {
-        return prettyName(aNodeOrID) + " aria-expanded changed";
+      this.getID = function busyify_getID()
+      {
+        return prettyName(aID) + " aria-busy changed to '" + aIsBusy + "'";
       };
     }
 
-    function busyify(aNodeOrID, aBusy)
+    function setAttrOfMixedType(aID, aAttr, aState, aValue)
     {
-      this.DOMNode = getNode(aNodeOrID);
+      this.DOMNode = getNode(aID);
+
+      this.eventSeq = [
+        new stateChangeChecker(aState, kOrdinalState,
+                               aValue == "true", this.DOMNode)
+      ];
 
-      this.invoke = function busyify_invoke() {
-        this.DOMNode.setAttribute("aria-busy", aBusy);
+      if (hasState(aID, STATE_MIXED) || aValue == "mixed") {
+        this.eventSeq.push(
+          new stateChangeChecker(STATE_MIXED, kOrdinalState,
+                                 aValue == "mixed", this.DOMNode)
+        );
+      }
+
+      this.invoke = function setAttrOfMixedType_invoke()
+      {
+        this.DOMNode.setAttribute(aAttr, aValue);
       };
 
-      this.check = function busyify_check(event) {
-        testStates(aNodeOrID,
-                   (aBusy ? STATE_BUSY : 0), 0,
-                   (aBusy ? 0 : STATE_BUSY), 0);
+      this.getID = function setAttrOfMixedType_getID()
+      {
+        return prettyName(aID) + " " + aAttr + " changed to '" + aValue + "'";
       };
+    }
 
-      this.getID = function busyify_getID() {
-        return prettyName(aNodeOrID) + " aria-busy changed to " + aBusy;
-      };
+    function setPressed(aID, aValue)
+    {
+      this.__proto__ =
+        new setAttrOfMixedType(aID, "aria-pressed", STATE_PRESSED, aValue);
+    }
+
+    function setChecked(aID, aValue)
+    {
+      this.__proto__ =
+        new setAttrOfMixedType(aID, "aria-checked", STATE_CHECKED, aValue);
+    }
+
+    function buildQueueForAttrOfMixedType(aQueue, aID, aInvokerFunc)
+    {
+      var list = [ "", "undefined", "false", "true", "mixed" ];
+      for (var i = 0; i < list.length; i++) {
+        for (var j = i + 1; j < list.length; j++) {
+          // XXX: changes from/to "undefined"/"" shouldn't fire state change
+          // events, bug 472142.
+          aQueue.push(new aInvokerFunc(aID, list[i]));
+          aQueue.push(new aInvokerFunc(aID, list[j]));
+        }
+      }
     }
 
     function doTests()
     {
-      gQueue = new eventQueue(EVENT_STATE_CHANGE);
+      gQueue = new eventQueue();
 
       gQueue.push(new expandNode("section", true));
       gQueue.push(new expandNode("section", false));
       gQueue.push(new expandNode("div", true));
       gQueue.push(new expandNode("div", false));
 
       gQueue.push(new busyify("aria_doc", true));
       gQueue.push(new busyify("aria_doc", false));
+
+      buildQueueForAttrOfMixedType(gQueue, "pressable", setPressed);
+      buildQueueForAttrOfMixedType(gQueue, "checkable", setChecked);
+
       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=551684"
      title="No statechange event for aria-expanded on native HTML elements, is fired on ARIA widgets">
     Mozilla Bug 551684
-  </a>
-
+  </a><br>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=648133"
      title="fire state change event for aria-busy"
     Mozilla Bug 648133
+  </a><br>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=467143"
+     title="mixed state change event is fired for focused accessible only"
+    Mozilla Bug 467143
   </a>
+
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
   <div id="eventdump"></div>
 
   <!-- aria-expanded -->
   <div id="section" role="section" aria-expanded="false">expandable section</div>
   <div id="div" aria-expanded="false">expandable native div</div>
 
   <!-- aria-busy -->
   <div id="aria_doc" role="document" tabindex="0">A document</div>
+
+  <!-- aria-pressed -->
+  <div id="pressable" role="button"></div>
+
+  <!-- aria-checked -->
+  <div id="checkable" role="checkbox"></div>
 </body>
 </html>
--- a/accessible/tests/mochitest/states.js
+++ b/accessible/tests/mochitest/states.js
@@ -40,16 +40,18 @@ const EXT_STATE_EXPANDABLE = nsIAccessib
 const EXT_STATE_HORIZONTAL = nsIAccessibleStates.EXT_STATE_HORIZONTAL;
 const EXT_STATE_MULTI_LINE = nsIAccessibleStates.EXT_STATE_MULTI_LINE;
 const EXT_STATE_SINGLE_LINE = nsIAccessibleStates.EXT_STATE_SINGLE_LINE;
 const EXT_STATE_STALE = nsIAccessibleStates.EXT_STATE_STALE;
 const EXT_STATE_SUPPORTS_AUTOCOMPLETION =
   nsIAccessibleStates.EXT_STATE_SUPPORTS_AUTOCOMPLETION;
 const EXT_STATE_VERTICAL = nsIAccessibleStates.EXT_STATE_VERTICAL;
 
+const kOrdinalState = 0;
+const kExtraState = 1;
 
 ////////////////////////////////////////////////////////////////////////////////
 // Test functions
 
 /**
  * Tests the states and extra states of the given accessible.
  * Also tests for unwanted states and extra states.
  * In addition, the function performs a few plausibility checks derived from the