Fire parent-changed events when an accessible node becomes (un)ignored

If an existing node becomes ignored or unignored, we notify platform ATs
that the parent of that node has changed children, but not that the
children of that node have a changed parent. Some platforms, such as
AT-SPI2, cache parent information separately from child information and
expect notifications about parent changes to maintain an accurate cache.
Address this problem by creating a new AXEventGenerator parent-changed
event and emit it on the existing children of a newly-un(ignored) node.

It turns out that the ordering of these notifications can also matter.
If an about-to-become-focused node is in an about-to-be-reparented
subtree, we need to notify ATs about the reparenting before we notify
them about the focus change. If we do the reverse, the AT may have
stale information about where the new focus is with respect to the
accessibility tree which in turn can break AT-driven navigation. But
BrowserAccessibilityManager::OnAccessibilityEvents was firing focus
events prior to events for any other tree updates in order to ensure
screen readers will correctly process events associated with the
focused node. We should be able to address both needs by first firing
tree updates that come from the ancestors of the focused node, then
then firing a focus event if needed, and finally firing the remaining
events.

Also add expectations for existing related content browser tests for
which AuraLinux expectations did not exist, along with fixing a bug in
the AuraLinux accessible-event recorder and adding detail for children-
changed events.

AXRelnotes: The Orca screen reader has a non-performant workaround for
the lack of parent-changed notifications, namely to not use the AT-SPI
parent caching feature. This fix will make it possible for Orca to use
AT-SPI's parent caching and improve performance.

Bug: 1047496
Change-Id: I6421543f471054c10cf6a4fa657de4d184cd6bfb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2418652
Commit-Queue: Joanmarie Diggs <jdiggs@igalia.com>
Reviewed-by: Dominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#814638}
diff --git a/content/browser/accessibility/accessibility_event_recorder_auralinux.cc b/content/browser/accessibility/accessibility_event_recorder_auralinux.cc
index 20fe9b6..a6a3947 100644
--- a/content/browser/accessibility/accessibility_event_recorder_auralinux.cc
+++ b/content/browser/accessibility/accessibility_event_recorder_auralinux.cc
@@ -88,9 +88,15 @@
   g_signal_query(hint->signal_id, &query);
 
   if (instance_) {
-    instance_->ProcessATKEvent(query.signal_name, n_params, params);
+    // "add" and "remove" are details; not part of the signal name itself.
+    gchar* signal_name =
+        g_strcmp0(query.signal_name, "children-changed")
+            ? g_strdup(query.signal_name)
+            : g_strconcat(query.signal_name, ":",
+                          g_quark_to_string(hint->detail), nullptr);
+    instance_->ProcessATKEvent(signal_name, n_params, params);
+    g_free(signal_name);
   }
-
   return true;
 }
 
@@ -245,6 +251,12 @@
           g_value_get_string(&property_values->new_value);
       log += "DESCRIPTION-CHANGED:";
       log += (new_description) ? new_description : "(null)";
+    } else if (g_strcmp0(property_values->property_name, "accessible-parent") ==
+               0) {
+      log += "PARENT-CHANGED";
+      if (AtkObject* new_parent = static_cast<AtkObject*>(
+              g_value_get_object(&property_values->new_value)))
+        log += " PARENT:(" + AtkObjectToString(new_parent, log_name) + ")";
     } else {
       return;
     }
@@ -255,11 +267,7 @@
     int index = static_cast<int>(g_value_get_uint(&params[1]));
     log += base::StringPrintf(" index:%d", index);
     AtkObject* child = static_cast<AtkObject*>(g_value_get_pointer(&params[2]));
-
-    // Removed children may become stale references by this point.
-    if (event_name.find("::remove") != std::string::npos)
-      log += " CHILD:(REMOVED)";
-    else if (child)
+    if (child)
       log += " CHILD:(" + AtkObjectToString(child, log_name) + ")";
     else
       log += " CHILD:(NULL)";
diff --git a/content/browser/accessibility/browser_accessibility_manager.cc b/content/browser/accessibility/browser_accessibility_manager.cc
index 4745879..cb1c6e7 100644
--- a/content/browser/accessibility/browser_accessibility_manager.cc
+++ b/content/browser/accessibility/browser_accessibility_manager.cc
@@ -467,22 +467,14 @@
     connected_to_parent_tree_node_ = false;
   }
 
-  // Based on the changes to the tree, fire focus events if needed.
-  // Screen readers might not do the right thing if they're not aware of what
-  // has focus, so always try that first. Nothing will be fired if the window
-  // itself isn't focused or if focus hasn't changed.
-  //
-  // We need to fire focus events specifically from the root manager, since we
-  // need the top document's delegate to check if its view has focus.
-  //
-  // If this manager is disconnected from the top document, then root_manager
-  // will be a null pointer and FireFocusEventsIfNeeded won't be able to
-  // retrieve the global focus (not firing an event anyway).
-  if (root_manager)
-    root_manager->FireFocusEventsIfNeeded();
-
+  // Fire any events related to changes to the tree that come from ancestors of
+  // the currently-focused node. We do this so that screen readers are made
+  // aware of changes in the tree which might be relevant to subsequent events
+  // on the focused node, such as the focused node being a descendant of a
+  // reparented node or a newly-shown dialog box.
+  BrowserAccessibility* focus = GetFocus();
+  std::vector<ui::AXEventGenerator::TargetedEvent> deferred_events;
   bool received_load_complete_event = false;
-  // Fire any events related to changes to the tree.
   for (const auto& targeted_event : event_generator()) {
     BrowserAccessibility* event_target = GetFromAXNode(targeted_event.node);
     if (!event_target)
@@ -498,6 +490,38 @@
       received_load_complete_event = true;
     }
 
