| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/accessibility/browser_accessibility.h" |
| |
| #include "build/build_config.h" |
| #include "content/browser/accessibility/browser_accessibility_manager.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/accessibility/ax_enums.mojom-shared.h" |
| #include "ui/accessibility/ax_node_position.h" |
| #include "ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "content/browser/accessibility/browser_accessibility_manager_android.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| BrowserAccessibilityManager* CreateBrowserAccessibilityManager( |
| const ui::AXTreeUpdate& initial_tree, |
| ui::AXPlatformTreeManagerDelegate* delegate) { |
| #if BUILDFLAG(IS_ANDROID) |
| return content::BrowserAccessibilityManagerAndroid::Create(initial_tree, |
| delegate); |
| #else |
| return content::BrowserAccessibilityManager::Create(initial_tree, delegate); |
| #endif |
| } |
| } // namespace |
| |
| using RetargetEventType = ui::AXTreeManager::RetargetEventType; |
| |
| class BrowserAccessibilityTest : public ::testing::Test { |
| public: |
| BrowserAccessibilityTest(); |
| |
| BrowserAccessibilityTest(const BrowserAccessibilityTest&) = delete; |
| BrowserAccessibilityTest& operator=(const BrowserAccessibilityTest&) = delete; |
| |
| ~BrowserAccessibilityTest() override; |
| |
| protected: |
| std::unique_ptr<ui::TestAXPlatformTreeManagerDelegate> |
| test_browser_accessibility_delegate_; |
| |
| private: |
| void SetUp() override; |
| |
| BrowserTaskEnvironment task_environment_; |
| }; |
| |
| BrowserAccessibilityTest::BrowserAccessibilityTest() {} |
| |
| BrowserAccessibilityTest::~BrowserAccessibilityTest() = default; |
| |
| void BrowserAccessibilityTest::SetUp() { |
| test_browser_accessibility_delegate_ = |
| std::make_unique<ui::TestAXPlatformTreeManagerDelegate>(); |
| } |
| |
| TEST_F(BrowserAccessibilityTest, TestCanFireEvents) { |
| ui::AXNodeData text1; |
| text1.id = 111; |
| text1.role = ax::mojom::Role::kStaticText; |
| text1.SetName("One two three."); |
| |
| ui::AXNodeData para1; |
| para1.id = 11; |
| para1.role = ax::mojom::Role::kParagraph; |
| para1.child_ids.push_back(text1.id); |
| |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids.push_back(para1.id); |
| |
| std::unique_ptr<BrowserAccessibilityManager> manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, para1, text1), |
| test_browser_accessibility_delegate_.get())); |
| |
| BrowserAccessibility* root_obj = manager->GetBrowserAccessibilityRoot(); |
| EXPECT_FALSE(root_obj->IsLeaf()); |
| EXPECT_TRUE(root_obj->CanFireEvents()); |
| |
| BrowserAccessibility* para_obj = root_obj->PlatformGetChild(0); |
| EXPECT_TRUE(para_obj->CanFireEvents()); |
| #if BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(para_obj->IsLeaf()); |
| #else |
| EXPECT_FALSE(para_obj->IsLeaf()); |
| #endif |
| |
| BrowserAccessibility* text_obj = manager->GetFromID(111); |
| EXPECT_TRUE(text_obj->IsLeaf()); |
| #if !BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(text_obj->CanFireEvents()); |
| #endif |
| BrowserAccessibility* retarget = |
| manager->RetargetBrowserAccessibilityForEvents( |
| text_obj, RetargetEventType::RetargetEventTypeBlinkHover); |
| EXPECT_TRUE(retarget->CanFireEvents()); |
| |
| manager.reset(); |
| } |
| |
| #if BUILDFLAG(IS_WIN) || BUILDFLAG(USE_ATK) |
| TEST_F(BrowserAccessibilityTest, PlatformChildIterator) { |
| // (i) => node is ignored |
| // Parent Tree |
| // 1 |
| // |__________ |
| // | | | |
| // 2(i) 3 4 |
| // |__________________________________ |
| // | | | | |
| // 5 6 7(i) 8(i) |
| // | | |________ |
| // | | | | |
| // Child Tree 9(i) 10(i) 11 |
| // | |____ |
| // | | | |
| // 12(i) 13 14 |
| // Child Tree |
| // 1 |
| // |_________ |
| // | | | |
| // 2 3 4 |
| // | |
| // 5 |
| ui::AXTreeID parent_tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| ui::AXTreeID child_tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| |
| ui::AXTreeUpdate parent_tree_update; |
| parent_tree_update.tree_data.tree_id = parent_tree_id; |
| parent_tree_update.has_tree_data = true; |
| parent_tree_update.root_id = 1; |
| parent_tree_update.nodes.resize(14); |
| parent_tree_update.nodes[0].id = 1; |
| parent_tree_update.nodes[0].child_ids = {2, 3, 4}; |
| |
| parent_tree_update.nodes[1].id = 2; |
| parent_tree_update.nodes[1].child_ids = {5, 6, 7, 8}; |
| parent_tree_update.nodes[1].AddState(ax::mojom::State::kIgnored); |
| |
| parent_tree_update.nodes[2].id = 3; |
| parent_tree_update.nodes[3].id = 4; |
| |
| parent_tree_update.nodes[4].id = 5; |
| parent_tree_update.nodes[4].AddChildTreeId(child_tree_id); |
| |
| parent_tree_update.nodes[5].id = 6; |
| parent_tree_update.nodes[5].child_ids = {9}; |
| |
| parent_tree_update.nodes[6].id = 7; |
| parent_tree_update.nodes[6].child_ids = {10, 11}; |
| parent_tree_update.nodes[6].AddState(ax::mojom::State::kIgnored); |
| |
| parent_tree_update.nodes[7].id = 8; |
| parent_tree_update.nodes[7].AddState(ax::mojom::State::kIgnored); |
| |
| parent_tree_update.nodes[8].id = 9; |
| parent_tree_update.nodes[8].child_ids = {12}; |
| parent_tree_update.nodes[8].AddState(ax::mojom::State::kIgnored); |
| |
| parent_tree_update.nodes[9].id = 10; |
| parent_tree_update.nodes[9].child_ids = {13, 14}; |
| parent_tree_update.nodes[9].AddState(ax::mojom::State::kIgnored); |
| |
| parent_tree_update.nodes[10].id = 11; |
| |
| parent_tree_update.nodes[11].id = 12; |
| parent_tree_update.nodes[11].AddState(ax::mojom::State::kIgnored); |
| |
| parent_tree_update.nodes[12].id = 13; |
| |
| parent_tree_update.nodes[13].id = 14; |
| |
| ui::AXTreeUpdate child_tree_update; |
| child_tree_update.tree_data.tree_id = child_tree_id; |
| child_tree_update.tree_data.parent_tree_id = parent_tree_id; |
| child_tree_update.has_tree_data = true; |
| child_tree_update.root_id = 1; |
| child_tree_update.nodes.resize(5); |
| child_tree_update.nodes[0].id = 1; |
| child_tree_update.nodes[0].child_ids = {2, 3, 4}; |
| |
| child_tree_update.nodes[1].id = 2; |
| |
| child_tree_update.nodes[2].id = 3; |
| child_tree_update.nodes[2].child_ids = {5}; |
| |
| child_tree_update.nodes[3].id = 4; |
| |
| child_tree_update.nodes[4].id = 5; |
| |
| std::unique_ptr<BrowserAccessibilityManager> parent_manager( |
| CreateBrowserAccessibilityManager(parent_tree_update, nullptr)); |
| |
| std::unique_ptr<BrowserAccessibilityManager> child_manager( |
| CreateBrowserAccessibilityManager(child_tree_update, nullptr)); |
| |
| BrowserAccessibility* root_obj = |
| parent_manager->GetBrowserAccessibilityRoot(); |
| // Test traversal |
| // PlatformChildren(root_obj) = {5, 6, 13, 15, 11, 3, 4} |
| BrowserAccessibility::PlatformChildIterator platform_iterator = |
| root_obj->PlatformChildrenBegin(); |
| EXPECT_EQ(5, platform_iterator->GetId()); |
| EXPECT_EQ(nullptr, platform_iterator->PlatformGetPreviousSibling()); |
| EXPECT_EQ(1u, platform_iterator->PlatformChildCount()); |
| |
| // Test Child-Tree Traversal |
| BrowserAccessibility* child_tree_root = |
| platform_iterator->PlatformGetFirstChild(); |
| EXPECT_EQ(1, child_tree_root->GetId()); |
| BrowserAccessibility::PlatformChildIterator child_tree_iterator = |
| child_tree_root->PlatformChildrenBegin(); |
| |
| EXPECT_EQ(2, child_tree_iterator->GetId()); |
| ++child_tree_iterator; |
| EXPECT_EQ(3, child_tree_iterator->GetId()); |
| ++child_tree_iterator; |
| EXPECT_EQ(4, child_tree_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(6, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(13, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(14, platform_iterator->GetId()); |
| |
| --platform_iterator; |
| EXPECT_EQ(13, platform_iterator->GetId()); |
| |
| --platform_iterator; |
| EXPECT_EQ(6, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(13, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(14, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(11, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(3, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(4, platform_iterator->GetId()); |
| |
| ++platform_iterator; |
| EXPECT_EQ(root_obj->PlatformChildrenEnd(), platform_iterator); |
| |
| // test empty list |
| // PlatformChildren(3) = {} |
| BrowserAccessibility* node2 = parent_manager->GetFromID(3); |
| platform_iterator = node2->PlatformChildrenBegin(); |
| EXPECT_EQ(node2->PlatformChildrenEnd(), platform_iterator); |
| |
| // empty list from ignored node |
| // PlatformChildren(8) = {} |
| BrowserAccessibility* node8 = parent_manager->GetFromID(8); |
| platform_iterator = node8->PlatformChildrenBegin(); |
| EXPECT_EQ(node8->PlatformChildrenEnd(), platform_iterator); |
| |
| // non-empty list from ignored node |
| // PlatformChildren(10) = {13, 15} |
| BrowserAccessibility* node10 = parent_manager->GetFromID(10); |
| platform_iterator = node10->PlatformChildrenBegin(); |
| EXPECT_EQ(13, platform_iterator->GetId()); |
| |
| // Two UnignoredChildIterators from the same parent at the same position |
| // should be equivalent, even in end position. |
| platform_iterator = root_obj->PlatformChildrenBegin(); |
| BrowserAccessibility::PlatformChildIterator platform_iterator2 = |
| root_obj->PlatformChildrenBegin(); |
| auto end = root_obj->PlatformChildrenEnd(); |
| while (platform_iterator != end) { |
| ASSERT_EQ(platform_iterator, platform_iterator2); |
| ++platform_iterator; |
| ++platform_iterator2; |
| } |
| ASSERT_EQ(platform_iterator, platform_iterator2); |
| } |
| #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(USE_ATK) |
| |
| TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRect) { |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); |
| |
| ui::AXNodeData static_text; |
| static_text.id = 2; |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("Hello, world."); |
| static_text.relative_bounds.bounds = gfx::RectF(100, 100, 29, 18); |
| root.child_ids.push_back(2); |
| |
| ui::AXNodeData inline_text1; |
| inline_text1.id = 3; |
| inline_text1.role = ax::mojom::Role::kInlineTextBox; |
| inline_text1.SetName("Hello, "); |
| inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 29, 9); |
| inline_text1.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| std::vector<int32_t> character_offsets1; |
| character_offsets1.push_back(6); |
| character_offsets1.push_back(11); |
| character_offsets1.push_back(16); |
| character_offsets1.push_back(21); |
| character_offsets1.push_back(26); |
| character_offsets1.push_back(29); |
| character_offsets1.push_back(29); |
| inline_text1.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets1); |
| static_text.child_ids.push_back(3); |
| |
| ui::AXNodeData inline_text2; |
| inline_text2.id = 4; |
| inline_text2.role = ax::mojom::Role::kInlineTextBox; |
| inline_text2.SetName("world."); |
| inline_text2.relative_bounds.bounds = gfx::RectF(100, 109, 28, 9); |
| inline_text2.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| std::vector<int32_t> character_offsets2; |
| character_offsets2.push_back(5); |
| character_offsets2.push_back(10); |
| character_offsets2.push_back(15); |
| character_offsets2.push_back(20); |
| character_offsets2.push_back(25); |
| character_offsets2.push_back(28); |
| inline_text2.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets2); |
| static_text.child_ids.push_back(4); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, static_text, inline_text1, |
| inline_text2), |
| test_browser_accessibility_delegate_.get())); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| BrowserAccessibility* static_text_accessible = |
| root_accessible->PlatformGetChild(0); |
| ASSERT_NE(nullptr, static_text_accessible); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Android disallows getting inner text from root accessibility nodes. |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 1, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| #else |
| // Validate the bounding box of 'H' from root. |
| EXPECT_EQ(gfx::Rect(100, 100, 6, 9).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 1, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| #endif |
| |
| // Validate the bounding box of 'H' from static text. |
| EXPECT_EQ(gfx::Rect(100, 100, 6, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 1, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Validate the bounding box of 'Hello' from static text. |
| EXPECT_EQ(gfx::Rect(100, 100, 26, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 5, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Validate the bounding box of 'Hello, world.' from static text. |
| EXPECT_EQ(gfx::Rect(100, 100, 29, 18).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 13, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Android disallows getting inner text from root accessibility nodes. |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 13, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| #else |
| // Validate the bounding box of 'Hello, world.' from root. |
| EXPECT_EQ(gfx::Rect(100, 100, 29, 18).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 13, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| #endif |
| } |
| |
| TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectPlainTextField) { |
| // Text area with 'Hello' text |
| // rootWebArea |
| // ++textField |
| // ++++genericContainer |
| // ++++++staticText |
| // ++++++++inlineTextBox |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); |
| |
| ui::AXNodeData textarea; |
| textarea.id = 2; |
| textarea.role = ax::mojom::Role::kTextField; |
| textarea.SetValue("Hello"); |
| textarea.relative_bounds.bounds = gfx::RectF(100, 100, 150, 20); |
| root.child_ids.push_back(2); |
| |
| ui::AXNodeData container; |
| container.id = 3; |
| container.role = ax::mojom::Role::kGenericContainer; |
| container.relative_bounds.bounds = textarea.relative_bounds.bounds; |
| textarea.child_ids.push_back(3); |
| |
| ui::AXNodeData static_text; |
| static_text.id = 4; |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("Hello"); |
| static_text.relative_bounds.bounds = gfx::RectF(100, 100, 50, 10); |
| container.child_ids.push_back(4); |
| |
| ui::AXNodeData inline_text1; |
| inline_text1.id = 5; |
| inline_text1.role = ax::mojom::Role::kInlineTextBox; |
| inline_text1.SetName("Hello"); |
| inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 50, 10); |
| inline_text1.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, {10, 20, 30, 40, 50}); |
| |
| inline_text1.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| static_text.child_ids.push_back(5); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, textarea, container, static_text, |
| inline_text1), |
| test_browser_accessibility_delegate_.get())); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| BrowserAccessibility* textarea_accessible = |
| root_accessible->PlatformGetChild(0); |
| ASSERT_NE(nullptr, textarea_accessible); |
| |
| // Validate the bounds of 'ell'. |
| EXPECT_EQ(gfx::Rect(110, 100, 30, 10), |
| textarea_accessible->GetInnerTextRangeBoundsRect( |
| 1, 4, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped)); |
| } |
| |
| TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectMultiElement) { |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); |
| |
| ui::AXNodeData static_text; |
| static_text.id = 2; |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("ABC"); |
| static_text.relative_bounds.bounds = gfx::RectF(0, 20, 33, 9); |
| root.child_ids.push_back(2); |
| |
| ui::AXNodeData inline_text1; |
| inline_text1.id = 3; |
| inline_text1.role = ax::mojom::Role::kInlineTextBox; |
| inline_text1.SetName("ABC"); |
| inline_text1.relative_bounds.bounds = gfx::RectF(0, 20, 33, 9); |
| inline_text1.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| std::vector<int32_t> character_offsets{10, 21, 33}; |
| inline_text1.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets); |
| static_text.child_ids.push_back(3); |
| |
| ui::AXNodeData static_text2; |
| static_text2.id = 4; |
| static_text2.role = ax::mojom::Role::kStaticText; |
| static_text2.SetName("ABC"); |
| static_text2.relative_bounds.bounds = gfx::RectF(10, 40, 33, 9); |
| root.child_ids.push_back(4); |
| |
| ui::AXNodeData inline_text2; |
| inline_text2.id = 5; |
| inline_text2.role = ax::mojom::Role::kInlineTextBox; |
| inline_text2.SetName("ABC"); |
| inline_text2.relative_bounds.bounds = gfx::RectF(10, 40, 33, 9); |
| inline_text2.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| inline_text2.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets); |
| static_text2.child_ids.push_back(5); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, static_text, inline_text1, |
| static_text2, inline_text2), |
| test_browser_accessibility_delegate_.get())); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| BrowserAccessibility* static_text_accessible = |
| root_accessible->PlatformGetChild(0); |
| ASSERT_NE(nullptr, static_text_accessible); |
| BrowserAccessibility* static_text_accessible2 = |
| root_accessible->PlatformGetChild(1); |
| ASSERT_NE(nullptr, static_text_accessible); |
| |
| // Validate the bounds of 'ABC' on the first line. |
| EXPECT_EQ(gfx::Rect(0, 20, 33, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 3, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Validate the bounds of only 'AB' on the first line. |
| EXPECT_EQ(gfx::Rect(0, 20, 21, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 2, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Validate the bounds of only 'BC' on the first line. |
| EXPECT_EQ(gfx::Rect(10, 20, 23, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 1, 3, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Validate the bounds of 'ABC' on the second line. |
| EXPECT_EQ(gfx::Rect(10, 40, 33, 9).ToString(), |
| static_text_accessible2 |
| ->GetInnerTextRangeBoundsRect( |
| 0, 3, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Android disallows getting inner text from accessibility root nodes. |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 6, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Android disallows getting inner text from accessibility root nodes. |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 2, 4, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| #else |
| // Validate the bounds of 'ABCABC' from both lines. |
| EXPECT_EQ(gfx::Rect(0, 20, 43, 29).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 6, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // Validate the bounds of 'CA' from both lines. |
| EXPECT_EQ(gfx::Rect(10, 20, 23, 29).ToString(), |
| root_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 2, 4, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| #endif |
| } |
| |
| TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectBiDi) { |
| // In this example, we assume that the string "123abc" is rendered with "123" |
| // going left-to-right and "abc" going right-to-left. In other words, |
| // on-screen it would look like "123cba". |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); |
| |
| ui::AXNodeData static_text; |
| static_text.id = 2; |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("123abc"); |
| static_text.relative_bounds.bounds = gfx::RectF(100, 100, 60, 20); |
| root.child_ids.push_back(2); |
| |
| ui::AXNodeData inline_text1; |
| inline_text1.id = 3; |
| inline_text1.role = ax::mojom::Role::kInlineTextBox; |
| inline_text1.SetName("123"); |
| inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 30, 20); |
| inline_text1.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| std::vector<int32_t> character_offsets1; |
| character_offsets1.push_back(10); // 0 |
| character_offsets1.push_back(20); // 1 |
| character_offsets1.push_back(30); // 2 |
| inline_text1.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets1); |
| static_text.child_ids.push_back(3); |
| |
| ui::AXNodeData inline_text2; |
| inline_text2.id = 4; |
| inline_text2.role = ax::mojom::Role::kInlineTextBox; |
| inline_text2.SetName("abc"); |
| inline_text2.relative_bounds.bounds = gfx::RectF(130, 100, 30, 20); |
| inline_text2.SetTextDirection(ax::mojom::WritingDirection::kRtl); |
| std::vector<int32_t> character_offsets2; |
| character_offsets2.push_back(10); |
| character_offsets2.push_back(20); |
| character_offsets2.push_back(30); |
| inline_text2.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets2); |
| static_text.child_ids.push_back(4); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, static_text, inline_text1, |
| inline_text2), |
| test_browser_accessibility_delegate_.get())); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| BrowserAccessibility* static_text_accessible = |
| root_accessible->PlatformGetChild(0); |
| ASSERT_NE(nullptr, static_text_accessible); |
| |
| EXPECT_EQ(gfx::Rect(100, 100, 60, 20).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 6, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| EXPECT_EQ(gfx::Rect(100, 100, 10, 20).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 1, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| EXPECT_EQ(gfx::Rect(100, 100, 30, 20).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 3, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| EXPECT_EQ(gfx::Rect(150, 100, 10, 20).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 3, 4, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| EXPECT_EQ(gfx::Rect(130, 100, 30, 20).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 3, 6, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| |
| // This range is only two characters, but because of the direction switch |
| // the bounds are as wide as four characters. |
| EXPECT_EQ(gfx::Rect(120, 100, 40, 20).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 2, 4, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| } |
| |
| TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectScrolledWindow) { |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.AddIntAttribute(ax::mojom::IntAttribute::kScrollX, 25); |
| root.AddIntAttribute(ax::mojom::IntAttribute::kScrollY, 50); |
| root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); |
| |
| ui::AXNodeData static_text; |
| static_text.id = 2; |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("ABC"); |
| static_text.relative_bounds.bounds = gfx::RectF(100, 100, 16, 9); |
| root.child_ids.push_back(2); |
| |
| ui::AXNodeData inline_text; |
| inline_text.id = 3; |
| inline_text.role = ax::mojom::Role::kInlineTextBox; |
| inline_text.SetName("ABC"); |
| inline_text.relative_bounds.bounds = gfx::RectF(100, 100, 16, 9); |
| inline_text.SetTextDirection(ax::mojom::WritingDirection::kLtr); |
| std::vector<int32_t> character_offsets1; |
| character_offsets1.push_back(6); // 0 |
| character_offsets1.push_back(11); // 1 |
| character_offsets1.push_back(16); // 2 |
| inline_text.AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets1); |
| static_text.child_ids.push_back(3); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, static_text, inline_text), |
| test_browser_accessibility_delegate_.get())); |
| |
| browser_accessibility_manager |
| ->SetUseRootScrollOffsetsWhenComputingBoundsForTesting(true); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| BrowserAccessibility* static_text_accessible = |
| root_accessible->PlatformGetChild(0); |
| ASSERT_NE(nullptr, static_text_accessible); |
| |
| if (browser_accessibility_manager |
| ->UseRootScrollOffsetsWhenComputingBounds()) { |
| EXPECT_EQ(gfx::Rect(75, 50, 16, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 3, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| } else { |
| EXPECT_EQ(gfx::Rect(100, 100, 16, 9).ToString(), |
| static_text_accessible |
| ->GetInnerTextRangeBoundsRect( |
| 0, 3, ui::AXCoordinateSystem::kRootFrame, |
| ui::AXClippingBehavior::kUnclipped) |
| .ToString()); |
| } |
| } |
| |
| TEST_F(BrowserAccessibilityTest, GetAuthorUniqueId) { |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.AddStringAttribute(ax::mojom::StringAttribute::kHtmlId, "my_html_id"); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root), |
| test_browser_accessibility_delegate_.get())); |
| ASSERT_NE(nullptr, browser_accessibility_manager.get()); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| |
| ASSERT_EQ(u"my_html_id", root_accessible->GetAuthorUniqueId()); |
| } |
| |
| TEST_F(BrowserAccessibilityTest, NextWordPositionWithHypertext) { |
| // Build a tree simulating an INPUT control with placeholder text. |
| ui::AXNodeData root; |
| root.id = 1; |
| ui::AXNodeData input; |
| input.id = 2; |
| ui::AXNodeData text_container; |
| text_container.id = 3; |
| ui::AXNodeData static_text; |
| static_text.id = 4; |
| ui::AXNodeData inline_text; |
| inline_text.id = 5; |
| |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {input.id}; |
| |
| input.role = ax::mojom::Role::kTextField; |
| input.AddState(ax::mojom::State::kEditable); |
| input.SetName("Search the web"); |
| input.child_ids = {text_container.id}; |
| |
| text_container.role = ax::mojom::Role::kGenericContainer; |
| text_container.child_ids = {static_text.id}; |
| |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("Search the web"); |
| static_text.child_ids = {inline_text.id}; |
| |
| inline_text.role = ax::mojom::Role::kInlineTextBox; |
| inline_text.SetName("Search the web"); |
| inline_text.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, |
| {0, 7, 11}); |
| inline_text.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, |
| {6, 10, 14}); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, input, text_container, static_text, |
| inline_text), |
| test_browser_accessibility_delegate_.get())); |
| ASSERT_NE(nullptr, browser_accessibility_manager.get()); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| ASSERT_NE(0u, root_accessible->InternalChildCount()); |
| BrowserAccessibility* input_accessible = root_accessible->InternalGetChild(0); |
| ASSERT_NE(nullptr, input_accessible); |
| |
| // Create a text position at offset 0 in the input control |
| BrowserAccessibility::AXPosition position = |
| input_accessible->CreateTextPositionAt(0); |
| |
| // On platforms that expose IA2 or ATK hypertext, moving by word should work |
| // the same as if the value of the text field is equal to the placeholder |
| // text. |
| // |
| // This is because visually the placeholder text appears in the text field in |
| // the same location as its value, and the user should be able to read it |
| // using standard screen reader commands, such as "read current word" and |
| // "read current line". Only once the user starts typing should the |
| // placeholder disappear. |
| |
| BrowserAccessibility::AXPosition next_word_start = |
| position->CreateNextWordStartPosition( |
| {ui::AXBoundaryBehavior::kCrossBoundary, |
| ui::AXBoundaryDetection::kDontCheckInitialPosition}); |
| if (position->MaxTextOffset() == 0) { |
| EXPECT_TRUE(next_word_start->IsNullPosition()); |
| } else { |
| EXPECT_EQ( |
| "TextPosition anchor_id=2 text_offset=7 affinity=downstream " |
| "annotated_text=Search <t>he web", |
| next_word_start->ToString()); |
| } |
| |
| BrowserAccessibility::AXPosition next_word_end = |
| position->CreateNextWordEndPosition( |
| {ui::AXBoundaryBehavior::kCrossBoundary, |
| ui::AXBoundaryDetection::kDontCheckInitialPosition}); |
| if (position->MaxTextOffset() == 0) { |
| EXPECT_TRUE(next_word_end->IsNullPosition()); |
| } else { |
| EXPECT_EQ( |
| "TextPosition anchor_id=2 text_offset=6 affinity=downstream " |
| "annotated_text=Search< >the web", |
| next_word_end->ToString()); |
| } |
| } |
| |
| // Tests that the browser manager retrieves the name from the root node of the |
| // child subtree for portals. |
| TEST_F(BrowserAccessibilityTest, PortalName) { |
| ui::AXTreeID parent_tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| ui::AXTreeID child_tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| |
| ui::AXTreeUpdate parent_tree_update; |
| parent_tree_update.tree_data.tree_id = parent_tree_id; |
| parent_tree_update.has_tree_data = true; |
| parent_tree_update.root_id = 1; |
| parent_tree_update.nodes.resize(1); |
| |
| parent_tree_update.nodes[0].id = 1; |
| parent_tree_update.nodes[0].role = ax::mojom::Role::kPortal; |
| parent_tree_update.nodes[0].AddChildTreeId(child_tree_id); |
| |
| ui::AXTreeUpdate child_tree_update; |
| child_tree_update.tree_data.tree_id = child_tree_id; |
| child_tree_update.tree_data.parent_tree_id = parent_tree_id; |
| child_tree_update.has_tree_data = true; |
| child_tree_update.root_id = 1; |
| child_tree_update.nodes.resize(1); |
| |
| child_tree_update.nodes[0].id = 1; |
| child_tree_update.nodes[0].role = ax::mojom::Role::kRootWebArea; |
| child_tree_update.nodes[0].AddStringAttribute( |
| ax::mojom::StringAttribute::kName, "name"); |
| |
| std::unique_ptr<BrowserAccessibilityManager> parent_manager( |
| CreateBrowserAccessibilityManager(parent_tree_update, nullptr)); |
| |
| std::unique_ptr<BrowserAccessibilityManager> child_manager( |
| CreateBrowserAccessibilityManager(child_tree_update, nullptr)); |
| |
| // Portal node should use name from root of child tree. |
| EXPECT_EQ("name", child_manager->GetBrowserAccessibilityRoot()->GetName()); |
| EXPECT_EQ("name", parent_manager->GetBrowserAccessibilityRoot()->GetName()); |
| |
| // Explicitly add name to portal node. |
| parent_tree_update.nodes[0].AddStringAttribute( |
| ax::mojom::StringAttribute::kName, "name2"); |
| parent_tree_update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kAttribute); |
| parent_manager->Initialize(parent_tree_update); |
| |
| // Portal node should now use name from attribute. |
| EXPECT_EQ("name", child_manager->GetBrowserAccessibilityRoot()->GetName()); |
| EXPECT_EQ("name2", parent_manager->GetBrowserAccessibilityRoot()->GetName()); |
| } |
| |
| TEST_F(BrowserAccessibilityTest, GetIndexInParent) { |
| ui::AXNodeData root; |
| root.id = 1; |
| root.role = ax::mojom::Role::kRootWebArea; |
| root.child_ids = {2}; |
| |
| ui::AXNodeData static_text; |
| static_text.id = 2; |
| static_text.role = ax::mojom::Role::kStaticText; |
| static_text.SetName("ABC"); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root, static_text), |
| test_browser_accessibility_delegate_.get())); |
| ASSERT_NE(nullptr, browser_accessibility_manager.get()); |
| |
| BrowserAccessibility* root_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot(); |
| ASSERT_NE(nullptr, root_accessible); |
| // Should be nullopt for kRootWebArea since it doesn't have a calculated |
| // index. |
| EXPECT_FALSE(root_accessible->GetIndexInParent().has_value()); |
| BrowserAccessibility* child_accessible = root_accessible->InternalGetChild(0); |
| ASSERT_NE(nullptr, child_accessible); |
| // Returns the index calculated in AXNode. |
| EXPECT_EQ(0u, child_accessible->GetIndexInParent()); |
| } |
| |
| TEST_F(BrowserAccessibilityTest, CreatePositionAt) { |
| ui::AXNodeData root_1; |
| root_1.id = 1; |
| root_1.role = ax::mojom::Role::kRootWebArea; |
| root_1.child_ids = {2}; |
| |
| ui::AXNodeData gc_2; |
| gc_2.id = 2; |
| gc_2.role = ax::mojom::Role::kGenericContainer; |
| gc_2.child_ids = {3}; |
| |
| ui::AXNodeData text_3; |
| text_3.id = 3; |
| text_3.role = ax::mojom::Role::kStaticText; |
| text_3.SetName("text"); |
| |
| std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( |
| CreateBrowserAccessibilityManager( |
| MakeAXTreeUpdateForTesting(root_1, gc_2, text_3), |
| test_browser_accessibility_delegate_.get())); |
| ASSERT_NE(nullptr, browser_accessibility_manager.get()); |
| |
| BrowserAccessibility* gc_accessible = |
| browser_accessibility_manager->GetBrowserAccessibilityRoot() |
| ->PlatformGetChild(0); |
| ASSERT_NE(nullptr, gc_accessible); |
| |
| BrowserAccessibility::AXPosition pos = gc_accessible->CreatePositionAt(0); |
| EXPECT_TRUE(pos->IsTreePosition()); |
| |
| ASSERT_EQ(1U, gc_accessible->InternalChildCount()); |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android, nodes with only static text can drop their children. |
| ASSERT_EQ(0U, gc_accessible->PlatformChildCount()); |
| #else |
| ASSERT_EQ(1U, gc_accessible->PlatformChildCount()); |
| BrowserAccessibility* text_accessible = gc_accessible->PlatformGetChild(0); |
| ASSERT_NE(nullptr, text_accessible); |
| |
| pos = text_accessible->CreatePositionAt(0); |
| EXPECT_TRUE(pos->IsTextPosition()); |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| } // namespace content |