| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/ax_node.h" |
| |
| #include <stdint.h> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/accessibility/ax_enums.mojom-shared.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/accessibility/ax_position.h" |
| #include "ui/accessibility/ax_tree.h" |
| #include "ui/accessibility/ax_tree_data.h" |
| #include "ui/accessibility/ax_tree_id.h" |
| #include "ui/accessibility/test_ax_tree_update.h" |
| #include "ui/accessibility/test_single_ax_tree_manager.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| // The third argument is an optional description string which we don't need |
| // because, after verifying manually, test errors are descriptive enough. |
| MATCHER_P(HasAXNodeID, ax_node_data, "") { |
| return arg->id() == ax_node_data.id; |
| } |
| |
| } // namespace |
| |
| using ::testing::ElementsAre; |
| |
| TEST(AXNodeTest, TreeWalking) { |
| // ++kRootWebArea |
| // ++++kParagraph |
| // ++++++kStaticText IGNORED |
| // ++++kParagraph IGNORED |
| // ++++++kStaticText |
| // ++++kParagraph |
| // ++++++kStaticText |
| // ++++kParagraph IGNORED |
| // ++++++kLink IGNORED |
| // ++++++++kStaticText |
| // ++++++kButton |
| |
| // Numbers at the end of variable names indicate their position under the |
| // root. |
| AXNodeData root; |
| AXNodeData paragraph_0; |
| AXNodeData static_text_0_0_ignored; |
| AXNodeData paragraph_1_ignored; |
| AXNodeData static_text_1_0; |
| AXNodeData paragraph_2; |
| AXNodeData static_text_2_0; |
| AXNodeData paragraph_3_ignored; |
| AXNodeData link_3_0_ignored; |
| AXNodeData static_text_3_0_0; |
| AXNodeData button_3_1; |
| |
| root.id = 1; |
| paragraph_0.id = 2; |
| static_text_0_0_ignored.id = 3; |
| paragraph_1_ignored.id = 4; |
| static_text_1_0.id = 5; |
| paragraph_2.id = 6; |
| static_text_2_0.id = 7; |
| paragraph_3_ignored.id = 8; |
| link_3_0_ignored.id = 9; |
| static_text_3_0_0.id = 10; |
| button_3_1.id = 11; |
| |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {paragraph_0.id, paragraph_1_ignored.id, paragraph_2.id, |
| paragraph_3_ignored.id}; |
| |
| paragraph_0.role = ax::mojom::Role::kParagraph; |
| paragraph_0.child_ids = {static_text_0_0_ignored.id}; |
| |
| static_text_0_0_ignored.role = ax::mojom::Role::kStaticText; |
| static_text_0_0_ignored.AddState(ax::mojom::State::kIgnored); |
| static_text_0_0_ignored.SetName("static_text_0_0_ignored"); |
| |
| paragraph_1_ignored.role = ax::mojom::Role::kParagraph; |
| paragraph_1_ignored.AddState(ax::mojom::State::kIgnored); |
| paragraph_1_ignored.child_ids = {static_text_1_0.id}; |
| |
| static_text_1_0.role = ax::mojom::Role::kStaticText; |
| static_text_1_0.SetName("static_text_1_0"); |
| |
| paragraph_2.role = ax::mojom::Role::kParagraph; |
| paragraph_2.child_ids = {static_text_2_0.id}; |
| |
| static_text_2_0.role = ax::mojom::Role::kStaticText; |
| static_text_2_0.SetName("static_text_2_0"); |
| |
| paragraph_3_ignored.role = ax::mojom::Role::kParagraph; |
| paragraph_3_ignored.AddState(ax::mojom::State::kIgnored); |
| paragraph_3_ignored.child_ids = {link_3_0_ignored.id, button_3_1.id}; |
| |
| link_3_0_ignored.role = ax::mojom::Role::kLink; |
| link_3_0_ignored.AddState(ax::mojom::State::kLinked); |
| link_3_0_ignored.AddState(ax::mojom::State::kIgnored); |
| link_3_0_ignored.child_ids = {static_text_3_0_0.id}; |
| |
| static_text_3_0_0.role = ax::mojom::Role::kStaticText; |
| static_text_3_0_0.SetName("static_text_3_0_0"); |
| |
| button_3_1.role = ax::mojom::Role::kButton; |
| button_3_1.SetName("button_3_1"); |
| |
| AXTreeUpdate initial_state; |
| initial_state.root_id = root.id; |
| initial_state.nodes = {root, |
| paragraph_0, |
| static_text_0_0_ignored, |
| paragraph_1_ignored, |
| static_text_1_0, |
| paragraph_2, |
| static_text_2_0, |
| paragraph_3_ignored, |
| link_3_0_ignored, |
| static_text_3_0_0, |
| button_3_1}; |
| initial_state.has_tree_data = true; |
| |
| AXTreeData tree_data; |
| tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data.title = "Application"; |
| initial_state.tree_data = tree_data; |
| |
| AXTree tree; |
| ASSERT_TRUE(tree.Unserialize(initial_state)) << tree.error(); |
| |
| const AXNode* root_node = tree.root(); |
| ASSERT_EQ(root.id, root_node->id()); |
| |
| EXPECT_THAT( |
| root_node->GetAllChildren(), |
| ElementsAre(HasAXNodeID(paragraph_0), HasAXNodeID(paragraph_1_ignored), |
| HasAXNodeID(paragraph_2), HasAXNodeID(paragraph_3_ignored))); |
| EXPECT_EQ(4u, root_node->GetChildCount()); |
| EXPECT_EQ(4u, root_node->GetChildCountCrossingTreeBoundary()); |
| EXPECT_EQ(5u, root_node->GetUnignoredChildCount()); |
| EXPECT_EQ(5u, root_node->GetUnignoredChildCountCrossingTreeBoundary()); |
| |
| EXPECT_EQ(paragraph_0.id, root_node->GetChildAtIndex(0)->id()); |
| EXPECT_EQ(paragraph_1_ignored.id, root_node->GetChildAtIndex(1)->id()); |
| EXPECT_EQ(paragraph_2.id, root_node->GetChildAtIndex(2)->id()); |
| EXPECT_EQ(paragraph_3_ignored.id, root_node->GetChildAtIndex(3)->id()); |
| EXPECT_EQ(nullptr, root_node->GetChildAtIndex(4)); |
| |
| EXPECT_EQ(paragraph_0.id, |
| root_node->GetChildAtIndexCrossingTreeBoundary(0)->id()); |
| EXPECT_EQ(paragraph_1_ignored.id, |
| root_node->GetChildAtIndexCrossingTreeBoundary(1)->id()); |
| EXPECT_EQ(paragraph_2.id, |
| root_node->GetChildAtIndexCrossingTreeBoundary(2)->id()); |
| EXPECT_EQ(paragraph_3_ignored.id, |
| root_node->GetChildAtIndexCrossingTreeBoundary(3)->id()); |
| EXPECT_EQ(nullptr, root_node->GetChildAtIndexCrossingTreeBoundary(4)); |
| |
| EXPECT_EQ(paragraph_0.id, root_node->GetUnignoredChildAtIndex(0)->id()); |
| EXPECT_EQ(static_text_1_0.id, root_node->GetUnignoredChildAtIndex(1)->id()); |
| EXPECT_EQ(paragraph_2.id, root_node->GetUnignoredChildAtIndex(2)->id()); |
| EXPECT_EQ(static_text_3_0_0.id, root_node->GetUnignoredChildAtIndex(3)->id()); |
| EXPECT_EQ(button_3_1.id, root_node->GetUnignoredChildAtIndex(4)->id()); |
| EXPECT_EQ(nullptr, root_node->GetUnignoredChildAtIndex(5)); |
| |
| EXPECT_EQ(paragraph_0.id, |
| root_node->GetUnignoredChildAtIndexCrossingTreeBoundary(0)->id()); |
| EXPECT_EQ(static_text_1_0.id, |
| root_node->GetUnignoredChildAtIndexCrossingTreeBoundary(1)->id()); |
| EXPECT_EQ(paragraph_2.id, |
| root_node->GetUnignoredChildAtIndexCrossingTreeBoundary(2)->id()); |
| EXPECT_EQ(static_text_3_0_0.id, |
| root_node->GetUnignoredChildAtIndexCrossingTreeBoundary(3)->id()); |
| EXPECT_EQ(button_3_1.id, |
| root_node->GetUnignoredChildAtIndexCrossingTreeBoundary(4)->id()); |
| EXPECT_EQ(nullptr, |
| root_node->GetUnignoredChildAtIndexCrossingTreeBoundary(5)); |
| |
| EXPECT_EQ(nullptr, root_node->GetParent()); |
| EXPECT_EQ(nullptr, root_node->GetParentCrossingTreeBoundary()); |
| EXPECT_EQ(nullptr, root_node->GetUnignoredParent()); |
| EXPECT_EQ(nullptr, root_node->GetUnignoredParentCrossingTreeBoundary()); |
| |
| EXPECT_EQ(root_node, tree.GetFromId(paragraph_0.id)->GetParent()); |
| EXPECT_EQ(root_node, |
| tree.GetFromId(paragraph_0.id)->GetParentCrossingTreeBoundary()); |
| EXPECT_EQ(root_node, tree.GetFromId(paragraph_0.id)->GetUnignoredParent()); |
| EXPECT_EQ( |
| root_node, |
| tree.GetFromId(paragraph_0.id)->GetUnignoredParentCrossingTreeBoundary()); |
| |
| EXPECT_EQ(tree.GetFromId(paragraph_1_ignored.id), |
| tree.GetFromId(static_text_1_0.id)->GetParent()); |
| EXPECT_EQ( |
| tree.GetFromId(paragraph_1_ignored.id), |
| tree.GetFromId(static_text_1_0.id)->GetParentCrossingTreeBoundary()); |
| EXPECT_EQ(root_node, |
| tree.GetFromId(static_text_1_0.id)->GetUnignoredParent()); |
| EXPECT_EQ(root_node, tree.GetFromId(static_text_1_0.id) |
| ->GetUnignoredParentCrossingTreeBoundary()); |
| |
| EXPECT_EQ(0u, root_node->GetIndexInParent()); |
| EXPECT_EQ(0u, root_node->GetUnignoredIndexInParent()); |
| |
| EXPECT_EQ(2u, tree.GetFromId(paragraph_2.id)->GetIndexInParent()); |
| EXPECT_EQ(1u, |
| tree.GetFromId(static_text_1_0.id)->GetUnignoredIndexInParent()); |
| EXPECT_EQ(2u, tree.GetFromId(paragraph_2.id)->GetUnignoredIndexInParent()); |
| |
| EXPECT_EQ(paragraph_0.id, root_node->GetFirstChild()->id()); |
| EXPECT_EQ(paragraph_0.id, |
| root_node->GetFirstChildCrossingTreeBoundary()->id()); |
| EXPECT_EQ(paragraph_0.id, root_node->GetFirstUnignoredChild()->id()); |
| EXPECT_EQ(paragraph_0.id, |
| root_node->GetFirstUnignoredChildCrossingTreeBoundary()->id()); |
| |
| EXPECT_EQ(paragraph_3_ignored.id, root_node->GetLastChild()->id()); |
| EXPECT_EQ(paragraph_3_ignored.id, |
| root_node->GetLastChildCrossingTreeBoundary()->id()); |
| EXPECT_EQ(button_3_1.id, root_node->GetLastUnignoredChild()->id()); |
| EXPECT_EQ(button_3_1.id, |
| root_node->GetLastUnignoredChildCrossingTreeBoundary()->id()); |
| |
| EXPECT_EQ(static_text_0_0_ignored.id, |
| root_node->GetDeepestFirstDescendant()->id()); |
| EXPECT_EQ(paragraph_0.id, |
| root_node->GetDeepestFirstUnignoredDescendant()->id()); |
| |
| EXPECT_EQ(button_3_1.id, root_node->GetDeepestLastDescendant()->id()); |
| EXPECT_EQ(button_3_1.id, |
| root_node->GetDeepestLastUnignoredDescendant()->id()); |
| |
| { |
| std::vector<AXNode*> siblings; |
| for (AXNode* sibling = tree.GetFromId(paragraph_0.id); sibling; |
| sibling = sibling->GetNextSibling()) { |
| siblings.push_back(sibling); |
| } |
| EXPECT_THAT(siblings, ElementsAre(HasAXNodeID(paragraph_0), |
| HasAXNodeID(paragraph_1_ignored), |
| HasAXNodeID(paragraph_2), |
| HasAXNodeID(paragraph_3_ignored))); |
| } |
| |
| { |
| std::vector<AXNode*> siblings; |
| for (AXNode* sibling = tree.GetFromId(paragraph_0.id); sibling; |
| sibling = sibling->GetNextUnignoredSibling()) { |
| siblings.push_back(sibling); |
| } |
| EXPECT_THAT( |
| siblings, |
| ElementsAre(HasAXNodeID(paragraph_0), HasAXNodeID(static_text_1_0), |
| HasAXNodeID(paragraph_2), HasAXNodeID(static_text_3_0_0), |
| HasAXNodeID(button_3_1))); |
| } |
| |
| { |
| std::vector<AXNode*> siblings; |
| for (AXNode* sibling = tree.GetFromId(paragraph_3_ignored.id); |
| sibling; sibling = sibling->GetPreviousSibling()) { |
| siblings.push_back(sibling); |
| } |
| EXPECT_THAT(siblings, ElementsAre(HasAXNodeID(paragraph_3_ignored), |
| HasAXNodeID(paragraph_2), |
| HasAXNodeID(paragraph_1_ignored), |
| HasAXNodeID(paragraph_0))); |
| } |
| |
| { |
| std::vector<AXNode*> siblings; |
| for (AXNode* sibling = tree.GetFromId(button_3_1.id); sibling; |
| sibling = sibling->GetPreviousUnignoredSibling()) { |
| siblings.push_back(sibling); |
| } |
| EXPECT_THAT( |
| siblings, |
| ElementsAre(HasAXNodeID(button_3_1), HasAXNodeID(static_text_3_0_0), |
| HasAXNodeID(paragraph_2), HasAXNodeID(static_text_1_0), |
| HasAXNodeID(paragraph_0))); |
| } |
| |
| { |
| std::vector<AXNode::AllChildIterator> siblings; |
| for (auto iter = root_node->AllChildrenBegin(); |
| iter != root_node->AllChildrenEnd(); ++iter) { |
| siblings.push_back(iter); |
| } |
| EXPECT_THAT(siblings, ElementsAre(HasAXNodeID(paragraph_0), |
| HasAXNodeID(paragraph_1_ignored), |
| HasAXNodeID(paragraph_2), |
| HasAXNodeID(paragraph_3_ignored))); |
| } |
| |
| { |
| std::vector< AXNode::AllChildCrossingTreeBoundaryIterator> siblings; |
| for (auto iter = root_node->AllChildrenCrossingTreeBoundaryBegin(); |
| iter != root_node->AllChildrenCrossingTreeBoundaryEnd(); ++iter) { |
| siblings.push_back(iter); |
| } |
| EXPECT_THAT(siblings, ElementsAre(HasAXNodeID(paragraph_0), |
| HasAXNodeID(paragraph_1_ignored), |
| HasAXNodeID(paragraph_2), |
| HasAXNodeID(paragraph_3_ignored))); |
| } |
| |
| { |
| std::vector<AXNode::UnignoredChildIterator> siblings; |
| for (auto iter = root_node->UnignoredChildrenBegin(); |
| iter != root_node->UnignoredChildrenEnd(); ++iter) { |
| siblings.push_back(iter); |
| } |
| EXPECT_THAT( |
| siblings, |
| ElementsAre(HasAXNodeID(paragraph_0), HasAXNodeID(static_text_1_0), |
| HasAXNodeID(paragraph_2), HasAXNodeID(static_text_3_0_0), |
| HasAXNodeID(button_3_1))); |
| } |
| |
| { |
| std::vector<AXNode::UnignoredChildCrossingTreeBoundaryIterator> |
| siblings; |
| for (auto iter = root_node->UnignoredChildrenCrossingTreeBoundaryBegin(); |
| iter != root_node->UnignoredChildrenCrossingTreeBoundaryEnd(); |
| ++iter) { |
| siblings.push_back(iter); |
| } |
| EXPECT_THAT( |
| siblings, |
| ElementsAre(HasAXNodeID(paragraph_0), HasAXNodeID(static_text_1_0), |
| HasAXNodeID(paragraph_2), HasAXNodeID(static_text_3_0_0), |
| HasAXNodeID(button_3_1))); |
| } |
| } |
| |
| TEST(AXNodeTest, TreeWalkingCrossingTreeBoundary) { |
| AXTreeData tree_data_1; |
| tree_data_1.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data_1.title = "Application"; |
| |
| AXTreeData tree_data_2; |
| tree_data_2.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data_2.parent_tree_id = tree_data_1.tree_id; |
| tree_data_2.title = "Iframe"; |
| |
| AXNodeData root_1; |
| AXNodeData root_2; |
| root_1.id = 1; |
| root_2.id = 1; |
| root_1.role = ax::mojom::Role::kRootWebArea; |
| root_1.AddChildTreeId(tree_data_2.tree_id); |
| root_2.role = ax::mojom::Role::kRootWebArea; |
| |
| AXTreeUpdate initial_state_1; |
| initial_state_1.root_id = root_1.id; |
| initial_state_1.nodes = {root_1}; |
| initial_state_1.has_tree_data = true; |
| initial_state_1.tree_data = tree_data_1; |
| |
| AXTreeUpdate initial_state_2; |
| initial_state_2.root_id = root_2.id; |
| initial_state_2.nodes = {root_2}; |
| initial_state_2.has_tree_data = true; |
| initial_state_2.tree_data = tree_data_2; |
| |
| auto tree_1 = std::make_unique<AXTree>(initial_state_1); |
| TestSingleAXTreeManager tree_manager_1(std::move(tree_1)); |
| auto tree_2 = std::make_unique<AXTree>(initial_state_2); |
| TestSingleAXTreeManager tree_manager_2(std::move(tree_2)); |
| |
| const AXNode* root_node_1 = tree_manager_1.GetRoot(); |
| ASSERT_EQ(root_1.id, root_node_1->id()); |
| |
| const AXNode* root_node_2 = tree_manager_2.GetRoot(); |
| ASSERT_EQ(root_2.id, root_node_2->id()); |
| |
| EXPECT_EQ(0u, root_node_1->GetChildCount()); |
| EXPECT_EQ(1u, root_node_1->GetChildCountCrossingTreeBoundary()); |
| EXPECT_EQ(0u, root_node_1->GetUnignoredChildCount()); |
| EXPECT_EQ(1u, root_node_1->GetUnignoredChildCountCrossingTreeBoundary()); |
| |
| EXPECT_EQ(nullptr, root_node_1->GetChildAtIndex(0)); |
| EXPECT_EQ(root_node_2, root_node_1->GetChildAtIndexCrossingTreeBoundary(0)); |
| EXPECT_EQ(nullptr, root_node_1->GetUnignoredChildAtIndex(0)); |
| EXPECT_EQ(root_node_2, |
| root_node_1->GetUnignoredChildAtIndexCrossingTreeBoundary(0)); |
| |
| EXPECT_EQ(nullptr, root_node_2->GetParent()); |
| EXPECT_EQ(root_node_1, root_node_2->GetParentCrossingTreeBoundary()); |
| EXPECT_EQ(nullptr, root_node_2->GetUnignoredParent()); |
| EXPECT_EQ(root_node_1, root_node_2->GetUnignoredParentCrossingTreeBoundary()); |
| } |
| |
| TEST(AXNodeTest, GetValueForControlTextField) { |
| ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( |
| AXEmbeddedObjectBehavior::kSuppressCharacter); |
| |
| // kRootWebArea |
| // ++kTextField (contenteditable) |
| // ++++kGenericContainer |
| // ++++++kStaticText "Line 1" |
| // ++++++kImage |
| // ++++++kLineBreak '\n' |
| // ++++++kStaticText "Line 2" |
| |
| AXNodeData root; |
| root.id = 1; |
| AXNodeData rich_text_field; |
| rich_text_field.id = 2; |
| AXNodeData rich_text_field_text_container; |
| rich_text_field_text_container.id = 3; |
| AXNodeData rich_text_field_line_1; |
| rich_text_field_line_1.id = 4; |
| AXNodeData rich_text_field_image; |
| rich_text_field_image.id = 5; |
| AXNodeData rich_text_field_line_break; |
| rich_text_field_line_break.id = 6; |
| AXNodeData rich_text_field_line_2; |
| rich_text_field_line_2.id = 7; |
| |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {rich_text_field.id}; |
| |
| rich_text_field.role = ax::mojom::Role::kTextField; |
| rich_text_field.AddState(ax::mojom::State::kEditable); |
| rich_text_field.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field.AddBoolAttribute( |
| ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); |
| rich_text_field.SetName("Rich text field"); |
| rich_text_field.child_ids = {rich_text_field_text_container.id}; |
| |
| rich_text_field_text_container.role = ax::mojom::Role::kGenericContainer; |
| rich_text_field_text_container.AddState(ax::mojom::State::kIgnored); |
| rich_text_field_text_container.AddState(ax::mojom::State::kEditable); |
| rich_text_field_text_container.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field_text_container.child_ids = { |
| rich_text_field_line_1.id, rich_text_field_image.id, |
| rich_text_field_line_break.id, rich_text_field_line_2.id}; |
| |
| rich_text_field_line_1.role = ax::mojom::Role::kStaticText; |
| rich_text_field_line_1.AddState(ax::mojom::State::kEditable); |
| rich_text_field_line_1.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field_line_1.SetName("Line 1"); |
| |
| rich_text_field_image.role = ax::mojom::Role::kImage; |
| rich_text_field_image.AddState(ax::mojom::State::kEditable); |
| rich_text_field_image.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field_image.SetName(AXNode::kEmbeddedObjectCharacterUTF8); |
| |
| rich_text_field_line_break.role = ax::mojom::Role::kLineBreak; |
| rich_text_field_line_break.AddState(ax::mojom::State::kEditable); |
| rich_text_field_line_break.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field_line_break.SetName("\n"); |
| |
| rich_text_field_line_2.role = ax::mojom::Role::kStaticText; |
| rich_text_field_line_2.AddState(ax::mojom::State::kEditable); |
| rich_text_field_line_2.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field_line_2.SetName("Line 2"); |
| |
| AXTreeUpdate update; |
| update.has_tree_data = true; |
| update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| update.root_id = root.id; |
| update.nodes = {root, |
| rich_text_field, |
| rich_text_field_text_container, |
| rich_text_field_line_1, |
| rich_text_field_image, |
| rich_text_field_line_break, |
| rich_text_field_line_2}; |
| |
| auto tree = std::make_unique<AXTree>(update); |
| TestSingleAXTreeManager manager(std::move(tree)); |
| |
| { |
| const AXNode* text_field_node = |
| manager.GetTree()->GetFromId(rich_text_field.id); |
| ASSERT_NE(nullptr, text_field_node); |
| // In the accessibility tree's text representation, there is an implicit |
| // line break before every embedded object, such as an image. |
| EXPECT_EQ("Line 1\n\nLine 2", text_field_node->GetValueForControl()); |
| } |
| |
| // Only rich text fields should have their value attribute automatically |
| // computed from their inner text. Atomic text fields, such as <input> or |
| // <textarea> should not. |
| rich_text_field.RemoveState(ax::mojom::State::kRichlyEditable); |
| rich_text_field.RemoveBoolAttribute( |
| ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot); |
| AXTreeUpdate update_2; |
| update_2.nodes = {rich_text_field}; |
| |
| ASSERT_TRUE(manager.GetTree()->Unserialize(update_2)) |
| << manager.GetTree()->error(); |
| { |
| const AXNode* text_field_node = |
| manager.GetTree()->GetFromId(rich_text_field.id); |
| ASSERT_NE(nullptr, text_field_node); |
| EXPECT_EQ("", text_field_node->GetValueForControl()); |
| } |
| |
| rich_text_field.AddState(ax::mojom::State::kRichlyEditable); |
| rich_text_field.AddBoolAttribute( |
| ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); |
| |
| // A node's data should override any computed node data. |
| rich_text_field.SetValue("Line 1\nLine 2"); |
| AXTreeUpdate update_3; |
| update_3.nodes = {rich_text_field}; |
| |
| ASSERT_TRUE(manager.GetTree()->Unserialize(update_3)) |
| << manager.GetTree()->error(); |
| { |
| const AXNode* text_field_node = |
| manager.GetTree()->GetFromId(rich_text_field.id); |
| ASSERT_NE(nullptr, text_field_node); |
| EXPECT_EQ("Line 1\nLine 2", text_field_node->GetValueForControl()); |
| } |
| } |
| |
| TEST(AXNodeTest, GetLowestPlatformAncestor) { |
| // ++kRootWebArea |
| // ++++kButton (IsLeaf=false) |
| // ++++++kGenericContainer ignored |
| // ++++++++kStaticText "Hello" |
| // ++++++++++kInlineTextBox "Hello" (IsLeaf=true) |
| // ++++kTextField "World" (IsLeaf=true) |
| // ++++++kStaticText "World" |
| // ++++++++kInlineTextBox "World" (IsLeaf=true) |
| AXNodeData root; |
| AXNodeData button; |
| AXNodeData generic_container; |
| AXNodeData static_text_1; |
| AXNodeData inline_box_1; |
| AXNodeData text_field; |
| AXNodeData static_text_2; |
| AXNodeData inline_box_2; |
| |
| root.id = 1; |
| button.id = 2; |
| generic_container.id = 3; |
| static_text_1.id = 4; |
| inline_box_1.id = 5; |
| text_field.id = 6; |
| static_text_2.id = 7; |
| inline_box_2.id = 8; |
| |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {button.id, text_field.id}; |
| |
| button.role = ax::mojom::Role::kButton; |
| button.SetValue("Hello"); |
| button.child_ids = {generic_container.id}; |
| |
| generic_container.role = ax::mojom::Role::kGenericContainer; |
| generic_container.AddState(ax::mojom::State::kIgnored); |
| generic_container.child_ids = {static_text_1.id}; |
| |
| static_text_1.role = ax::mojom::Role::kStaticText; |
| static_text_1.SetName("Hello"); |
| static_text_1.child_ids = {inline_box_1.id}; |
| |
| inline_box_1.role = ax::mojom::Role::kInlineTextBox; |
| inline_box_1.SetName("Hello"); |
| |
| text_field.role = ax::mojom::Role::kTextField; |
| text_field.AddState(ax::mojom::State::kEditable); |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); |
| text_field.SetValue("World"); |
| text_field.child_ids = {static_text_2.id}; |
| |
| static_text_2.role = ax::mojom::Role::kStaticText; |
| static_text_2.AddState(ax::mojom::State::kEditable); |
| static_text_2.SetName("World"); |
| static_text_2.child_ids = {inline_box_2.id}; |
| |
| inline_box_2.role = ax::mojom::Role::kInlineTextBox; |
| inline_box_2.AddState(ax::mojom::State::kEditable); |
| inline_box_2.SetName("World"); |
| |
| AXTreeUpdate initial_state; |
| initial_state.root_id = root.id; |
| initial_state.nodes = {root, button, generic_container, |
| static_text_1, inline_box_1, text_field, |
| static_text_2, inline_box_2}; |
| initial_state.has_tree_data = true; |
| |
| AXTreeData tree_data; |
| tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data.title = "Application"; |
| initial_state.tree_data = tree_data; |
| |
| AXTree tree; |
| ASSERT_TRUE(tree.Unserialize(initial_state)) << tree.error(); |
| |
| const AXNode* root_node = tree.root(); |
| ASSERT_EQ(root.id, root_node->id()); |
| EXPECT_EQ(root_node, root_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* button_node = root_node->children()[0]; |
| ASSERT_EQ(button.id, button_node->id()); |
| EXPECT_EQ(button_node, button_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* generic_container_node = button_node->children()[0]; |
| ASSERT_EQ(generic_container.id, generic_container_node->id()); |
| EXPECT_EQ(button_node, generic_container_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* static_text_1_node = generic_container_node->children()[0]; |
| ASSERT_EQ(static_text_1.id, static_text_1_node->id()); |
| EXPECT_EQ(static_text_1_node, |
| static_text_1_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* inline_box_1_node = static_text_1_node->children()[0]; |
| ASSERT_EQ(inline_box_1.id, inline_box_1_node->id()); |
| EXPECT_EQ(static_text_1_node, inline_box_1_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* text_field_node = root_node->children()[1]; |
| ASSERT_EQ(text_field.id, text_field_node->id()); |
| EXPECT_EQ(text_field_node, text_field_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* static_text_2_node = text_field_node->children()[0]; |
| ASSERT_EQ(static_text_2.id, static_text_2_node->id()); |
| EXPECT_EQ(text_field_node, static_text_2_node->GetLowestPlatformAncestor()); |
| |
| const AXNode* inline_box_2_node = static_text_2_node->children()[0]; |
| ASSERT_EQ(inline_box_2.id, inline_box_2_node->id()); |
| EXPECT_EQ(text_field_node, inline_box_2_node->GetLowestPlatformAncestor()); |
| } |
| |
| TEST(AXNodeTest, GetTextContentRangeBounds) { |
| constexpr char16_t kEnglishText[] = u"Hey"; |
| const std::vector<int32_t> kEnglishCharacterOffsets = {12, 19, 27}; |
| // A Hindi word (which means "Hindi") consisting of two letters. |
| constexpr char16_t kHindiText[] = u"\x0939\x093F\x0928\x094D\x0926\x0940"; |
| const std::vector<int32_t> kHindiCharacterOffsets = {40, 40, 59, 59, 59, 59}; |
| // A Thai word (which means "feel") consisting of 3 letters. |
| constexpr char16_t kThaiText[] = u"\x0E23\x0E39\x0E49\x0E2A\x0E36\x0E01"; |
| const std::vector<int32_t> kThaiCharacterOffsets = {66, 66, 66, 76, 76, 85}; |
| |
| AXNodeData root_data; |
| root_data.id = 1; |
| root_data.role = ax::mojom::Role::kRootWebArea; |
| |
| AXNodeData text_data1; |
| text_data1.id = 2; |
| text_data1.role = ax::mojom::Role::kStaticText; |
| text_data1.SetName(kEnglishText); |
| text_data1.AddIntAttribute( |
| ax::mojom::IntAttribute::kTextDirection, |
| static_cast<int32_t>(ax::mojom::WritingDirection::kLtr)); |
| text_data1.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, |
| kEnglishCharacterOffsets); |
| |
| AXNodeData text_data2; |
| text_data2.id = 3; |
| text_data2.role = ax::mojom::Role::kStaticText; |
| text_data2.SetName(kHindiText); |
| text_data2.AddIntAttribute( |
| ax::mojom::IntAttribute::kTextDirection, |
| static_cast<int32_t>(ax::mojom::WritingDirection::kRtl)); |
| text_data2.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, |
| kHindiCharacterOffsets); |
| |
| AXNodeData text_data3; |
| text_data3.id = 4; |
| text_data3.role = ax::mojom::Role::kStaticText; |
| text_data3.SetName(kThaiText); |
| text_data3.AddIntAttribute( |
| ax::mojom::IntAttribute::kTextDirection, |
| static_cast<int32_t>(ax::mojom::WritingDirection::kTtb)); |
| text_data3.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, |
| kThaiCharacterOffsets); |
| |
| root_data.child_ids = {text_data1.id, text_data2.id, text_data3.id}; |
| |
| AXTreeUpdate update; |
| update.root_id = root_data.id; |
| update.nodes = {root_data, text_data1, text_data2, text_data3}; |
| update.has_tree_data = true; |
| |
| AXTreeData tree_data; |
| tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data.title = "Application"; |
| update.tree_data = tree_data; |
| |
| AXTree tree; |
| ASSERT_TRUE(tree.Unserialize(update)) << tree.error(); |
| |
| const AXNode* root_node = tree.root(); |
| ASSERT_EQ(root_data.id, root_node->id()); |
| |
| const AXNode* text1_node = root_node->GetUnignoredChildAtIndex(0); |
| ASSERT_EQ(text_data1.id, text1_node->id()); |
| const AXNode* text2_node = root_node->GetUnignoredChildAtIndex(1); |
| ASSERT_EQ(text_data2.id, text2_node->id()); |
| const AXNode* text3_node = root_node->GetUnignoredChildAtIndex(2); |
| ASSERT_EQ(text_data3.id, text3_node->id()); |
| |
| // Offsets correspond to code units in UTF-16 |
| // Each character is a single glyph in `kEnglishText`. |
| EXPECT_EQ(gfx::RectF(0, 0, 27, 0), |
| text1_node->GetTextContentRangeBoundsUTF16(0, 3)); |
| EXPECT_EQ(gfx::RectF(12, 0, 7, 0), |
| text1_node->GetTextContentRangeBoundsUTF16(1, 2)); |
| EXPECT_EQ(gfx::RectF(), text1_node->GetTextContentRangeBoundsUTF16(2, 4)); |
| |
| // `kHindiText` is 6 code units in UTF-16. |
| EXPECT_EQ(gfx::RectF(0, 0, 59, 0), |
| text2_node->GetTextContentRangeBoundsUTF16(0, 6)); |
| EXPECT_EQ(gfx::RectF(0, 0, 19, 0), |
| text2_node->GetTextContentRangeBoundsUTF16(2, 4)); |
| |
| // `kThaiText` is 6 code units in UTF-16. |
| EXPECT_EQ(gfx::RectF(0, 0, 0, 85), |
| text3_node->GetTextContentRangeBoundsUTF16(0, 6)); |
| EXPECT_EQ(gfx::RectF(0, 66, 0, 10), |
| text3_node->GetTextContentRangeBoundsUTF16(2, 4)); |
| } |
| |
| TEST(AXNodeTest, IsGridCellReadOnlyOrDisabled) { |
| // ++kRootWebArea |
| // ++++kGrid |
| // ++++kRow |
| // ++++++kGridCell |
| // ++++++kGridCell |
| AXNodeData root; |
| AXNodeData grid; |
| AXNodeData row; |
| AXNodeData gridcell_1; |
| AXNodeData gridcell_2; |
| AXNodeData gridcell_3; |
| |
| root.id = 1; |
| grid.id = 2; |
| row.id = 3; |
| gridcell_1.id = 4; |
| gridcell_2.id = 5; |
| gridcell_3.id = 6; |
| |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {grid.id}; |
| |
| grid.role = ax::mojom::Role::kGrid; |
| grid.child_ids = {row.id}; |
| |
| row.role = ax::mojom::Role::kRow; |
| row.child_ids = {gridcell_1.id, gridcell_2.id, gridcell_3.id}; |
| |
| gridcell_1.role = ax::mojom::Role::kCell; |
| gridcell_1.AddIntAttribute( |
| ax::mojom::IntAttribute::kRestriction, |
| static_cast<int32_t>(ax::mojom::Restriction::kNone)); |
| |
| gridcell_2.role = ax::mojom::Role::kCell; |
| gridcell_2.AddIntAttribute( |
| ax::mojom::IntAttribute::kRestriction, |
| static_cast<int32_t>(ax::mojom::Restriction::kReadOnly)); |
| |
| gridcell_3.role = ax::mojom::Role::kCell; |
| gridcell_3.AddIntAttribute( |
| ax::mojom::IntAttribute::kRestriction, |
| static_cast<int32_t>(ax::mojom::Restriction::kDisabled)); |
| |
| AXTreeUpdate initial_state; |
| initial_state.root_id = root.id; |
| initial_state.nodes = {root, grid, row, gridcell_1, gridcell_2, gridcell_3}; |
| initial_state.has_tree_data = true; |
| |
| AXTreeData tree_data; |
| tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data.title = "Application"; |
| initial_state.tree_data = tree_data; |
| |
| AXTree tree; |
| ASSERT_TRUE(tree.Unserialize(initial_state)) << tree.error(); |
| |
| const AXNode* root_node = tree.root(); |
| ASSERT_EQ(root.id, root_node->id()); |
| |
| const AXNode* grid_node = root_node->children()[0]; |
| ASSERT_EQ(grid.id, grid_node->id()); |
| EXPECT_FALSE(grid_node->IsReadOnlyOrDisabled()); |
| |
| const AXNode* row_node = grid_node->children()[0]; |
| ASSERT_EQ(row.id, row_node->id()); |
| EXPECT_TRUE(row_node->IsReadOnlyOrDisabled()); |
| |
| const AXNode* gridcell_1_node = row_node->children()[0]; |
| ASSERT_EQ(gridcell_1.id, gridcell_1_node->id()); |
| EXPECT_FALSE(gridcell_1_node->IsReadOnlyOrDisabled()); |
| |
| const AXNode* gridcell_2_node = row_node->children()[1]; |
| ASSERT_EQ(gridcell_2.id, gridcell_2_node->id()); |
| EXPECT_TRUE(gridcell_2_node->IsReadOnlyOrDisabled()); |
| |
| const AXNode* gridcell_3_node = row_node->children()[2]; |
| ASSERT_EQ(gridcell_3.id, gridcell_3_node->id()); |
| EXPECT_TRUE(gridcell_3_node->IsReadOnlyOrDisabled()); |
| } |
| |
| TEST(AXNodeTest, GetLowestCommonAncestor) { |
| // ++kRootWebArea |
| // ++++kParagraph |
| // ++++++kStaticText |
| // ++++kParagraph |
| // ++++++kLink |
| // ++++++++kStaticText |
| // ++++++kButton |
| |
| // Numbers at the end of variable names indicate their position under the |
| // root. |
| AXNodeData root; |
| AXNodeData paragraph_0; |
| AXNodeData static_text_0_0; |
| AXNodeData paragraph_1; |
| AXNodeData link_1_0; |
| AXNodeData static_text_1_0_0; |
| AXNodeData button_1_1; |
| |
| root.id = 1; |
| paragraph_0.id = 2; |
| static_text_0_0.id = 3; |
| paragraph_1.id = 4; |
| link_1_0.id = 5; |
| static_text_1_0_0.id = 6; |
| button_1_1.id = 7; |
| |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {paragraph_0.id, paragraph_1.id}; |
| |
| paragraph_0.role = ax::mojom::Role::kParagraph; |
| paragraph_0.child_ids = {static_text_0_0.id}; |
| |
| static_text_0_0.role = ax::mojom::Role::kStaticText; |
| static_text_0_0.SetName("static_text_0_0"); |
| |
| paragraph_1.role = ax::mojom::Role::kParagraph; |
| paragraph_1.child_ids = {link_1_0.id, button_1_1.id}; |
| |
| link_1_0.role = ax::mojom::Role::kLink; |
| link_1_0.AddState(ax::mojom::State::kLinked); |
| link_1_0.child_ids = {static_text_1_0_0.id}; |
| |
| static_text_1_0_0.role = ax::mojom::Role::kStaticText; |
| static_text_1_0_0.SetName("static_text_1_0_0"); |
| |
| button_1_1.role = ax::mojom::Role::kButton; |
| button_1_1.SetName("button_1_1"); |
| |
| AXTreeUpdate initial_state; |
| initial_state.root_id = root.id; |
| initial_state.nodes = {root, paragraph_0, static_text_0_0, |
| paragraph_1, link_1_0, static_text_1_0_0, |
| button_1_1}; |
| initial_state.has_tree_data = true; |
| |
| AXTreeData tree_data; |
| tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| tree_data.title = "Application"; |
| initial_state.tree_data = tree_data; |
| |
| AXTree tree; |
| ASSERT_TRUE(tree.Unserialize(initial_state)) << tree.error(); |
| |
| AXNode* root_node = tree.GetFromId(root.id); |
| AXNode* paragraph_0_node = tree.GetFromId(paragraph_0.id); |
| AXNode* static_text_0_0_node = tree.GetFromId(static_text_0_0.id); |
| AXNode* paragraph_1_node = tree.GetFromId(paragraph_1.id); |
| AXNode* link_1_0_node = tree.GetFromId(link_1_0.id); |
| AXNode* static_text_1_0_0_node = tree.GetFromId(static_text_1_0_0.id); |
| AXNode* button_1_1_node = tree.GetFromId(button_1_1.id); |
| |
| // The lowest common ancestor of a node and itself is itself. |
| ASSERT_EQ(root_node, root_node->GetLowestCommonAncestor(*root_node)); |
| ASSERT_EQ(paragraph_0_node, |
| paragraph_0_node->GetLowestCommonAncestor(*paragraph_0_node)); |
| |
| // Lowest common ancestor is reflexive. |
| ASSERT_EQ(paragraph_1_node, |
| link_1_0_node->GetLowestCommonAncestor(*button_1_1_node)); |
| ASSERT_EQ(paragraph_1_node, |
| button_1_1_node->GetLowestCommonAncestor(*link_1_0_node)); |
| |
| // Finds the lowest common ancestor for two nodes not on the same level. |
| ASSERT_EQ(root_node, static_text_1_0_0_node->GetLowestCommonAncestor( |
| *static_text_0_0_node)); |
| ASSERT_EQ(link_1_0_node, |
| static_text_1_0_0_node->GetLowestCommonAncestor(*link_1_0_node)); |
| } |
| |
| TEST(AXNodeTest, DescendantOfNonAtomicTextField) { |
| // The test covers the different scenarios for descendants of a non-atomic |
| // text field: |
| // <div contenteditable role=spinbutton> |
| // <div role=spinbutton></div> |
| // <input type=text role=spinbutton> |
| // <div></div> |
| // </div> |
| // ++1 kRootWebArea |
| // ++++2 kSpinButton |
| // ++++++3 kSpinButton |
| // ++++++4 kSpinButton |
| // ++++++5 kGenericContainer |
| |
| AXNodeData root_1; |
| AXNodeData spin_button_2; |
| AXNodeData spin_button_3; |
| AXNodeData spin_button_4; |
| AXNodeData generic_container_5; |
| |
| root_1.id = 1; |
| spin_button_2.id = 2; |
| spin_button_3.id = 3; |
| spin_button_4.id = 4; |
| generic_container_5.id = 5; |
| |
| root_1.role = ax::mojom::Role::kRootWebArea; |
| root_1.child_ids = {spin_button_2.id}; |
| |
| spin_button_2.role = ax::mojom::Role::kSpinButton; |
| spin_button_2.AddState(ax::mojom::State::kRichlyEditable); |
| spin_button_2.AddState(ax::mojom::State::kEditable); |
| spin_button_2.AddBoolAttribute( |
| ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); |
| spin_button_2.child_ids = {spin_button_3.id, spin_button_4.id, |
| generic_container_5.id}; |
| |
| spin_button_3.role = ax::mojom::Role::kSpinButton; |
| |
| spin_button_4.role = ax::mojom::Role::kSpinButton; |
| spin_button_4.AddState(ax::mojom::State::kEditable); |
| |
| generic_container_5.role = ax::mojom::Role::kGenericContainer; |
| |
| AXTreeUpdate initial_state; |
| initial_state.root_id = root_1.id; |
| initial_state.nodes = {root_1, spin_button_2, spin_button_3, spin_button_4, |
| generic_container_5}; |
| initial_state.has_tree_data = true; |
| initial_state.tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); |
| |
| AXTree tree(initial_state); |
| |
| EXPECT_TRUE(tree.GetFromId(spin_button_2.id)->data().IsSpinnerTextField()); |
| EXPECT_TRUE(tree.GetFromId(spin_button_2.id)->data().IsTextField()); |
| EXPECT_FALSE( |
| tree.GetFromId(spin_button_3.id)->data().IsSpinnerTextField()); |
| EXPECT_FALSE(tree.GetFromId(spin_button_3.id)->data().IsTextField()); |
| EXPECT_TRUE(tree.GetFromId(spin_button_4.id)->data().IsSpinnerTextField()); |
| EXPECT_FALSE(tree.GetFromId(generic_container_5.id)->data().IsSpinnerTextField()); |
| } |
| |
| TEST(AXNodeTest, MenuItemCheckboxPosInSet) { |
| TestAXTreeUpdate update(std::string(R"HTML( |
| ++1 kRootWebArea |
| ++++2 kGroup |
| ++++++3 kMenuItemCheckBox |
| ++++++++4 kStaticText name="item" |
| ++++++++++5 kInlineTextBox name="item" |
| ++++++6 kMenuItemCheckBox |
| ++++++++7 kStaticText name="item2" |
| ++++++++++8 kInlineTextBox name="item2" |
| )HTML")); |
| |
| AXTree tree(update); |
| |
| EXPECT_EQ(tree.GetFromId(3)->GetPosInSet(), 1); |
| EXPECT_EQ(tree.GetFromId(3)->GetSetSize(), 2); |
| EXPECT_EQ(tree.GetFromId(6)->GetPosInSet(), 2); |
| EXPECT_EQ(tree.GetFromId(6)->GetSetSize(), 2); |
| } |
| |
| TEST(AXNodeTest, TreeItemAsTreeItemParentPosInSetSetSize) { |
| TestAXTreeUpdate update(std::string(R"HTML( |
| ++1 kRootWebArea |
| ++++2 kTree |
| ++++++3 kTreeItem intAttribute=kPosInSet,1 intAttribute=kSetSize,2 |
| ++++++++4 kTreeItem intAttribute=kPosInSet,1 intAttribute=kSetSize,6 |
| ++++++++5 kTreeItem intAttribute=kPosInSet,2 intAttribute=kSetSize,6 |
| ++++++6 kTreeItem intAttribute=kPosInSet,2 intAttribute=kSetSize,2 |
| ++++++++7 kTreeItem intAttribute=kPosInSet,3 intAttribute=kSetSize,6 |
| )HTML")); |
| |
| AXTree tree(update); |
| |
| EXPECT_EQ(tree.GetFromId(3)->GetPosInSet(), 1); |
| EXPECT_EQ(tree.GetFromId(3)->GetSetSize(), 2); |
| |
| EXPECT_EQ(tree.GetFromId(4)->GetPosInSet(), 1); |
| EXPECT_EQ(tree.GetFromId(4)->GetSetSize(), 6); |
| |
| EXPECT_EQ(tree.GetFromId(5)->GetPosInSet(), 2); |
| EXPECT_EQ(tree.GetFromId(5)->GetSetSize(), 6); |
| |
| EXPECT_EQ(tree.GetFromId(6)->GetPosInSet(), 2); |
| EXPECT_EQ(tree.GetFromId(6)->GetSetSize(), 2); |
| |
| EXPECT_EQ(tree.GetFromId(7)->GetPosInSet(), 3); |
| EXPECT_EQ(tree.GetFromId(7)->GetSetSize(), 6); |
| } |
| |
| TEST(AXNodeTest, GroupAsTreeItemParentPosInSetSetSize) { |
| TestAXTreeUpdate update(std::string(R"HTML( |
| ++1 kRootWebArea |
| ++++2 kTree |
| ++++++3 kGroup |
| ++++++++4 kTreeItem intAttribute=kPosInSet,1 intAttribute=kSetSize,6 |
| ++++++++5 kTreeItem intAttribute=kPosInSet,2 intAttribute=kSetSize,6 |
| ++++++6 kTreeItem |
| ++++++++7 kGroup |
| ++++++++++8 kTreeItem intAttribute=kPosInSet,1 intAttribute=kSetSize,6 |
| )HTML")); |
| |
| AXTree tree(update); |
| |
| EXPECT_EQ(tree.GetFromId(4)->GetPosInSet(), 1); |
| EXPECT_EQ(tree.GetFromId(4)->GetSetSize(), 6); |
| |
| EXPECT_EQ(tree.GetFromId(5)->GetPosInSet(), 2); |
| EXPECT_EQ(tree.GetFromId(5)->GetSetSize(), 6); |
| |
| EXPECT_EQ(tree.GetFromId(8)->GetPosInSet(), 1); |
| EXPECT_EQ(tree.GetFromId(8)->GetSetSize(), 6); |
| } |
| |
| } // namespace ui |