+    // IsDescendantOf() also returns true in the case of equality.
+    if (focus && focus != event_target && focus->IsDescendantOf(event_target))
+      FireGeneratedEvent(targeted_event.event_params.event, event_target);
+    else
+      deferred_events.push_back(targeted_event);
+  }
+
+  // Screen readers might not process events related to the currently-focused
+  // node if they are not aware that node is now focused, so fire a focus event
+  // before firing any other events on that node. No focus event will be fired
+  // if the window itself isn't focused or if focus hasn't changed.
+  //
+  // We need to fire focus events specifically from the root manager, since we
+  // need the top document's delegate to check if its view has focus.
+  //
+  // If this manager is disconnected from the top document, then root_manager
+  // will be a null pointer and FireFocusEventsIfNeeded won't be able to
+  // retrieve the global focus (not firing an event anyway).
+  if (root_manager)
+    root_manager->FireFocusEventsIfNeeded();
+
+  // Now fire all of the rest of the generated events we previously deferred.
+  for (const auto& targeted_event : deferred_events) {
+    BrowserAccessibility* event_target = GetFromAXNode(targeted_event.node);
+    if (!event_target)
+      continue;
+
+    event_target = RetargetForEvents(
+        event_target, RetargetEventType::RetargetEventTypeGenerated);
+    if (!event_target || !event_target->CanFireEvents())
+      continue;
+
     FireGeneratedEvent(targeted_event.event_params.event, event_target);
   }
   event_generator().ClearEvents();
diff --git a/content/browser/accessibility/browser_accessibility_manager_android.cc b/content/browser/accessibility/browser_accessibility_manager_android.cc
index c6f7e5c0..6fc6d9a 100644
--- a/content/browser/accessibility/browser_accessibility_manager_android.cc
+++ b/content/browser/accessibility/browser_accessibility_manager_android.cc
@@ -273,6 +273,7 @@
     case ui::AXEventGenerator::Event::NAME_CHANGED:
     case ui::AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED:
     case ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED:
+    case ui::AXEventGenerator::Event::PARENT_CHANGED:
     case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED:
     case ui::AXEventGenerator::Event::PORTAL_ACTIVATED:
     case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED:
diff --git a/content/browser/accessibility/browser_accessibility_manager_auralinux.cc b/content/browser/accessibility/browser_accessibility_manager_auralinux.cc
index 5c1467a..b95c41e 100644
--- a/content/browser/accessibility/browser_accessibility_manager_auralinux.cc
+++ b/content/browser/accessibility/browser_accessibility_manager_auralinux.cc
@@ -120,6 +120,11 @@
   ToBrowserAccessibilityAuraLinux(node)->GetNode()->OnDescriptionChanged();
 }
 
+void BrowserAccessibilityManagerAuraLinux::FireParentChangedEvent(
+    BrowserAccessibility* node) {
+  ToBrowserAccessibilityAuraLinux(node)->GetNode()->OnParentChanged();
+}
+
 void BrowserAccessibilityManagerAuraLinux::FireSortDirectionChangedEvent(
     BrowserAccessibility* node) {
   ToBrowserAccessibilityAuraLinux(node)->GetNode()->OnSortDirectionChanged();
@@ -203,6 +208,9 @@
     case ui::AXEventGenerator::Event::INVALID_STATUS_CHANGED:
       FireEvent(node, ax::mojom::Event::kInvalidStatusChanged);
       break;
+    case ui::AXEventGenerator::Event::PARENT_CHANGED:
+      FireParentChangedEvent(node);
+      break;
     case ui::AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED:
     case ui::AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED:
       FireTextAttributesChangedEvent(node);
diff --git a/content/browser/accessibility/browser_accessibility_manager_auralinux.h b/content/browser/accessibility/browser_accessibility_manager_auralinux.h
index 834f2db..4550abc 100644
--- a/content/browser/accessibility/browser_accessibility_manager_auralinux.h
+++ b/content/browser/accessibility/browser_accessibility_manager_auralinux.h
@@ -40,6 +40,7 @@
   void FireLoadingEvent(BrowserAccessibility* node, bool is_loading);
   void FireNameChangedEvent(BrowserAccessibility* node);
   void FireDescriptionChangedEvent(BrowserAccessibility* node);
+  void FireParentChangedEvent(BrowserAccessibility* node);
   void FireSortDirectionChangedEvent(BrowserAccessibility* node);
   void FireTextAttributesChangedEvent(BrowserAccessibility* node);
   void FireSubtreeCreatedEvent(BrowserAccessibility* node);
diff --git a/content/browser/accessibility/browser_accessibility_manager_mac.mm b/content/browser/accessibility/browser_accessibility_manager_mac.mm
index 8c1c60e..84c58c7 100644
--- a/content/browser/accessibility/browser_accessibility_manager_mac.mm
+++ b/content/browser/accessibility/browser_accessibility_manager_mac.mm
@@ -446,6 +446,7 @@
     case ui::AXEventGenerator::Event::NAME_CHANGED:
     case ui::AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED:
     case ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED:
+    case ui::AXEventGenerator::Event::PARENT_CHANGED:
     case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED:
     case ui::AXEventGenerator::Event::PORTAL_ACTIVATED:
     case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED:
diff --git a/content/browser/accessibility/browser_accessibility_manager_win.cc b/content/browser/accessibility/browser_accessibility_manager_win.cc
index b4909c2..2ebf927 100644
--- a/content/browser/accessibility/browser_accessibility_manager_win.cc
+++ b/content/browser/accessibility/browser_accessibility_manager_win.cc
@@ -434,6 +434,7 @@
     case ui::AXEventGenerator::Event::FOCUS_CHANGED:
     case ui::AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED:
     case ui::AXEventGenerator::Event::LOAD_START:
+    case ui::AXEventGenerator::Event::PARENT_CHANGED:
     case ui::AXEventGenerator::Event::PORTAL_ACTIVATED:
     case ui::AXEventGenerator::Event::MENU_ITEM_SELECTED:
     case ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED:
diff --git a/content/browser/accessibility/dump_accessibility_events_browsertest.cc b/content/browser/accessibility/dump_accessibility_events_browsertest.cc
index 84e68ab..41be748 100644
--- a/content/browser/accessibility/dump_accessibility_events_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_events_browsertest.cc
@@ -631,6 +631,7 @@
                        AccessibilityEventsFocusListboxMultiselect) {
   RunEventTest(FILE_PATH_LITERAL("focus-listbox-multiselect.html"));
 }
