blob: 934922bd70a41d272b048cd887f583652e44a109 [file] [log] [blame]
// 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