// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/accessibility/ax_tree.h" #include #include #include #include "base/stl_util.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_node.h" #include "ui/accessibility/ax_serializable_tree.h" #include "ui/accessibility/ax_tree_serializer.h" #include "ui/gfx/transform.h" namespace ui { namespace { std::string IntVectorToString(const std::vector& items) { std::string str; for (size_t i = 0; i < items.size(); ++i) { if (i > 0) str += ","; str += base::NumberToString(items[i]); } return str; } std::string GetBoundsAsString(const AXTree& tree, int32_t id) { AXNode* node = tree.GetFromId(id); gfx::RectF bounds = tree.GetTreeBounds(node); return base::StringPrintf("(%.0f, %.0f) size (%.0f x %.0f)", bounds.x(), bounds.y(), bounds.width(), bounds.height()); } std::string GetUnclippedBoundsAsString(const AXTree& tree, int32_t id) { AXNode* node = tree.GetFromId(id); gfx::RectF bounds = tree.GetTreeBounds(node, nullptr, false); return base::StringPrintf("(%.0f, %.0f) size (%.0f x %.0f)", bounds.x(), bounds.y(), bounds.width(), bounds.height()); } bool IsNodeOffscreen(const AXTree& tree, int32_t id) { AXNode* node = tree.GetFromId(id); bool result = false; tree.GetTreeBounds(node, &result); return result; } class FakeAXTreeDelegate : public AXTreeDelegate { public: FakeAXTreeDelegate() : tree_data_changed_(false), root_changed_(false) {} void OnNodeDataWillChange(AXTree* tree, const AXNodeData& old_node_data, const AXNodeData& new_node_data) override {} void OnTreeDataChanged(AXTree* tree, const ui::AXTreeData& old_data, const ui::AXTreeData& new_data) override { tree_data_changed_ = true; } void OnNodeWillBeDeleted(AXTree* tree, AXNode* node) override { deleted_ids_.push_back(node->id()); } void OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) override { subtree_deleted_ids_.push_back(node->id()); } void OnNodeWillBeReparented(AXTree* tree, AXNode* node) override {} void OnSubtreeWillBeReparented(AXTree* tree, AXNode* node) override {} void OnNodeCreated(AXTree* tree, AXNode* node) override { created_ids_.push_back(node->id()); } void OnNodeReparented(AXTree* tree, AXNode* node) override {} void OnNodeChanged(AXTree* tree, AXNode* node) override { changed_ids_.push_back(node->id()); } void OnAtomicUpdateFinished(AXTree* tree, bool root_changed, const std::vector& changes) override { root_changed_ = root_changed; for (size_t i = 0; i < changes.size(); ++i) { int id = changes[i].node->id(); switch (changes[i].type) { case NODE_CREATED: node_creation_finished_ids_.push_back(id); break; case SUBTREE_CREATED: subtree_creation_finished_ids_.push_back(id); break; case NODE_REPARENTED: node_reparented_finished_ids_.push_back(id); break; case SUBTREE_REPARENTED: subtree_reparented_finished_ids_.push_back(id); break; case NODE_CHANGED: change_finished_ids_.push_back(id); break; } } } void OnRoleChanged(AXTree* tree, AXNode* node, ax::mojom::Role old_role, ax::mojom::Role new_role) override { attribute_change_log_.push_back(base::StringPrintf( "Role changed from %s to %s", ToString(old_role), ToString(new_role))); } void OnStateChanged(AXTree* tree, AXNode* node, ax::mojom::State state, bool new_value) override { attribute_change_log_.push_back(base::StringPrintf( "%s changed to %s", ToString(state), new_value ? "true" : "false")); } void OnStringAttributeChanged(AXTree* tree, AXNode* node, ax::mojom::StringAttribute attr, const std::string& old_value, const std::string& new_value) override { attribute_change_log_.push_back( base::StringPrintf("%s changed from %s to %s", ToString(attr), old_value.c_str(), new_value.c_str())); } void OnIntAttributeChanged(AXTree* tree, AXNode* node, ax::mojom::IntAttribute attr, int32_t old_value, int32_t new_value) override { attribute_change_log_.push_back(base::StringPrintf( "%s changed from %d to %d", ToString(attr), old_value, new_value)); } void OnFloatAttributeChanged(AXTree* tree, AXNode* node, ax::mojom::FloatAttribute attr, float old_value, float new_value) override { attribute_change_log_.push_back( base::StringPrintf("%s changed from %s to %s", ToString(attr), base::NumberToString(old_value).c_str(), base::NumberToString(new_value).c_str())); } void OnBoolAttributeChanged(AXTree* tree, AXNode* node, ax::mojom::BoolAttribute attr, bool new_value) override { attribute_change_log_.push_back(base::StringPrintf( "%s changed to %s", ToString(attr), new_value ? "true" : "false")); } void OnIntListAttributeChanged( AXTree* tree, AXNode* node, ax::mojom::IntListAttribute attr, const std::vector& old_value, const std::vector& new_value) override { attribute_change_log_.push_back( base::StringPrintf("%s changed from %s to %s", ToString(attr), IntVectorToString(old_value).c_str(), IntVectorToString(new_value).c_str())); } bool tree_data_changed() const { return tree_data_changed_; } bool root_changed() const { return root_changed_; } const std::vector& deleted_ids() { return deleted_ids_; } const std::vector& subtree_deleted_ids() { return subtree_deleted_ids_; } const std::vector& created_ids() { return created_ids_; } const std::vector& node_creation_finished_ids() { return node_creation_finished_ids_; } const std::vector& subtree_creation_finished_ids() { return subtree_creation_finished_ids_; } const std::vector& node_reparented_finished_ids() { return node_reparented_finished_ids_; } const std::vector& subtree_reparented_finished_ids() { return subtree_reparented_finished_ids_; } const std::vector& change_finished_ids() { return change_finished_ids_; } const std::vector& attribute_change_log() { return attribute_change_log_; } private: bool tree_data_changed_; bool root_changed_; std::vector deleted_ids_; std::vector subtree_deleted_ids_; std::vector created_ids_; std::vector changed_ids_; std::vector node_creation_finished_ids_; std::vector subtree_creation_finished_ids_; std::vector node_reparented_finished_ids_; std::vector subtree_reparented_finished_ids_; std::vector change_finished_ids_; std::vector attribute_change_log_; }; } // namespace TEST(AXTreeTest, SerializeSimpleAXTree) { AXNodeData root; root.id = 1; root.role = ax::mojom::Role::kDialog; root.AddState(ax::mojom::State::kFocusable); root.location = gfx::RectF(0, 0, 800, 600); root.child_ids.push_back(2); root.child_ids.push_back(3); AXNodeData button; button.id = 2; button.role = ax::mojom::Role::kButton; button.location = gfx::RectF(20, 20, 200, 30); AXNodeData checkbox; checkbox.id = 3; checkbox.role = ax::mojom::Role::kCheckBox; checkbox.location = gfx::RectF(20, 50, 200, 30); AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.push_back(root); initial_state.nodes.push_back(button); initial_state.nodes.push_back(checkbox); initial_state.has_tree_data = true; initial_state.tree_data.title = "Title"; AXSerializableTree src_tree(initial_state); std::unique_ptr> tree_source(src_tree.CreateTreeSource()); AXTreeSerializer serializer( tree_source.get()); AXTreeUpdate update; serializer.SerializeChanges(src_tree.root(), &update); AXTree dst_tree; ASSERT_TRUE(dst_tree.Unserialize(update)); const AXNode* root_node = dst_tree.root(); ASSERT_TRUE(root_node != nullptr); EXPECT_EQ(root.id, root_node->id()); EXPECT_EQ(root.role, root_node->data().role); ASSERT_EQ(2, root_node->child_count()); const AXNode* button_node = root_node->ChildAtIndex(0); EXPECT_EQ(button.id, button_node->id()); EXPECT_EQ(button.role, button_node->data().role); const AXNode* checkbox_node = root_node->ChildAtIndex(1); EXPECT_EQ(checkbox.id, checkbox_node->id()); EXPECT_EQ(checkbox.role, checkbox_node->data().role); EXPECT_EQ( "AXTree title=Title\n" "id=1 dialog FOCUSABLE (0, 0)-(800, 600) actions= child_ids=2,3\n" " id=2 button (20, 20)-(200, 30) actions=\n" " id=3 checkBox (20, 50)-(200, 30) actions=\n", dst_tree.ToString()); } TEST(AXTreeTest, SerializeAXTreeUpdate) { AXNodeData list; list.id = 3; list.role = ax::mojom::Role::kList; list.child_ids.push_back(4); list.child_ids.push_back(5); list.child_ids.push_back(6); AXNodeData list_item_2; list_item_2.id = 5; list_item_2.role = ax::mojom::Role::kListItem; AXNodeData list_item_3; list_item_3.id = 6; list_item_3.role = ax::mojom::Role::kListItem; AXNodeData button; button.id = 7; button.role = ax::mojom::Role::kButton; AXTreeUpdate update; update.root_id = 3; update.nodes.push_back(list); update.nodes.push_back(list_item_2); update.nodes.push_back(list_item_3); update.nodes.push_back(button); EXPECT_EQ( "AXTreeUpdate: root id 3\n" "id=3 list (0, 0)-(0, 0) actions= child_ids=4,5,6\n" " id=5 listItem (0, 0)-(0, 0) actions=\n" " id=6 listItem (0, 0)-(0, 0) actions=\n" "id=7 button (0, 0)-(0, 0) actions=\n", update.ToString()); } TEST(AXTreeTest, DeleteUnknownSubtreeFails) { AXNodeData root; root.id = 1; AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.push_back(root); AXTree tree(initial_state); // This should fail because we're asking it to delete // a subtree rooted at id=2, which doesn't exist. AXTreeUpdate update; update.node_id_to_clear = 2; update.nodes.resize(1); update.nodes[0].id = 1; EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Bad node_id_to_clear: 2", tree.error()); } TEST(AXTreeTest, LeaveOrphanedDeletedSubtreeFails) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); initial_state.nodes[0].id = 1; initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[0].child_ids.push_back(3); initial_state.nodes[1].id = 2; initial_state.nodes[2].id = 3; AXTree tree(initial_state); // This should fail because we delete a subtree rooted at id=2 // but never update it. AXTreeUpdate update; update.node_id_to_clear = 2; update.nodes.resize(1); update.nodes[0].id = 3; EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Nodes left pending by the update: 2", tree.error()); } TEST(AXTreeTest, LeaveOrphanedNewChildFails) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; AXTree tree(initial_state); // This should fail because we add a new child to the root node // but never update it. AXTreeUpdate update; update.nodes.resize(1); update.nodes[0].id = 1; update.nodes[0].child_ids.push_back(2); EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Nodes left pending by the update: 2", tree.error()); } TEST(AXTreeTest, DuplicateChildIdFails) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; AXTree tree(initial_state); // This should fail because a child id appears twice. AXTreeUpdate update; update.nodes.resize(2); update.nodes[0].id = 1; update.nodes[0].child_ids.push_back(2); update.nodes[0].child_ids.push_back(2); update.nodes[1].id = 2; EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Node 1 has duplicate child id 2", tree.error()); } TEST(AXTreeTest, InvalidReparentingFails) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); initial_state.nodes[0].id = 1; initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[1].id = 2; initial_state.nodes[1].child_ids.push_back(3); initial_state.nodes[2].id = 3; AXTree tree(initial_state); // This should fail because node 3 is reparented from node 2 to node 1 // without deleting node 1's subtree first. AXTreeUpdate update; update.nodes.resize(3); update.nodes[0].id = 1; update.nodes[0].child_ids.push_back(3); update.nodes[0].child_ids.push_back(2); update.nodes[1].id = 2; update.nodes[2].id = 3; EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Node 3 reparented from 2 to 1", tree.error()); } TEST(AXTreeTest, NoReparentingOfRootIfNoNewRoot) { AXNodeData root; root.id = 1; AXNodeData child1; child1.id = 2; AXNodeData child2; child2.id = 3; root.child_ids = {child1.id}; child1.child_ids = {child2.id}; AXTreeUpdate initial_state; initial_state.root_id = root.id; initial_state.nodes = {root, child1, child2}; AXTree tree(initial_state); // Update the root but don't change it by reparenting |child2| to be a child // of the root. root.child_ids = {child1.id, child2.id}; child1.child_ids = {}; AXTreeUpdate update; update.root_id = root.id; update.node_id_to_clear = root.id; update.nodes = {root, child1, child2}; FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); ASSERT_TRUE(tree.Unserialize(update)); EXPECT_EQ(0U, fake_delegate.deleted_ids().size()); EXPECT_EQ(0U, fake_delegate.subtree_deleted_ids().size()); EXPECT_EQ(0U, fake_delegate.created_ids().size()); EXPECT_EQ(0U, fake_delegate.node_creation_finished_ids().size()); EXPECT_EQ(0U, fake_delegate.subtree_creation_finished_ids().size()); EXPECT_EQ(0U, fake_delegate.node_reparented_finished_ids().size()); ASSERT_EQ(2U, fake_delegate.subtree_reparented_finished_ids().size()); EXPECT_EQ(child1.id, fake_delegate.subtree_reparented_finished_ids()[0]); EXPECT_EQ(child2.id, fake_delegate.subtree_reparented_finished_ids()[1]); ASSERT_EQ(1U, fake_delegate.change_finished_ids().size()); EXPECT_EQ(root.id, fake_delegate.change_finished_ids()[0]); EXPECT_FALSE(fake_delegate.root_changed()); EXPECT_FALSE(fake_delegate.tree_data_changed()); tree.SetDelegate(nullptr); } TEST(AXTreeTest, ReparentRootIfRootChanged) { AXNodeData root; root.id = 1; AXNodeData child1; child1.id = 2; AXNodeData child2; child2.id = 3; root.child_ids = {child1.id}; child1.child_ids = {child2.id}; AXTreeUpdate initial_state; initial_state.root_id = root.id; initial_state.nodes = {root, child1, child2}; AXTree tree(initial_state); // Create a new root and reparent |child2| to be a child of the new root. AXNodeData root2; root2.id = 4; root2.child_ids = {child1.id, child2.id}; child1.child_ids = {}; AXTreeUpdate update; update.root_id = root2.id; update.node_id_to_clear = root.id; update.nodes = {root2, child1, child2}; FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); ASSERT_TRUE(tree.Unserialize(update)); ASSERT_EQ(1U, fake_delegate.deleted_ids().size()); EXPECT_EQ(root.id, fake_delegate.deleted_ids()[0]); ASSERT_EQ(1U, fake_delegate.subtree_deleted_ids().size()); EXPECT_EQ(root.id, fake_delegate.subtree_deleted_ids()[0]); ASSERT_EQ(1U, fake_delegate.created_ids().size()); EXPECT_EQ(root2.id, fake_delegate.created_ids()[0]); EXPECT_EQ(0U, fake_delegate.node_creation_finished_ids().size()); ASSERT_EQ(1U, fake_delegate.subtree_creation_finished_ids().size()); EXPECT_EQ(root2.id, fake_delegate.subtree_creation_finished_ids()[0]); ASSERT_EQ(2U, fake_delegate.node_reparented_finished_ids().size()); EXPECT_EQ(child1.id, fake_delegate.node_reparented_finished_ids()[0]); EXPECT_EQ(child2.id, fake_delegate.node_reparented_finished_ids()[1]); EXPECT_EQ(0U, fake_delegate.subtree_reparented_finished_ids().size()); EXPECT_EQ(0U, fake_delegate.change_finished_ids().size()); EXPECT_TRUE(fake_delegate.root_changed()); EXPECT_FALSE(fake_delegate.tree_data_changed()); tree.SetDelegate(nullptr); } TEST(AXTreeTest, TreeDelegateIsCalled) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(2); initial_state.nodes[0].id = 1; initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[1].id = 2; AXTree tree(initial_state); AXTreeUpdate update; update.root_id = 3; update.node_id_to_clear = 1; update.nodes.resize(2); update.nodes[0].id = 3; update.nodes[0].child_ids.push_back(4); update.nodes[1].id = 4; FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); ASSERT_TRUE(tree.Unserialize(update)); ASSERT_EQ(2U, fake_delegate.deleted_ids().size()); EXPECT_EQ(1, fake_delegate.deleted_ids()[0]); EXPECT_EQ(2, fake_delegate.deleted_ids()[1]); ASSERT_EQ(1U, fake_delegate.subtree_deleted_ids().size()); EXPECT_EQ(1, fake_delegate.subtree_deleted_ids()[0]); ASSERT_EQ(2U, fake_delegate.created_ids().size()); EXPECT_EQ(3, fake_delegate.created_ids()[0]); EXPECT_EQ(4, fake_delegate.created_ids()[1]); ASSERT_EQ(1U, fake_delegate.subtree_creation_finished_ids().size()); EXPECT_EQ(3, fake_delegate.subtree_creation_finished_ids()[0]); ASSERT_EQ(1U, fake_delegate.node_creation_finished_ids().size()); EXPECT_EQ(4, fake_delegate.node_creation_finished_ids()[0]); ASSERT_TRUE(fake_delegate.root_changed()); tree.SetDelegate(nullptr); } TEST(AXTreeTest, TreeDelegateIsCalledForTreeDataChanges) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; initial_state.has_tree_data = true; initial_state.tree_data.title = "Initial"; AXTree tree(initial_state); FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); // An empty update shouldn't change tree data. AXTreeUpdate empty_update; EXPECT_TRUE(tree.Unserialize(empty_update)); EXPECT_FALSE(fake_delegate.tree_data_changed()); EXPECT_EQ("Initial", tree.data().title); // An update with tree data shouldn't change tree data if // |has_tree_data| isn't set. AXTreeUpdate ignored_tree_data_update; ignored_tree_data_update.tree_data.title = "Ignore Me"; EXPECT_TRUE(tree.Unserialize(ignored_tree_data_update)); EXPECT_FALSE(fake_delegate.tree_data_changed()); EXPECT_EQ("Initial", tree.data().title); // An update with |has_tree_data| set should update the tree data. AXTreeUpdate tree_data_update; tree_data_update.has_tree_data = true; tree_data_update.tree_data.title = "New Title"; EXPECT_TRUE(tree.Unserialize(tree_data_update)); EXPECT_TRUE(fake_delegate.tree_data_changed()); EXPECT_EQ("New Title", tree.data().title); tree.SetDelegate(nullptr); } TEST(AXTreeTest, ReparentingDoesNotTriggerNodeCreated) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); initial_state.nodes[0].id = 1; initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[1].id = 2; initial_state.nodes[1].child_ids.push_back(3); initial_state.nodes[2].id = 3; FakeAXTreeDelegate fake_delegate; AXTree tree(initial_state); tree.SetDelegate(&fake_delegate); AXTreeUpdate update; update.nodes.resize(2); update.node_id_to_clear = 2; update.root_id = 1; update.nodes[0].id = 1; update.nodes[0].child_ids.push_back(3); update.nodes[1].id = 3; EXPECT_TRUE(tree.Unserialize(update)) << tree.error(); std::vector created = fake_delegate.node_creation_finished_ids(); std::vector subtree_reparented = fake_delegate.subtree_reparented_finished_ids(); std::vector node_reparented = fake_delegate.node_reparented_finished_ids(); ASSERT_FALSE(base::ContainsValue(created, 3)); ASSERT_TRUE(base::ContainsValue(subtree_reparented, 3)); ASSERT_FALSE(base::ContainsValue(node_reparented, 3)); } TEST(AXTreeTest, TreeDelegateIsNotCalledForReparenting) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(2); initial_state.nodes[0].id = 1; initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[1].id = 2; AXTree tree(initial_state); AXTreeUpdate update; update.node_id_to_clear = 1; update.root_id = 2; update.nodes.resize(2); update.nodes[0].id = 2; update.nodes[0].child_ids.push_back(4); update.nodes[1].id = 4; FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); EXPECT_TRUE(tree.Unserialize(update)); ASSERT_EQ(1U, fake_delegate.deleted_ids().size()); EXPECT_EQ(1, fake_delegate.deleted_ids()[0]); ASSERT_EQ(1U, fake_delegate.subtree_deleted_ids().size()); EXPECT_EQ(1, fake_delegate.subtree_deleted_ids()[0]); ASSERT_EQ(1U, fake_delegate.created_ids().size()); EXPECT_EQ(4, fake_delegate.created_ids()[0]); ASSERT_EQ(1U, fake_delegate.subtree_creation_finished_ids().size()); EXPECT_EQ(4, fake_delegate.subtree_creation_finished_ids()[0]); ASSERT_EQ(1U, fake_delegate.subtree_reparented_finished_ids().size()); EXPECT_EQ(2, fake_delegate.subtree_reparented_finished_ids()[0]); EXPECT_EQ(0U, fake_delegate.node_creation_finished_ids().size()); EXPECT_EQ(0U, fake_delegate.node_reparented_finished_ids().size()); ASSERT_TRUE(fake_delegate.root_changed()); tree.SetDelegate(nullptr); } // UAF caught by ax_tree_fuzzer TEST(AXTreeTest, BogusAXTree) { AXTreeUpdate initial_state; AXNodeData node; node.id = 0; initial_state.nodes.push_back(node); initial_state.nodes.push_back(node); ui::AXTree tree; tree.Unserialize(initial_state); } // UAF caught by ax_tree_fuzzer TEST(AXTreeTest, BogusAXTree2) { AXTreeUpdate initial_state; AXNodeData node; node.id = 0; initial_state.nodes.push_back(node); AXNodeData node2; node2.id = 0; node2.child_ids.push_back(0); node2.child_ids.push_back(0); initial_state.nodes.push_back(node2); ui::AXTree tree; tree.Unserialize(initial_state); } // UAF caught by ax_tree_fuzzer TEST(AXTreeTest, BogusAXTree3) { AXTreeUpdate initial_state; AXNodeData node; node.id = 0; node.child_ids.push_back(1); initial_state.nodes.push_back(node); AXNodeData node2; node2.id = 1; node2.child_ids.push_back(1); node2.child_ids.push_back(1); initial_state.nodes.push_back(node2); ui::AXTree tree; tree.Unserialize(initial_state); } TEST(AXTreeTest, RoleAndStateChangeCallbacks) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; initial_state.nodes[0].role = ax::mojom::Role::kButton; initial_state.nodes[0].SetCheckedState(ax::mojom::CheckedState::kTrue); initial_state.nodes[0].AddState(ax::mojom::State::kFocusable); AXTree tree(initial_state); FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); // Change the role and state. AXTreeUpdate update; update.root_id = 1; update.nodes.resize(1); update.nodes[0].id = 1; update.nodes[0].role = ax::mojom::Role::kCheckBox; update.nodes[0].SetCheckedState(ax::mojom::CheckedState::kFalse); update.nodes[0].AddState(ax::mojom::State::kFocusable); update.nodes[0].AddState(ax::mojom::State::kVisited); EXPECT_TRUE(tree.Unserialize(update)); const std::vector& change_log = fake_delegate.attribute_change_log(); ASSERT_EQ(3U, change_log.size()); EXPECT_EQ("Role changed from button to checkBox", change_log[0]); EXPECT_EQ("visited changed to true", change_log[1]); EXPECT_EQ("checkedState changed from 2 to 1", change_log[2]); tree.SetDelegate(nullptr); } TEST(AXTreeTest, AttributeChangeCallbacks) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; initial_state.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kName, "N1"); initial_state.nodes[0].AddStringAttribute( ax::mojom::StringAttribute::kDescription, "D1"); initial_state.nodes[0].AddBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic, true); initial_state.nodes[0].AddBoolAttribute(ax::mojom::BoolAttribute::kBusy, false); initial_state.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kMinValueForRange, 1.0); initial_state.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kMaxValueForRange, 10.0); initial_state.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kStepValueForRange, 3.0); initial_state.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kScrollX, 5); initial_state.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin, 1); AXTree tree(initial_state); FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); // Change existing attributes. AXTreeUpdate update0; update0.root_id = 1; update0.nodes.resize(1); update0.nodes[0].id = 1; update0.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kName, "N2"); update0.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kDescription, "D2"); update0.nodes[0].AddBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic, false); update0.nodes[0].AddBoolAttribute(ax::mojom::BoolAttribute::kBusy, true); update0.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kMinValueForRange, 2.0); update0.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kMaxValueForRange, 9.0); update0.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kStepValueForRange, 0.5); update0.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kScrollX, 6); update0.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin, 2); EXPECT_TRUE(tree.Unserialize(update0)); const std::vector& change_log = fake_delegate.attribute_change_log(); ASSERT_EQ(9U, change_log.size()); EXPECT_EQ("name changed from N1 to N2", change_log[0]); EXPECT_EQ("description changed from D1 to D2", change_log[1]); EXPECT_EQ("liveAtomic changed to false", change_log[2]); EXPECT_EQ("busy changed to true", change_log[3]); EXPECT_EQ("minValueForRange changed from 1 to 2", change_log[4]); EXPECT_EQ("maxValueForRange changed from 10 to 9", change_log[5]); EXPECT_EQ("stepValueForRange changed from 3 to .5", change_log[6]); EXPECT_EQ("scrollX changed from 5 to 6", change_log[7]); EXPECT_EQ("scrollXMin changed from 1 to 2", change_log[8]); FakeAXTreeDelegate fake_delegate2; tree.SetDelegate(&fake_delegate2); // Add and remove attributes. AXTreeUpdate update1; update1.root_id = 1; update1.nodes.resize(1); update1.nodes[0].id = 1; update1.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kDescription, "D3"); update1.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kValue, "V3"); update1.nodes[0].AddBoolAttribute(ax::mojom::BoolAttribute::kModal, true); update1.nodes[0].AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, 5.0); update1.nodes[0].AddFloatAttribute( ax::mojom::FloatAttribute::kMaxValueForRange, 9.0); update1.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kScrollX, 7); update1.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kScrollXMax, 10); EXPECT_TRUE(tree.Unserialize(update1)); const std::vector& change_log2 = fake_delegate2.attribute_change_log(); ASSERT_EQ(11U, change_log2.size()); EXPECT_EQ("name changed from N2 to ", change_log2[0]); EXPECT_EQ("description changed from D2 to D3", change_log2[1]); EXPECT_EQ("value changed from to V3", change_log2[2]); EXPECT_EQ("busy changed to false", change_log2[3]); EXPECT_EQ("modal changed to true", change_log2[4]); EXPECT_EQ("minValueForRange changed from 2 to 0", change_log2[5]); EXPECT_EQ("stepValueForRange changed from 3 to .5", change_log[6]); EXPECT_EQ("valueForRange changed from 0 to 5", change_log2[7]); EXPECT_EQ("scrollXMin changed from 2 to 0", change_log2[8]); EXPECT_EQ("scrollX changed from 6 to 7", change_log2[9]); EXPECT_EQ("scrollXMax changed from 0 to 10", change_log2[10]); tree.SetDelegate(nullptr); } TEST(AXTreeTest, IntListChangeCallbacks) { std::vector one; one.push_back(1); std::vector two; two.push_back(2); two.push_back(2); std::vector three; three.push_back(3); AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; initial_state.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kControlsIds, one); initial_state.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kRadioGroupIds, two); AXTree tree(initial_state); FakeAXTreeDelegate fake_delegate; tree.SetDelegate(&fake_delegate); // Change existing attributes. AXTreeUpdate update0; update0.root_id = 1; update0.nodes.resize(1); update0.nodes[0].id = 1; update0.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kControlsIds, two); update0.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kRadioGroupIds, three); EXPECT_TRUE(tree.Unserialize(update0)); const std::vector& change_log = fake_delegate.attribute_change_log(); ASSERT_EQ(2U, change_log.size()); EXPECT_EQ("controlsIds changed from 1 to 2,2", change_log[0]); EXPECT_EQ("radioGroupIds changed from 2,2 to 3", change_log[1]); FakeAXTreeDelegate fake_delegate2; tree.SetDelegate(&fake_delegate2); // Add and remove attributes. AXTreeUpdate update1; update1.root_id = 1; update1.nodes.resize(1); update1.nodes[0].id = 1; update1.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kRadioGroupIds, two); update1.nodes[0].AddIntListAttribute(ax::mojom::IntListAttribute::kFlowtoIds, three); EXPECT_TRUE(tree.Unserialize(update1)); const std::vector& change_log2 = fake_delegate2.attribute_change_log(); ASSERT_EQ(3U, change_log2.size()); EXPECT_EQ("controlsIds changed from 2,2 to ", change_log2[0]); EXPECT_EQ("radioGroupIds changed from 3 to 2,2", change_log2[1]); EXPECT_EQ("flowtoIds changed from to 3", change_log2[2]); tree.SetDelegate(nullptr); } // Create a very simple tree and make sure that we can get the bounds of // any node. TEST(AXTreeTest, GetBoundsBasic) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(2); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(100, 10, 400, 300); AXTree tree(tree_update); EXPECT_EQ("(0, 0) size (800 x 600)", GetBoundsAsString(tree, 1)); EXPECT_EQ("(100, 10) size (400 x 300)", GetBoundsAsString(tree, 2)); } // If a node doesn't specify its location but at least one child does have // a location, its computed bounds should be the union of all child bounds. TEST(AXTreeTest, EmptyNodeBoundsIsUnionOfChildren) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(); // Deliberately empty. tree_update.nodes[1].child_ids.push_back(3); tree_update.nodes[1].child_ids.push_back(4); tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(100, 10, 400, 20); tree_update.nodes[3].id = 4; tree_update.nodes[3].location = gfx::RectF(200, 30, 400, 20); AXTree tree(tree_update); EXPECT_EQ("(100, 10) size (500 x 40)", GetBoundsAsString(tree, 2)); } // If a node doesn't specify its location but at least one child does have // a location, it will be offscreen if all of its children are offscreen. TEST(AXTreeTest, EmptyNodeNotOffscreenEvenIfAllChildrenOffscreen) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].role = ax::mojom::Role::kRootWebArea; tree_update.nodes[0].AddBoolAttribute( ax::mojom::BoolAttribute::kClipsChildren, true); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(); // Deliberately empty. tree_update.nodes[1].child_ids.push_back(3); tree_update.nodes[1].child_ids.push_back(4); // Both children are offscreen tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(900, 10, 400, 20); tree_update.nodes[3].id = 4; tree_update.nodes[3].location = gfx::RectF(1000, 30, 400, 20); AXTree tree(tree_update); EXPECT_FALSE(IsNodeOffscreen(tree, 2)); EXPECT_TRUE(IsNodeOffscreen(tree, 3)); EXPECT_TRUE(IsNodeOffscreen(tree, 4)); } // Test that getting the bounds of a node works when there's a transform. TEST(AXTreeTest, GetBoundsWithTransform) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(3); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 400, 300); tree_update.nodes[0].transform.reset(new gfx::Transform()); tree_update.nodes[0].transform->Scale(2.0, 2.0); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[0].child_ids.push_back(3); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(20, 10, 50, 5); tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(20, 30, 50, 5); tree_update.nodes[2].transform.reset(new gfx::Transform()); tree_update.nodes[2].transform->Scale(2.0, 2.0); AXTree tree(tree_update); EXPECT_EQ("(0, 0) size (800 x 600)", GetBoundsAsString(tree, 1)); EXPECT_EQ("(40, 20) size (100 x 10)", GetBoundsAsString(tree, 2)); EXPECT_EQ("(80, 120) size (200 x 20)", GetBoundsAsString(tree, 3)); } // Test that getting the bounds of a node that's inside a container // works correctly. TEST(AXTreeTest, GetBoundsWithContainerId) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(100, 50, 600, 500); tree_update.nodes[1].child_ids.push_back(3); tree_update.nodes[1].child_ids.push_back(4); tree_update.nodes[2].id = 3; tree_update.nodes[2].offset_container_id = 2; tree_update.nodes[2].location = gfx::RectF(20, 30, 50, 5); tree_update.nodes[3].id = 4; tree_update.nodes[3].location = gfx::RectF(20, 30, 50, 5); AXTree tree(tree_update); EXPECT_EQ("(120, 80) size (50 x 5)", GetBoundsAsString(tree, 3)); EXPECT_EQ("(20, 30) size (50 x 5)", GetBoundsAsString(tree, 4)); } // Test that getting the bounds of a node that's inside a scrolling container // works correctly. TEST(AXTreeTest, GetBoundsWithScrolling) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(3); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(100, 50, 600, 500); tree_update.nodes[1].AddIntAttribute(ax::mojom::IntAttribute::kScrollX, 5); tree_update.nodes[1].AddIntAttribute(ax::mojom::IntAttribute::kScrollY, 10); tree_update.nodes[1].child_ids.push_back(3); tree_update.nodes[2].id = 3; tree_update.nodes[2].offset_container_id = 2; tree_update.nodes[2].location = gfx::RectF(20, 30, 50, 5); AXTree tree(tree_update); EXPECT_EQ("(115, 70) size (50 x 5)", GetBoundsAsString(tree, 3)); } TEST(AXTreeTest, GetBoundsEmptyBoundsInheritsFromParent) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(3); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[1].AddBoolAttribute( ax::mojom::BoolAttribute::kClipsChildren, true); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(300, 200, 100, 100); tree_update.nodes[1].child_ids.push_back(3); tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(); AXTree tree(tree_update); EXPECT_EQ("(0, 0) size (800 x 600)", GetBoundsAsString(tree, 1)); EXPECT_EQ("(300, 200) size (100 x 100)", GetBoundsAsString(tree, 2)); EXPECT_EQ("(300, 200) size (100 x 100)", GetBoundsAsString(tree, 3)); EXPECT_EQ("(0, 0) size (800 x 600)", GetUnclippedBoundsAsString(tree, 1)); EXPECT_EQ("(300, 200) size (100 x 100)", GetUnclippedBoundsAsString(tree, 2)); EXPECT_EQ("(300, 200) size (100 x 100)", GetUnclippedBoundsAsString(tree, 3)); EXPECT_FALSE(IsNodeOffscreen(tree, 1)); EXPECT_FALSE(IsNodeOffscreen(tree, 2)); EXPECT_TRUE(IsNodeOffscreen(tree, 3)); } TEST(AXTreeTest, GetBoundsCropsChildToRoot) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(5); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].AddBoolAttribute( ax::mojom::BoolAttribute::kClipsChildren, true); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[0].child_ids.push_back(3); tree_update.nodes[0].child_ids.push_back(4); tree_update.nodes[0].child_ids.push_back(5); // Cropped in the top left tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(-100, -100, 150, 150); // Cropped in the bottom right tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(700, 500, 150, 150); // Offscreen on the top tree_update.nodes[3].id = 4; tree_update.nodes[3].location = gfx::RectF(50, -200, 150, 150); // Offscreen on the bottom tree_update.nodes[4].id = 5; tree_update.nodes[4].location = gfx::RectF(50, 700, 150, 150); AXTree tree(tree_update); EXPECT_EQ("(0, 0) size (50 x 50)", GetBoundsAsString(tree, 2)); EXPECT_EQ("(700, 500) size (100 x 100)", GetBoundsAsString(tree, 3)); EXPECT_EQ("(50, 0) size (150 x 1)", GetBoundsAsString(tree, 4)); EXPECT_EQ("(50, 599) size (150 x 1)", GetBoundsAsString(tree, 5)); // Check the unclipped bounds are as expected. EXPECT_EQ("(-100, -100) size (150 x 150)", GetUnclippedBoundsAsString(tree, 2)); EXPECT_EQ("(700, 500) size (150 x 150)", GetUnclippedBoundsAsString(tree, 3)); EXPECT_EQ("(50, -200) size (150 x 150)", GetUnclippedBoundsAsString(tree, 4)); EXPECT_EQ("(50, 700) size (150 x 150)", GetUnclippedBoundsAsString(tree, 5)); } TEST(AXTreeTest, GetBoundsSetsOffscreenIfClipsChildren) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(5); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].AddBoolAttribute( ax::mojom::BoolAttribute::kClipsChildren, true); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[0].child_ids.push_back(3); tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(0, 0, 200, 200); tree_update.nodes[1].AddBoolAttribute( ax::mojom::BoolAttribute::kClipsChildren, true); tree_update.nodes[1].child_ids.push_back(4); tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(0, 0, 200, 200); tree_update.nodes[2].child_ids.push_back(5); // Clipped by its parent tree_update.nodes[3].id = 4; tree_update.nodes[3].location = gfx::RectF(250, 250, 100, 100); tree_update.nodes[3].offset_container_id = 2; // Outside of its parent, but its parent does not clip children, // so it should not be offscreen. tree_update.nodes[4].id = 5; tree_update.nodes[4].location = gfx::RectF(250, 250, 100, 100); tree_update.nodes[4].offset_container_id = 3; AXTree tree(tree_update); EXPECT_TRUE(IsNodeOffscreen(tree, 4)); EXPECT_FALSE(IsNodeOffscreen(tree, 5)); } TEST(AXTreeTest, GetBoundsUpdatesOffscreen) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(5); tree_update.nodes[0].id = 1; tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600); tree_update.nodes[0].role = ax::mojom::Role::kRootWebArea; tree_update.nodes[0].AddBoolAttribute( ax::mojom::BoolAttribute::kClipsChildren, true); tree_update.nodes[0].child_ids.push_back(2); tree_update.nodes[0].child_ids.push_back(3); tree_update.nodes[0].child_ids.push_back(4); tree_update.nodes[0].child_ids.push_back(5); // Fully onscreen tree_update.nodes[1].id = 2; tree_update.nodes[1].location = gfx::RectF(10, 10, 150, 150); // Cropped in the bottom right tree_update.nodes[2].id = 3; tree_update.nodes[2].location = gfx::RectF(700, 500, 150, 150); // Offscreen on the top tree_update.nodes[3].id = 4; tree_update.nodes[3].location = gfx::RectF(50, -200, 150, 150); // Offscreen on the bottom tree_update.nodes[4].id = 5; tree_update.nodes[4].location = gfx::RectF(50, 700, 150, 150); AXTree tree(tree_update); EXPECT_FALSE(IsNodeOffscreen(tree, 2)); EXPECT_FALSE(IsNodeOffscreen(tree, 3)); EXPECT_TRUE(IsNodeOffscreen(tree, 4)); EXPECT_TRUE(IsNodeOffscreen(tree, 5)); } TEST(AXTreeTest, IntReverseRelations) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(4); initial_state.nodes[0].id = 1; initial_state.nodes[0].AddIntAttribute( ax::mojom::IntAttribute::kActivedescendantId, 2); initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[0].child_ids.push_back(3); initial_state.nodes[0].child_ids.push_back(4); initial_state.nodes[1].id = 2; initial_state.nodes[2].id = 3; initial_state.nodes[2].AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId, 1); initial_state.nodes[3].id = 4; initial_state.nodes[3].AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId, 1); AXTree tree(initial_state); auto reverse_active_descendant = tree.GetReverseRelations(ax::mojom::IntAttribute::kActivedescendantId, 2); ASSERT_EQ(1U, reverse_active_descendant.size()); EXPECT_TRUE(base::ContainsKey(reverse_active_descendant, 1)); reverse_active_descendant = tree.GetReverseRelations(ax::mojom::IntAttribute::kActivedescendantId, 1); ASSERT_EQ(0U, reverse_active_descendant.size()); auto reverse_errormessage = tree.GetReverseRelations(ax::mojom::IntAttribute::kErrormessageId, 1); ASSERT_EQ(0U, reverse_errormessage.size()); auto reverse_member_of = tree.GetReverseRelations(ax::mojom::IntAttribute::kMemberOfId, 1); ASSERT_EQ(2U, reverse_member_of.size()); EXPECT_TRUE(base::ContainsKey(reverse_member_of, 3)); EXPECT_TRUE(base::ContainsKey(reverse_member_of, 4)); AXTreeUpdate update = initial_state; update.nodes.resize(5); update.nodes[0].int_attributes.clear(); update.nodes[0].AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, 5); update.nodes[0].child_ids.push_back(5); update.nodes[2].int_attributes.clear(); update.nodes[4].id = 5; update.nodes[4].AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId, 1); EXPECT_TRUE(tree.Unserialize(update)); reverse_active_descendant = tree.GetReverseRelations(ax::mojom::IntAttribute::kActivedescendantId, 2); ASSERT_EQ(0U, reverse_active_descendant.size()); reverse_active_descendant = tree.GetReverseRelations(ax::mojom::IntAttribute::kActivedescendantId, 5); ASSERT_EQ(1U, reverse_active_descendant.size()); EXPECT_TRUE(base::ContainsKey(reverse_active_descendant, 1)); reverse_member_of = tree.GetReverseRelations(ax::mojom::IntAttribute::kMemberOfId, 1); ASSERT_EQ(2U, reverse_member_of.size()); EXPECT_TRUE(base::ContainsKey(reverse_member_of, 4)); EXPECT_TRUE(base::ContainsKey(reverse_member_of, 5)); } TEST(AXTreeTest, IntListReverseRelations) { std::vector node_two; node_two.push_back(2); std::vector nodes_two_three; nodes_two_three.push_back(2); nodes_two_three.push_back(3); AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); initial_state.nodes[0].id = 1; initial_state.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kLabelledbyIds, node_two); initial_state.nodes[0].child_ids.push_back(2); initial_state.nodes[0].child_ids.push_back(3); initial_state.nodes[1].id = 2; initial_state.nodes[2].id = 3; AXTree tree(initial_state); auto reverse_labelled_by = tree.GetReverseRelations(ax::mojom::IntListAttribute::kLabelledbyIds, 2); ASSERT_EQ(1U, reverse_labelled_by.size()); EXPECT_TRUE(base::ContainsKey(reverse_labelled_by, 1)); reverse_labelled_by = tree.GetReverseRelations(ax::mojom::IntListAttribute::kLabelledbyIds, 3); ASSERT_EQ(0U, reverse_labelled_by.size()); // Change existing attributes. AXTreeUpdate update = initial_state; update.nodes[0].intlist_attributes.clear(); update.nodes[0].AddIntListAttribute( ax::mojom::IntListAttribute::kLabelledbyIds, nodes_two_three); EXPECT_TRUE(tree.Unserialize(update)); reverse_labelled_by = tree.GetReverseRelations(ax::mojom::IntListAttribute::kLabelledbyIds, 3); ASSERT_EQ(1U, reverse_labelled_by.size()); EXPECT_TRUE(base::ContainsKey(reverse_labelled_by, 1)); } TEST(AXTreeTest, SkipIgnoredNodes) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(5); tree_update.nodes[0].id = 1; tree_update.nodes[0].child_ids = {2, 3}; tree_update.nodes[1].id = 2; tree_update.nodes[1].AddState(ax::mojom::State::kIgnored); tree_update.nodes[1].child_ids = {4, 5}; tree_update.nodes[2].id = 3; tree_update.nodes[3].id = 4; tree_update.nodes[4].id = 5; AXTree tree(tree_update); AXNode* root = tree.root(); ASSERT_EQ(2, root->child_count()); ASSERT_EQ(2, root->ChildAtIndex(0)->id()); ASSERT_EQ(3, root->ChildAtIndex(1)->id()); EXPECT_EQ(3, root->GetUnignoredChildCount()); EXPECT_EQ(4, root->GetUnignoredChildAtIndex(0)->id()); EXPECT_EQ(5, root->GetUnignoredChildAtIndex(1)->id()); EXPECT_EQ(3, root->GetUnignoredChildAtIndex(2)->id()); EXPECT_EQ(0, root->GetUnignoredChildAtIndex(0)->GetUnignoredIndexInParent()); EXPECT_EQ(1, root->GetUnignoredChildAtIndex(1)->GetUnignoredIndexInParent()); EXPECT_EQ(2, root->GetUnignoredChildAtIndex(2)->GetUnignoredIndexInParent()); EXPECT_EQ(1, root->GetUnignoredChildAtIndex(0)->GetUnignoredParent()->id()); } } // namespace ui