+
 IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
                        AccessibilityEventsInnerHtmlChange) {
   RunEventTest(FILE_PATH_LITERAL("inner-html-change.html"));
@@ -820,6 +821,21 @@
 }
 
 IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
+                       AccessibilityEventsSubtreeReparentedIgnoredChanged) {
+  RunEventTest(FILE_PATH_LITERAL("subtree-reparented-ignored-changed.html"));
+}
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
+                       AccessibilityEventsSubtreeReparentedViaAppendChild) {
+  RunEventTest(FILE_PATH_LITERAL("subtree-reparented-via-append-child.html"));
+}
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
+                       AccessibilityEventsSubtreeReparentedViaAriaOwns) {
+  RunEventTest(FILE_PATH_LITERAL("subtree-reparented-via-aria-owns.html"));
+}
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
                        AccessibilityEventsTabindexAddedOnPlainDiv) {
   RunEventTest(FILE_PATH_LITERAL("tabindex-added-on-plain-div.html"));
 }
diff --git a/content/test/data/accessibility/event/add-child-expected-auralinux.txt b/content/test/data/accessibility/event/add-child-expected-auralinux.txt
index 0599778..9a709b0 100644
--- a/content/test/data/accessibility/event/add-child-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/add-child-expected-auralinux.txt
@@ -1 +1 @@
-CHILDREN-CHANGED index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:ADD index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/add-hidden-attribute-expected-auralinux.txt b/content/test/data/accessibility/event/add-hidden-attribute-expected-auralinux.txt
new file mode 100644
index 0000000..647c024
--- /dev/null
+++ b/content/test/data/accessibility/event/add-hidden-attribute-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:REMOVE index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/add-hidden-attribute-subtree-expected-auralinux.txt b/content/test/data/accessibility/event/add-hidden-attribute-subtree-expected-auralinux.txt
new file mode 100644
index 0000000..647c024
--- /dev/null
+++ b/content/test/data/accessibility/event/add-hidden-attribute-subtree-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:REMOVE index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/add-hidden-attribute-subtree.html b/content/test/data/accessibility/event/add-hidden-attribute-subtree.html
index fc6dee4..fc279ff 100644
--- a/content/test/data/accessibility/event/add-hidden-attribute-subtree.html
+++ b/content/test/data/accessibility/event/add-hidden-attribute-subtree.html
@@ -1,3 +1,6 @@
+<!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
+-->
 <!DOCTYPE html>
 <html>
 <body>
diff --git a/content/test/data/accessibility/event/add-hidden-attribute.html b/content/test/data/accessibility/event/add-hidden-attribute.html
index c153de2..798ce66 100644
--- a/content/test/data/accessibility/event/add-hidden-attribute.html
+++ b/content/test/data/accessibility/event/add-hidden-attribute.html
@@ -1,3 +1,6 @@
+<!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
+-->
 <!DOCTYPE html>
 <html>
 <body>
diff --git a/content/test/data/accessibility/event/add-subtree-expected-auralinux.txt b/content/test/data/accessibility/event/add-subtree-expected-auralinux.txt
new file mode 100644
index 0000000..9a709b0
--- /dev/null
+++ b/content/test/data/accessibility/event/add-subtree-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:ADD index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/aria-button-expand-expected-auralinux.txt b/content/test/data/accessibility/event/aria-button-expand-expected-auralinux.txt
index c14b518..4dc9387c 100644
--- a/content/test/data/accessibility/event/aria-button-expand-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/aria-button-expand-expected-auralinux.txt
@@ -1,2 +1,2 @@
-CHILDREN-CHANGED index:1 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:ADD index:1 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
 STATE-CHANGE:EXPANDED:TRUE role=ROLE_PUSH_BUTTON name='Click Me' ENABLED,EXPANDABLE,EXPANDED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/aria-combo-box-delay-show-list-expected-auralinux.txt b/content/test/data/accessibility/event/aria-combo-box-delay-show-list-expected-auralinux.txt
index 48cb3d9..e492ed9 100644
--- a/content/test/data/accessibility/event/aria-combo-box-delay-show-list-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/aria-combo-box-delay-show-list-expected-auralinux.txt
@@ -1,6 +1,7 @@
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
-CHILDREN-CHANGED index:1 CHILD:(role=ROLE_LIST_BOX) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:ADD index:1 CHILD:(role=ROLE_LIST_BOX) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
 FOCUS-EVENT role=ROLE_LIST_ITEM name='Apple' ENABLED,FOCUSABLE,FOCUSED,SELECTABLE,SELECTED,SENSITIVE,SHOWING,VISIBLE
+PARENT-CHANGED PARENT:(role=ROLE_DOCUMENT_WEB name='(null)') role=ROLE_COMBO_BOX name='(null)' EDITABLE,ENABLED,EXPANDABLE,EXPANDED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SUPPORTS-AUTOCOMPLETION,SELECTABLE-TEXT
 SELECTION-CHANGED role=ROLE_LIST_BOX name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VERTICAL,VISIBLE
 STATE-CHANGE:FOCUSED:TRUE role=ROLE_LIST_ITEM name='Apple' ENABLED,FOCUSABLE,FOCUSED,SELECTABLE,SELECTED,SENSITIVE,SHOWING,VISIBLE
 STATE-CHANGE:SELECTED:TRUE role=ROLE_LIST_ITEM name='Apple' ENABLED,FOCUSABLE,FOCUSED,SELECTABLE,SELECTED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/aria-hidden-changed-expected-auralinux.txt b/content/test/data/accessibility/event/aria-hidden-changed-expected-auralinux.txt
new file mode 100644
index 0000000..2cb346d
--- /dev/null
+++ b/content/test/data/accessibility/event/aria-hidden-changed-expected-auralinux.txt
@@ -0,0 +1,4 @@
+CHILDREN-CHANGED:ADD index:1 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:ADD index:2 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_HEADING ENABLED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:1 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/aria-hidden-changed.html b/content/test/data/accessibility/event/aria-hidden-changed.html
index dd97f6c..1ccac65 100644
--- a/content/test/data/accessibility/event/aria-hidden-changed.html
+++ b/content/test/data/accessibility/event/aria-hidden-changed.html
@@ -1,4 +1,5 @@
 <!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
 @UIA-WIN-DENY:*
 @UIA-WIN-ALLOW:AriaProperties*
 -->
