// Copyright 2021 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_node.h" #include #include #include #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.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_manager.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->GetDeepestFirstChild()->id()); EXPECT_EQ(paragraph_0.id, root_node->GetDeepestFirstUnignoredChild()->id()); EXPECT_EQ(button_3_1.id, root_node->GetDeepestLastChild()->id()); EXPECT_EQ(button_3_1.id, root_node->GetDeepestLastUnignoredChild()->id()); { std::vector siblings; for (const 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 siblings; for (const 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 siblings; for (const 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 siblings; for (const 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 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 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 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 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(initial_state_1); TestAXTreeManager tree_manager_1(std::move(tree_1)); auto tree_2 = std::make_unique(initial_state_2); TestAXTreeManager tree_manager_2(std::move(tree_2)); const AXNode* root_node_1 = tree_manager_1.GetRootAsAXNode(); ASSERT_EQ(root_1.id, root_node_1->id()); const AXNode* root_node_2 = tree_manager_2.GetRootAsAXNode(); 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) { // kRootWebArea // ++kTextField (contenteditable) // ++++kGenericContainer // ++++++kStaticText "Line 1" // ++++++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_line_break; rich_text_field_line_break.id = 5; AXNodeData rich_text_field_line_2; rich_text_field_line_2.id = 6; 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_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_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_line_break, rich_text_field_line_2}; AXTree tree(update); { const AXNode* text_field_node = tree.GetFromId(rich_text_field.id); ASSERT_NE(nullptr, text_field_node); EXPECT_EQ("Line 1\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 or //