diff --git a/content/test/data/accessibility/event/aria-hidden-descendants-already-ignored-expected-auralinux.txt b/content/test/data/accessibility/event/aria-hidden-descendants-already-ignored-expected-auralinux.txt
new file mode 100644
index 0000000..19466cf
--- /dev/null
+++ b/content/test/data/accessibility/event/aria-hidden-descendants-already-ignored-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_HEADING) role=ROLE_TOOL_BAR ENABLED,HORIZONTAL,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/aria-invalid-changed.html b/content/test/data/accessibility/event/aria-invalid-changed.html
index 4bf5df2..01909152 100644
--- a/content/test/data/accessibility/event/aria-invalid-changed.html
+++ b/content/test/data/accessibility/event/aria-invalid-changed.html
@@ -1,3 +1,6 @@
+<!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
+-->
 <!DOCTYPE html>
 <html>
 <body>
diff --git a/content/test/data/accessibility/event/button-remove-children-expected-auralinux.txt b/content/test/data/accessibility/event/button-remove-children-expected-auralinux.txt
new file mode 100644
index 0000000..dd86679
--- /dev/null
+++ b/content/test/data/accessibility/event/button-remove-children-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_CHECK_BOX) role=ROLE_PUSH_BUTTON ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/button-remove-children.html b/content/test/data/accessibility/event/button-remove-children.html
index 21ead7f..69876fe 100644
--- a/content/test/data/accessibility/event/button-remove-children.html
+++ b/content/test/data/accessibility/event/button-remove-children.html
@@ -1,4 +1,5 @@
 <!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
 @WIN-DENY:EVENT_OBJECT_LOCATIONCHANGE*
 @WIN-DENY:IA2_EVENT_TEXT_REMOVED*
 
diff --git a/content/test/data/accessibility/event/css-display-expected-auralinux.txt b/content/test/data/accessibility/event/css-display-expected-auralinux.txt
new file mode 100644
index 0000000..7466a04
--- /dev/null
+++ b/content/test/data/accessibility/event/css-display-expected-auralinux.txt
@@ -0,0 +1,2 @@
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_LANDMARK) role=ROLE_TOOL_BAR ENABLED,HORIZONTAL,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_HEADING) role=ROLE_TOOL_BAR ENABLED,HORIZONTAL,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/css-display.html b/content/test/data/accessibility/event/css-display.html
index 763838f..960bc95 100644
--- a/content/test/data/accessibility/event/css-display.html
+++ b/content/test/data/accessibility/event/css-display.html
@@ -1,3 +1,6 @@
+<!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
+-->
 <!DOCTYPE html>
 <html>
 <head>
diff --git a/content/test/data/accessibility/event/remove-child-expected-auralinux.txt b/content/test/data/accessibility/event/remove-child-expected-auralinux.txt
index ce6a3547..647c024 100644
--- a/content/test/data/accessibility/event/remove-child-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/remove-child-expected-auralinux.txt
@@ -1,2 +1 @@
-CHILDREN-CHANGED index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
-STATE-CHANGE:DEFUNCT:TRUE role=ROLE_INVALID name='(null)' DEFUNCT
+CHILDREN-CHANGED:REMOVE index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/remove-child.html b/content/test/data/accessibility/event/remove-child.html
index 202034f..8810fd9 100644
--- a/content/test/data/accessibility/event/remove-child.html
+++ b/content/test/data/accessibility/event/remove-child.html
@@ -1,3 +1,6 @@
+<!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
+-->
 <!DOCTYPE html>
 <html>
 <body>
diff --git a/content/test/data/accessibility/event/remove-hidden-attribute-expected-auralinux.txt b/content/test/data/accessibility/event/remove-hidden-attribute-expected-auralinux.txt
new file mode 100644
index 0000000..9a709b0
--- /dev/null
+++ b/content/test/data/accessibility/event/remove-hidden-attribute-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:ADD index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/remove-hidden-attribute-subtree-expected-auralinux.txt b/content/test/data/accessibility/event/remove-hidden-attribute-subtree-expected-auralinux.txt
new file mode 100644
index 0000000..9a709b0
--- /dev/null
+++ b/content/test/data/accessibility/event/remove-hidden-attribute-subtree-expected-auralinux.txt
@@ -0,0 +1 @@
+CHILDREN-CHANGED:ADD index:2 CHILD:(role=ROLE_LIST_ITEM) role=ROLE_LIST ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/subtree-reparented-ignored-changed-expected-auralinux.txt b/content/test/data/accessibility/event/subtree-reparented-ignored-changed-expected-auralinux.txt
new file mode 100644
index 0000000..6a4fb911
--- /dev/null
+++ b/content/test/data/accessibility/event/subtree-reparented-ignored-changed-expected-auralinux.txt
@@ -0,0 +1,7 @@
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+FOCUS-EVENT role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+FOCUS-EVENT role=ROLE_ENTRY name='Search all issues' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
+PARENT-CHANGED PARENT:(role=ROLE_SECTION name='(null)') role=ROLE_LANDMARK name='(null)' ENABLED,SENSITIVE,SHOWING,VISIBLE
+STATE-CHANGE:FOCUSED:FALSE role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+STATE-CHANGE:FOCUSED:TRUE role=ROLE_ENTRY name='Search all issues' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
+TEXT-CARET-MOVED role=ROLE_ENTRY name='Search all issues' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
diff --git a/content/test/data/accessibility/event/subtree-reparented-ignored-changed.html b/content/test/data/accessibility/event/subtree-reparented-ignored-changed.html
new file mode 100644
index 0000000..7610631
--- /dev/null
+++ b/content/test/data/accessibility/event/subtree-reparented-ignored-changed.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div class="foo">
+  <div class="ignore-me-1">
+    <div class="ignore-me-2">
+      <div role="search">
+        <input id="input" aria-label="Search all issues">
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+  function go() {
+    setTimeout(() => {
+      var div = document.getElementsByClassName("foo")[0];
+      div.setAttribute("tabindex", "0");
+      document.getElementById("input").focus();
+    }, 1000);
+  }
+</script>
+</body>
+</html>
diff --git a/content/test/data/accessibility/event/subtree-reparented-via-append-child-expected-auralinux.txt b/content/test/data/accessibility/event/subtree-reparented-via-append-child-expected-auralinux.txt
new file mode 100644
index 0000000..0e63335
--- /dev/null
+++ b/content/test/data/accessibility/event/subtree-reparented-via-append-child-expected-auralinux.txt
@@ -0,0 +1,6 @@
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+FOCUS-EVENT role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+FOCUS-EVENT role=ROLE_ENTRY name='Search all issues' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
+STATE-CHANGE:FOCUSED:FALSE role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+STATE-CHANGE:FOCUSED:TRUE role=ROLE_ENTRY name='Search all issues' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
+TEXT-CARET-MOVED role=ROLE_ENTRY name='Search all issues' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
diff --git a/content/test/data/accessibility/event/subtree-reparented-via-append-child.html b/content/test/data/accessibility/event/subtree-reparented-via-append-child.html
new file mode 100644
index 0000000..3c674a76
--- /dev/null
+++ b/content/test/data/accessibility/event/subtree-reparented-via-append-child.html
@@ -0,0 +1,30 @@
+<!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
+-->
+<!DOCTYPE html>
+<html>
+<body id="body">
+<div class="ignore-me-1">
+  <div class="ignore-me-2">
+    <div id="search" role="search">
+      <input id="input" aria-label="Search all issues">
+    </div>
+  </div>
+</div>
+<script>
+  function go() {
+    setTimeout(() => {
+      var div = document.createElement("div");
+      div.setAttribute("tabindex", 0);
+
+      var body = document.getElementById("body");
+      body.insertBefore(div, body.childNodes[0]);
+
+      var subtree = document.getElementsByClassName("ignore-me-1")[0];
+      div.appendChild(subtree);
+      document.getElementById("input").focus();
+    }, 1000);
+  }
+</script>
+</body>
+</html>
diff --git a/content/test/data/accessibility/event/subtree-reparented-via-aria-owns-expected-auralinux.txt b/content/test/data/accessibility/event/subtree-reparented-via-aria-owns-expected-auralinux.txt
new file mode 100644
index 0000000..c039034
--- /dev/null
+++ b/content/test/data/accessibility/event/subtree-reparented-via-aria-owns-expected-auralinux.txt
@@ -0,0 +1 @@
+PARENT-CHANGED PARENT:(role=ROLE_DOCUMENT_WEB name='(null)') role=ROLE_PANEL name='(null)' ENABLED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/subtree-reparented-via-aria-owns.html b/content/test/data/accessibility/event/subtree-reparented-via-aria-owns.html
new file mode 100644
index 0000000..69925dd
--- /dev/null
+++ b/content/test/data/accessibility/event/subtree-reparented-via-aria-owns.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div role="group" id="group"></div>
+<div role="search" id="search">
+  <input id="input" aria-label="Search all issues">
+</div>
+<script>
+  function go() {
+    // This change will cause AXTreeSerializer::SerializeChanges to clear the
+    // subtree whose least common ancestor is the ignored generic container
+    // parent of the group. As a result, all of its descendants are treated
+    // as reparented, and we fire the parent-changed event on the group.
+    document.getElementById("group").setAttribute("aria-owns", "search");
+  }
+</script>
+</body>
+</html>
diff --git a/content/test/data/accessibility/event/tabindex-added-on-aria-hidden-expected-auralinux.txt b/content/test/data/accessibility/event/tabindex-added-on-aria-hidden-expected-auralinux.txt
new file mode 100644
index 0000000..f35513a46
--- /dev/null
+++ b/content/test/data/accessibility/event/tabindex-added-on-aria-hidden-expected-auralinux.txt
@@ -0,0 +1,3 @@
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+FOCUS-EVENT role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+STATE-CHANGE:FOCUSED:TRUE role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/tabindex-added-on-plain-div-expected-auralinux.txt b/content/test/data/accessibility/event/tabindex-added-on-plain-div-expected-auralinux.txt
new file mode 100644
index 0000000..f4e19bd
--- /dev/null
+++ b/content/test/data/accessibility/event/tabindex-added-on-plain-div-expected-auralinux.txt
@@ -0,0 +1,2 @@
+# TODO: Technically a state-changed:focusable event should probably be fired.
+# No children-changed event is expected because the div was already in the tree.
diff --git a/content/test/data/accessibility/event/tabindex-removed-on-plain-div-expected-auralinux.txt b/content/test/data/accessibility/event/tabindex-removed-on-plain-div-expected-auralinux.txt
new file mode 100644
index 0000000..23200a66
--- /dev/null
+++ b/content/test/data/accessibility/event/tabindex-removed-on-plain-div-expected-auralinux.txt
@@ -0,0 +1,2 @@
+# TODO: Technically a state-changed:focusable event should probably be fired.
+# No children-changed event is expected because the div remained in the tree.
diff --git a/content/test/data/accessibility/event/text-changed-contenteditable-expected-auralinux.txt b/content/test/data/accessibility/event/text-changed-contenteditable-expected-auralinux.txt
index 3c75b27..df30deb 100644
--- a/content/test/data/accessibility/event/text-changed-contenteditable-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/text-changed-contenteditable-expected-auralinux.txt
@@ -1,13 +1,13 @@
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_SECTION EDITABLE,ENABLED,FOCUSABLE,MULTI-LINE,SENSITIVE,SHOWING,VISIBLE,SELECTABLE-TEXT
 STATE-CHANGE:DEFUNCT:TRUE role=ROLE_INVALID name='(null)' DEFUNCT
 STATE-CHANGE:DEFUNCT:TRUE role=ROLE_INVALID name='(null)' DEFUNCT
 STATE-CHANGE:DEFUNCT:TRUE role=ROLE_INVALID name='(null)' DEFUNCT
diff --git a/content/test/data/accessibility/event/text-changed-expected-auralinux.txt b/content/test/data/accessibility/event/text-changed-expected-auralinux.txt
index 2d84009..dcdbe49 100644
--- a/content/test/data/accessibility/event/text-changed-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/text-changed-expected-auralinux.txt
@@ -1,5 +1,5 @@
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH ENABLED,SENSITIVE,SHOWING,VISIBLE
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH ENABLED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH ENABLED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_PARAGRAPH ENABLED,SENSITIVE,SHOWING,VISIBLE
 NAME-CHANGED:Modified Div role=ROLE_STATIC name='Modified Div' ENABLED,SENSITIVE,SHOWING,VISIBLE
 NAME-CHANGED:Modified Heading role=ROLE_STATIC name='Modified Heading' ENABLED,SENSITIVE,SHOWING,VISIBLE
 STATE-CHANGE:DEFUNCT:TRUE role=ROLE_INVALID name='(null)' DEFUNCT
diff --git a/content/test/data/accessibility/event/text-selection-inside-hidden-element-expected-auralinux.txt b/content/test/data/accessibility/event/text-selection-inside-hidden-element-expected-auralinux.txt
index 569eedf..fe75c0d 100644
--- a/content/test/data/accessibility/event/text-selection-inside-hidden-element-expected-auralinux.txt
+++ b/content/test/data/accessibility/event/text-selection-inside-hidden-element-expected-auralinux.txt
@@ -1,2 +1,2 @@
-CHILDREN-CHANGED index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_SECTION) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
 TEXT-CARET-MOVED role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/visibility-hidden-changed-expected-auralinux.txt b/content/test/data/accessibility/event/visibility-hidden-changed-expected-auralinux.txt
new file mode 100644
index 0000000..0ded3d8
--- /dev/null
+++ b/content/test/data/accessibility/event/visibility-hidden-changed-expected-auralinux.txt
@@ -0,0 +1,6 @@
+CHILDREN-CHANGED:ADD index:0 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:ADD index:1 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_HEADING ENABLED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_STATIC) role=ROLE_HEADING ENABLED,SENSITIVE,SHOWING,VISIBLE
+CHILDREN-CHANGED:REMOVE index:1 CHILD:(role=ROLE_HEADING) role=ROLE_DOCUMENT_WEB ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,VISIBLE
diff --git a/content/test/data/accessibility/event/visibility-hidden-changed.html b/content/test/data/accessibility/event/visibility-hidden-changed.html
index bb3b971..cc1d008 100644
--- a/content/test/data/accessibility/event/visibility-hidden-changed.html
+++ b/content/test/data/accessibility/event/visibility-hidden-changed.html
@@ -1,4 +1,5 @@
 <!--
+@AURALINUX-DENY:STATE-CHANGE:DEFUNCT*
 @UIA-WIN-DENY:*
 @UIA-WIN-ALLOW:AriaProperties*
 @UIA-WIN_DENY:AriaProperties changed on role=document
diff --git a/extensions/common/api/automation.idl b/extensions/common/api/automation.idl
index 4cee77c..4b5c3fc7 100644
--- a/extensions/common/api/automation.idl
+++ b/extensions/common/api/automation.idl
@@ -85,6 +85,7 @@
     nameChanged,
     objectAttributeChanged,
     otherAttributeChanged,
+    parentChanged,
     placeholderChanged,
     portalActivated,
     positionInSetChanged,
diff --git a/extensions/renderer/api/automation/automation_api_util.cc b/extensions/renderer/api/automation/automation_api_util.cc
index 745bfe8..c53ac2a5 100644
--- a/extensions/renderer/api/automation/automation_api_util.cc
+++ b/extensions/renderer/api/automation/automation_api_util.cc
@@ -146,6 +146,7 @@
     case ui::AXEventGenerator::Event::NAME_CHANGED:
     case ui::AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED:
     case ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED:
+    case ui::AXEventGenerator::Event::PARENT_CHANGED:
     case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED:
     case ui::AXEventGenerator::Event::PORTAL_ACTIVATED:
     case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED:
diff --git a/third_party/closure_compiler/externs/automation.js b/third_party/closure_compiler/externs/automation.js
index 937b9b8..59099af 100644
--- a/third_party/closure_compiler/externs/automation.js
+++ b/third_party/closure_compiler/externs/automation.js
@@ -91,6 +91,7 @@
   NAME_CHANGED: 'nameChanged',
   OBJECT_ATTRIBUTE_CHANGED: 'objectAttributeChanged',
   OTHER_ATTRIBUTE_CHANGED: 'otherAttributeChanged',
+  PARENT_CHANGED: 'parentChanged',
   PLACEHOLDER_CHANGED: 'placeholderChanged',
   PORTAL_ACTIVATED: 'portalActivated',
   POSITION_IN_SET_CHANGED: 'positionInSetChanged',
diff --git a/ui/accessibility/ax_event_generator.cc b/ui/accessibility/ax_event_generator.cc
index 1a59c15..0c2168f 100644
--- a/ui/accessibility/ax_event_generator.cc
+++ b/ui/accessibility/ax_event_generator.cc
@@ -48,7 +48,9 @@
 }
 
 // If a node toggled its ignored state, don't also fire children-changed because
-// platforms likely will do that in response to ignored-changed.
+// platforms likely will do that in response to ignored-changed. Also do not
+// fire parent-changed on ignored nodes because functionally the parent did not
+// change as far as platform assistive technologies are concerned.
 // Suppress name- and description-changed because those can be emitted as a side
 // effect of calculating alternative text values for a newly-displayed object.
 // Ditto for text attributes such as foreground and background colors, or
@@ -61,6 +63,7 @@
   RemoveEvent(node_events, AXEventGenerator::Event::DESCRIPTION_CHANGED);
   RemoveEvent(node_events, AXEventGenerator::Event::NAME_CHANGED);
   RemoveEvent(node_events, AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED);
+  RemoveEvent(node_events, AXEventGenerator::Event::PARENT_CHANGED);
   RemoveEvent(node_events, AXEventGenerator::Event::SORT_CHANGED);
   RemoveEvent(node_events, AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED);
   RemoveEvent(node_events,
@@ -229,6 +232,24 @@
                                ax::mojom::EventFrom::kNone,
                                tree_->event_intents());
   }
+
+  // If the ignored state of a node has changed, the inclusion/exclusion of that
+  // node in platform accessibility trees will change. Fire PARENT_CHANGED on
+  // the children of a node whose ignored state changed in order to notify ATs
+  // that existing children may have been reparented.
+  //
+  // We don't fire parent-changed if the invisible state of the node has changed
+  // because when invisibility changes, the entire subtree is being inserted /
+  // removed. For example if the 'hidden' property is changed on list item, we
+  // should not fire parent-changed on the list marker or static text.
+  if (old_node_data.IsIgnored() != new_node_data.IsIgnored() &&
+      !old_node_data.IsInvisible() && !new_node_data.IsInvisible()) {
+    AXNode* node = tree_->GetFromId(new_node_data.id);
+    for (size_t i = 0; i < node->GetUnignoredChildCount(); ++i) {
+      AXNode* child = node->GetUnignoredChildAtIndex(i);
+      AddEvent(child, Event::PARENT_CHANGED);
+    }
+  }
 }
 
 void AXEventGenerator::OnRoleChanged(AXTree* tree,
@@ -621,6 +642,11 @@
   DCHECK_EQ(tree_, tree);
 }
 
+void AXEventGenerator::OnNodeReparented(AXTree* tree, AXNode* node) {
+  DCHECK_EQ(tree_, tree);
+  AddEvent(node, Event::PARENT_CHANGED);
+}
+
 void AXEventGenerator::OnAtomicUpdateFinished(
     AXTree* tree,
     bool root_changed,
@@ -846,6 +872,7 @@
 void AXEventGenerator::PostprocessEvents() {
   std::map<AXNode*, IgnoredChangedStatesBitset> ancestor_ignored_changed_map;
   std::set<AXNode*> removed_subtree_created_nodes;
+  std::set<AXNode*> removed_parent_changed_nodes;
 
   // First pass through |tree_events_|, remove events that we do not need.
   for (auto& iter : tree_events_) {
@@ -887,8 +914,28 @@
       RemoveEvent(&node_events, Event::TEXT_ATTRIBUTE_CHANGED);
     }
 
+    // Don't fire parent changed on this node if any of its ancestors also has
+    // parent changed. However, if the ancestor also has subtree created, it is
+    // possible that the created subtree is actually a newly unignored parent
+    // of an existing node. In that instance, we need to inform ATs that the
+    // existing node's parent has changed on the platform.
+    if (HasEvent(node_events, Event::PARENT_CHANGED)) {
+      while (parent && (tree_events_.find(parent) != tree_events_.end() ||
+                        base::Contains(removed_parent_changed_nodes, parent))) {
+        if ((base::Contains(removed_parent_changed_nodes, parent) ||
+             HasEvent(tree_events_[parent], Event::PARENT_CHANGED)) &&
+            !HasEvent(tree_events_[parent], Event::SUBTREE_CREATED)) {
+          RemoveEvent(&node_events, Event::PARENT_CHANGED);
+          removed_parent_changed_nodes.insert(node);
+          break;
+        }
+        parent = parent->GetUnignoredParent();
+      }
+    }
+
     // Don't fire subtree created on this node if any of its ancestors also has
     // subtree created.
+    parent = node->GetUnignoredParent();
     if (HasEvent(node_events, Event::SUBTREE_CREATED)) {
       while (parent &&
              (tree_events_.find(parent) != tree_events_.end() ||
@@ -1042,6 +1089,8 @@
       return "objectAttributeChanged";
     case AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED:
       return "otherAttributeChanged";
+    case AXEventGenerator::Event::PARENT_CHANGED:
+      return "parentChanged";
     case AXEventGenerator::Event::PLACEHOLDER_CHANGED:
       return "placeholderChanged";
     case AXEventGenerator::Event::PORTAL_ACTIVATED:
diff --git a/ui/accessibility/ax_event_generator.h b/ui/accessibility/ax_event_generator.h
index 71cedcf..34447ba 100644
--- a/ui/accessibility/ax_event_generator.h
+++ b/ui/accessibility/ax_event_generator.h
@@ -74,6 +74,7 @@
     NAME_CHANGED,
     OBJECT_ATTRIBUTE_CHANGED,
     OTHER_ATTRIBUTE_CHANGED,
+    PARENT_CHANGED,
     PLACEHOLDER_CHANGED,
     PORTAL_ACTIVATED,
     POSITION_IN_SET_CHANGED,
@@ -247,6 +248,7 @@
   void OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) override;
   void OnNodeWillBeReparented(AXTree* tree, AXNode* node) override;
   void OnSubtreeWillBeReparented(AXTree* tree, AXNode* node) override;
+  void OnNodeReparented(AXTree* tree, AXNode* node) override;
   void OnAtomicUpdateFinished(AXTree* tree,
                               bool root_changed,
                               const std::vector<Change>& changes) override;
diff --git a/ui/accessibility/ax_event_generator_unittest.cc b/ui/accessibility/ax_event_generator_unittest.cc
index 34f349a..f6a17b2 100644
--- a/ui/accessibility/ax_event_generator_unittest.cc
+++ b/ui/accessibility/ax_event_generator_unittest.cc
@@ -1092,7 +1092,8 @@
   EXPECT_THAT(event_generator,
               UnorderedElementsAre(
                   HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 2),
-                  HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 4)));
+                  HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 4),
+                  HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 5)));
 }
 
 TEST(AXEventGeneratorTest, NodeBecomesIgnored2) {
@@ -1162,7 +1163,8 @@
               UnorderedElementsAre(
                   HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 2),
                   HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 4),
-                  HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 4)));
+                  HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 4),
+                  HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 5)));
 }
 
 TEST(AXEventGeneratorTest, NodeBecomesUnignored2) {
@@ -1203,6 +1205,75 @@
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 4)));
 }
 
+TEST(AXEventGeneratorTest, NodeInsertedViaRoleChange) {
+  // This test inserts a kSearch in between the kRootWebArea and the kTextField,
+  // but the node id are updated reflecting position in the tree. This results
+  // in node 2's role changing along with node 3 being created and added as a
+  // child of node 2.
+  AXTreeUpdate initial_state;
+  initial_state.root_id = 1;
+  initial_state.nodes.resize(2);
+  initial_state.nodes[0].id = 1;
+  initial_state.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  initial_state.nodes[0].child_ids.push_back(2);
+  initial_state.nodes[1].id = 2;
+  initial_state.nodes[1].role = ax::mojom::Role::kTextField;
+  AXTree tree(initial_state);
+
+  AXEventGenerator event_generator(&tree);
+  AXTreeUpdate update;
+  update.root_id = 1;
+  update.nodes.resize(3);
+  update.nodes[0].id = 1;
+  update.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  update.nodes[0].child_ids.push_back(2);
+  update.nodes[1].id = 2;
+  update.nodes[1].role = ax::mojom::Role::kSearch;
+  update.nodes[1].child_ids.push_back(3);
+  update.nodes[2].id = 3;
+  update.nodes[2].role = ax::mojom::Role::kTextField;
+  ASSERT_TRUE(tree.Unserialize(update));
+  EXPECT_THAT(event_generator,
+              UnorderedElementsAre(
+                  HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 3),
+                  HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 2),
+                  HasEventAtNode(AXEventGenerator::Event::ROLE_CHANGED, 2)));
+}
+
+TEST(AXEventGeneratorTest, NodeInserted) {
+  // This test inserts a kSearch in between the kRootWebArea and the kTextField.
+  // The node ids reflect the creation order, and the kTextField is not changed.
+  // Thus this is more like a reparenting.
+  AXTreeUpdate initial_state;
+  initial_state.root_id = 1;
+  initial_state.nodes.resize(2);
+  initial_state.nodes[0].id = 1;
+  initial_state.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  initial_state.nodes[0].child_ids.push_back(2);
+  initial_state.nodes[1].id = 2;
+  initial_state.nodes[1].role = ax::mojom::Role::kTextField;
+  AXTree tree(initial_state);
+
+  AXEventGenerator event_generator(&tree);
+  AXTreeUpdate update;
+  update.root_id = 1;
+  update.nodes.resize(3);
+  update.nodes[0].id = 1;
+  update.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  update.nodes[0].child_ids.push_back(3);
+  update.nodes[1].id = 3;
+  update.nodes[1].role = ax::mojom::Role::kSearch;
+  update.nodes[1].child_ids.push_back(2);
+  update.nodes[2].id = 2;
+  update.nodes[2].role = ax::mojom::Role::kTextField;
+  ASSERT_TRUE(tree.Unserialize(update));
+  EXPECT_THAT(event_generator,
+              UnorderedElementsAre(
+                  HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 3),
+                  HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 1),
+                  HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 2)));
+}
+
 TEST(AXEventGeneratorTest, SubtreeBecomesUnignored) {
   AXTreeUpdate initial_state;
   initial_state.root_id = 1;
@@ -1426,6 +1497,7 @@
               UnorderedElementsAre(
                   HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 2),
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 1),
+                  HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 2),
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 3),
                   HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 3)));
 }
@@ -1591,6 +1663,7 @@
                   HasEventAtNode(AXEventGenerator::Event::CHILDREN_CHANGED, 5),
                   HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 6),
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 1),
+                  HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 2),
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 6)));
 }
 
@@ -1674,6 +1747,7 @@
                   HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 6),
                   HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 7),
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 1),
+                  HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 2),
                   HasEventAtNode(AXEventGenerator::Event::IGNORED_CHANGED, 8)));
 }
 
@@ -1858,13 +1932,17 @@
   initial_state.nodes[2].AddIntAttribute(
       ax::mojom::IntAttribute::kActivedescendantId, 5);
   AXTreeUpdate update = initial_state;
+  // Setting the node_id_to_clear causes AXTree::ComputePendingChangesToNode to
+  // create all of the node's children. Since node 3 already exists and remains
+  // in the tree, that (re)created child is reporting a new parent.
   update.node_id_to_clear = 2;
   ASSERT_TRUE(tree.Unserialize(update));
   EXPECT_THAT(
       event_generator,
       UnorderedElementsAre(
           HasEventAtNode(AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED, 3),
-          HasEventAtNode(AXEventGenerator::Event::RELATED_NODE_CHANGED, 3)));
+          HasEventAtNode(AXEventGenerator::Event::RELATED_NODE_CHANGED, 3),
+          HasEventAtNode(AXEventGenerator::Event::PARENT_CHANGED, 3)));
 }
 
 TEST(AXEventGeneratorTest, ImageAnnotationChanged) {
diff --git a/ui/accessibility/ax_node_data.cc b/ui/accessibility/ax_node_data.cc
index aa82703..0106fd6 100644
--- a/ui/accessibility/ax_node_data.cc
+++ b/ui/accessibility/ax_node_data.cc
@@ -959,8 +959,12 @@
          role == ax::mojom::Role::kIgnored;
 }
 
+bool AXNodeData::IsInvisible() const {
+  return HasState(ax::mojom::State::kInvisible);
+}
+
 bool AXNodeData::IsInvisibleOrIgnored() const {
-  return IsIgnored() || HasState(ax::mojom::State::kInvisible);
+  return IsIgnored() || IsInvisible();
 }
 
 bool AXNodeData::IsInvocable() const {
diff --git a/ui/accessibility/ax_node_data.h b/ui/accessibility/ax_node_data.h
index bc2484c..f2a979d 100644
--- a/ui/accessibility/ax_node_data.h
+++ b/ui/accessibility/ax_node_data.h
@@ -220,6 +220,9 @@
   // Helper to determine if the data has the ignored state or ignored role.
   bool IsIgnored() const;
 
+  // Helper to determine if the data has the invisible state.
+  bool IsInvisible() const;
+
   // Helper to determine if the data has the ignored state, the invisible state
   // or the ignored role.
   bool IsInvisibleOrIgnored() const;