blob: 233cf7fab396b8a707efb42953fe7db98ac47324 [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 "services/accessibility/android/ax_tree_source_android.h"
#include <memory>
#include <optional>
#include <utility>
#include "base/memory/raw_ptr.h"
#include "extensions/browser/api/automation_internal/automation_event_router.h"
#include "services/accessibility/android/accessibility_node_info_data_wrapper.h"
#include "services/accessibility/android/accessibility_window_info_data_wrapper.h"
#include "services/accessibility/android/android_accessibility_util.h"
#include "services/accessibility/android/public/mojom/accessibility_helper.mojom.h"
#include "services/accessibility/android/test/android_accessibility_test_util.h"
#include "testing/gtest/include/gtest/gtest-death-test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/platform/ax_android_constants.h"
namespace ax::android {
using AXActionType = mojom::AccessibilityActionType;
using AXBooleanProperty = mojom::AccessibilityBooleanProperty;
using AXCollectionInfoData = mojom::AccessibilityCollectionInfoData;
using AXCollectionItemInfoData = mojom::AccessibilityCollectionItemInfoData;
using AXEventData = mojom::AccessibilityEventData;
using AXEventIntListProperty = mojom::AccessibilityEventIntListProperty;
using AXEventIntProperty = mojom::AccessibilityEventIntProperty;
using AXEventType = mojom::AccessibilityEventType;
using AXIntListProperty = mojom::AccessibilityIntListProperty;
using AXIntProperty = mojom::AccessibilityIntProperty;
using AXNodeInfoData = mojom::AccessibilityNodeInfoData;
using AXRangeInfoData = mojom::AccessibilityRangeInfoData;
using AXStringProperty = mojom::AccessibilityStringProperty;
using AXWindowBooleanProperty = mojom::AccessibilityWindowBooleanProperty;
using AXWindowInfoData = mojom::AccessibilityWindowInfoData;
using AXWindowIntProperty = mojom::AccessibilityWindowIntProperty;
using AXWindowIntListProperty = mojom::AccessibilityWindowIntListProperty;
using AXWindowStringProperty = mojom::AccessibilityWindowStringProperty;
namespace {
class MockAutomationEventRouter
: public extensions::AutomationEventRouterInterface {
public:
MockAutomationEventRouter() = default;
virtual ~MockAutomationEventRouter() = default;
ui::AXTree* tree() { return &tree_; }
// extensions::AutomationEventRouterInterface:
void DispatchAccessibilityEvents(
const ui::AXTreeID& tree_id,
const std::vector<ui::AXTreeUpdate>& updates,
const gfx::Point& mouse_location,
const std::vector<ui::AXEvent>& events) override {
for (auto&& event : events) {
ASSERT_NE(event.event_type, ax::mojom::Event::kNone);
event_count_[event.event_type]++;
}
if (events.size() == 0) {
// In order to validate a case where |events| is empty:
event_count_[ax::mojom::Event::kNone]++;
}
last_dispatched_events_ = std::move(events);
for (const auto& update : updates) {
tree_.Unserialize(update);
}
}
void DispatchAccessibilityLocationChange(
const ui::AXLocationChanges& details) override {}
void DispatchTreeDestroyedEvent(ui::AXTreeID tree_id) override {}
void DispatchActionResult(
const ui::AXActionData& data,
bool result,
content::BrowserContext* browser_context = nullptr) override {}
void DispatchGetTextLocationDataResult(
const ui::AXActionData& data,
const std::optional<gfx::Rect>& rect) override {}
std::vector<ui::AXEvent> last_dispatched_events() const {
return last_dispatched_events_;
}
std::map<ax::mojom::Event, int> event_count_;
ui::AXTree tree_;
private:
std::vector<ui::AXEvent> last_dispatched_events_;
};
} // namespace
class AXTreeSourceAndroidTest : public testing::Test,
public AXTreeSourceAndroid::Delegate {
public:
class TestSerializationDelegate
: public AXTreeSourceAndroid::SerializationDelegate {
// AXTreeSourceAndroid::SerializationDelegate overrides.
void PopulateBounds(const AccessibilityInfoDataWrapper& node,
ui::AXNodeData& out_data) const override {
gfx::RectF& out_bounds_px = out_data.relative_bounds.bounds;
gfx::Rect info_data_bounds = node.GetBounds();
out_bounds_px.SetRect(info_data_bounds.x(), info_data_bounds.y(),
info_data_bounds.width(),
info_data_bounds.height());
}
};
AXTreeSourceAndroidTest()
: router_(std::make_unique<MockAutomationEventRouter>()),
tree_source_(std::make_unique<AXTreeSourceAndroid>(
this,
std::make_unique<TestSerializationDelegate>(),
/*window=*/nullptr)) {
tree_source_->set_automation_event_router_for_test(router_.get());
}
AXTreeSourceAndroidTest(const AXTreeSourceAndroidTest&) = delete;
AXTreeSourceAndroidTest& operator=(const AXTreeSourceAndroidTest&) = delete;
// AXTreeSourceAndroid::Delegate overrides.
void OnAction(const ui::AXActionData& data) const override {}
bool UseFullFocusMode() const override { return full_focus_mode_; }
protected:
void CallNotifyAccessibilityEvent(AXEventData* event_data) {
tree_source_->NotifyAccessibilityEvent(event_data);
}
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>& GetChildren(
int32_t node_id) {
ui::AXNode* ax_node = tree()->GetFromId(node_id);
return ax_node->children();
}
const ui::AXNodeData& GetSerializedNode(int32_t node_id) {
ui::AXNode* ax_node = tree()->GetFromId(node_id);
return ax_node->data();
}
const ui::AXNodeData& GetSerializedWindow(int32_t window_id) {
ui::AXNode* ax_node = tree()->GetFromId(window_id);
return ax_node->data();
}
bool CallGetTreeData(ui::AXTreeData* data) {
return tree_source_->GetTreeData(data);
}
MockAutomationEventRouter* GetRouter() const { return router_.get(); }
int GetDispatchedEventCount(ax::mojom::Event type) {
return router_->event_count_[type];
}
std::vector<ui::AXEvent> last_dispatched_events() const {
return router_->last_dispatched_events();
}
ui::AXTree* tree() { return router_->tree(); }
void ExpectTree(const std::string& expected) {
const std::string& tree_text = tree()->ToString();
size_t first_new_line = tree_text.find("\n");
ASSERT_NE(std::string::npos, first_new_line);
ASSERT_GT(tree_text.size(), ++first_new_line);
// Omit the first line, which contains an unguessable ax tree id.
EXPECT_EQ(expected, tree_text.substr(first_new_line));
}
void set_full_focus_mode(bool enabled) { full_focus_mode_ = enabled; }
private:
const std::unique_ptr<MockAutomationEventRouter> router_;
const std::unique_ptr<AXTreeSourceAndroid> tree_source_;
bool full_focus_mode_ = false;
};
TEST_F(AXTreeSourceAndroidTest, ReorderChildrenByLayout) {
set_full_focus_mode(true);
auto event = AXEventData::New();
event->source_id = 100;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({11, 12}));
// Add child button and its wrapper.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* wrapper1 = event->node_data.back().get();
wrapper1->id = 11;
SetProperty(wrapper1, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1}));
SetProperty(wrapper1, AXBooleanProperty::VISIBLE_TO_USER, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* button1 = event->node_data.back().get();
button1->id = 1;
SetProperty(button1, AXStringProperty::CLASS_NAME, ui::kAXButtonClassname);
SetProperty(button1, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(button1, AXBooleanProperty::FOCUSABLE, true);
SetProperty(button1, AXBooleanProperty::IMPORTANCE, true);
SetProperty(button1, AXStringProperty::CONTENT_DESCRIPTION, "button1");
// Add another child button and its wrapper.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* wrapper2 = event->node_data.back().get();
wrapper2->id = 12;
SetProperty(wrapper2, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({2}));
SetProperty(wrapper2, AXBooleanProperty::VISIBLE_TO_USER, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* button2 = event->node_data.back().get();
button2->id = 2;
SetProperty(button2, AXStringProperty::CLASS_NAME, ui::kAXButtonClassname);
SetProperty(button2, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(button2, AXBooleanProperty::FOCUSABLE, true);
SetProperty(button2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(button2, AXStringProperty::CONTENT_DESCRIPTION, "button2");
// Non-overlapping, bottom to top.
button1->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
button2->bounds_in_screen = gfx::Rect(0, 0, 50, 50);
// Trigger an update which refreshes the computed bounds used for reordering.
CallNotifyAccessibilityEvent(event.get());
std::vector<raw_ptr<ui::AXNode, VectorExperimental>> top_to_bottom;
top_to_bottom = GetChildren(root->id);
ASSERT_EQ(2U, top_to_bottom.size());
EXPECT_EQ(12, top_to_bottom[0]->id());
EXPECT_EQ(11, top_to_bottom[1]->id());
// Non-overlapping, top to bottom.
button1->bounds_in_screen = gfx::Rect(0, 0, 50, 50);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
top_to_bottom = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, top_to_bottom.size());
EXPECT_EQ(11, top_to_bottom[0]->id());
EXPECT_EQ(12, top_to_bottom[1]->id());
// Overlapping; right to left.
button1->bounds_in_screen = gfx::Rect(101, 100, 99, 100);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
std::vector<raw_ptr<ui::AXNode, VectorExperimental>> left_to_right;
left_to_right = GetChildren(root->id);
ASSERT_EQ(2U, left_to_right.size());
EXPECT_EQ(12, left_to_right[0]->id());
EXPECT_EQ(11, left_to_right[1]->id());
// Overlapping; left to right.
button1->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
button2->bounds_in_screen = gfx::Rect(101, 100, 99, 100);
CallNotifyAccessibilityEvent(event.get());
left_to_right = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, left_to_right.size());
EXPECT_EQ(11, left_to_right[0]->id());
EXPECT_EQ(12, left_to_right[1]->id());
// Overlapping, bottom to top.
button1->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
button2->bounds_in_screen = gfx::Rect(100, 99, 100, 100);
CallNotifyAccessibilityEvent(event.get());
top_to_bottom = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, top_to_bottom.size());
EXPECT_EQ(12, top_to_bottom[0]->id());
EXPECT_EQ(11, top_to_bottom[1]->id());
// Overlapping, top to bottom.
button1->bounds_in_screen = gfx::Rect(100, 99, 100, 100);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
top_to_bottom = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, top_to_bottom.size());
EXPECT_EQ(11, top_to_bottom[0]->id());
EXPECT_EQ(12, top_to_bottom[1]->id());
// Identical. smaller to larger.
button1->bounds_in_screen = gfx::Rect(100, 100, 100, 10);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
std::vector<raw_ptr<ui::AXNode, VectorExperimental>> dimension;
dimension = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, dimension.size());
EXPECT_EQ(12, dimension[0]->id());
EXPECT_EQ(11, dimension[1]->id());
button1->bounds_in_screen = gfx::Rect(100, 100, 10, 100);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
dimension = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, dimension.size());
EXPECT_EQ(12, dimension[0]->id());
EXPECT_EQ(11, dimension[1]->id());
// Identical. Larger to smaller.
button1->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 10);
CallNotifyAccessibilityEvent(event.get());
dimension = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, dimension.size());
EXPECT_EQ(11, dimension[0]->id());
EXPECT_EQ(12, dimension[1]->id());
button1->bounds_in_screen = gfx::Rect(100, 100, 10, 100);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
dimension = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, dimension.size());
EXPECT_EQ(12, dimension[0]->id());
EXPECT_EQ(11, dimension[1]->id());
// When bounds_in_screen is the same as the (enclosing bounds of) child one,
// Then, do not sort.
wrapper1->bounds_in_screen = button1->bounds_in_screen;
wrapper2->bounds_in_screen = button2->bounds_in_screen;
CallNotifyAccessibilityEvent(event.get());
dimension = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, dimension.size());
EXPECT_EQ(11, dimension[0]->id());
EXPECT_EQ(12, dimension[1]->id());
// Bounds of buttons requires reordering. Comparison of wrapper bounds also
// requires reordering. This won't be reordered.
wrapper1->bounds_in_screen = gfx::Rect(100, 100, 50, 100);
button1->bounds_in_screen = gfx::Rect(100, 100, 10, 100);
wrapper2->bounds_in_screen = gfx::Rect(100, 100, 500, 100);
button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100);
CallNotifyAccessibilityEvent(event.get());
dimension = GetChildren(event->node_data[0].get()->id);
ASSERT_EQ(2U, dimension.size());
EXPECT_EQ(11, dimension[0]->id());
EXPECT_EQ(12, dimension[1]->id());
// Check completeness of tree output.
ExpectTree(
"id=100 window FOCUSABLE child_ids=10 (0, 0)-(0, 0) modal=true\n"
" id=10 genericContainer INVISIBLE child_ids=11,12 (0, 0)-(0, 0) "
"restriction=disabled\n"
" id=11 genericContainer IGNORED child_ids=1 (100, 100)-(50, 100)"
" restriction=disabled\n"
" id=1 button FOCUSABLE class_name=android.widget.Button"
" name=button1 name_from=attribute (100, 100)-(10, 100)"
" restriction=disabled\n"
" id=12 genericContainer IGNORED child_ids=2 (100, 100)-(500, 100)"
" restriction=disabled\n"
" id=2 button FOCUSABLE class_name=android.widget.Button"
" name=button2 name_from=attribute (100, 100)-(100, 100)"
" restriction=disabled\n");
}
TEST_F(AXTreeSourceAndroidTest, AccessibleNameComputationWindow) {
auto event = AXEventData::New();
event->source_id = 1;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 10;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root = event->window_data->back().get();
root->window_id = 1;
root->root_node_id = node->id;
// Live edit name related attributes.
ui::AXNodeData data;
// No attributes.
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedWindow(root->window_id);
std::string name;
ASSERT_FALSE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
// Title attribute
SetProperty(root, AXWindowStringProperty::TITLE, "window title");
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedWindow(root->window_id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("window title", name);
EXPECT_EQ(2, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceAndroidTest, NotificationWindow) {
auto event = AXEventData::New();
event->source_id = 1;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 10;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root = event->window_data->back().get();
root->window_id = 1;
root->root_node_id = node->id;
root->window_type = mojom::AccessibilityWindowType::TYPE_APPLICATION;
ui::AXNodeData data;
// Properties of normal app window.
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedWindow(root->window_id);
ASSERT_TRUE(data.GetBoolAttribute(ax::mojom::BoolAttribute::kModal));
ASSERT_EQ(ax::mojom::Role::kApplication, data.role);
// Set the tree as notification window.
event->notification_key = "test.notification.key";
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedWindow(root->window_id);
ASSERT_FALSE(data.GetBoolAttribute(ax::mojom::BoolAttribute::kModal));
ASSERT_EQ(ax::mojom::Role::kGenericContainer, data.role);
}
TEST_F(AXTreeSourceAndroidTest, AccessibleNameComputationWindowWithChildren) {
auto event = AXEventData::New();
event->source_id = 3;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root = event->window_data->back().get();
root->window_id = 100;
root->root_node_id = 3;
SetProperty(root, AXWindowIntListProperty::CHILD_WINDOW_IDS, {2, 5});
SetProperty(root, AXWindowStringProperty::TITLE, "window title");
// Add a child window.
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* child = event->window_data->back().get();
child->window_id = 2;
child->root_node_id = 4;
SetProperty(child, AXWindowStringProperty::TITLE, "child window title");
// Add a child node.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 3;
SetProperty(node, AXStringProperty::TEXT, "node text");
SetProperty(node, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node, AXBooleanProperty::VISIBLE_TO_USER, true);
// Add a child node to the child window as well.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child_node = event->node_data.back().get();
child_node->id = 4;
SetProperty(child_node, AXStringProperty::TEXT, "child node text");
SetProperty(child_node, AXBooleanProperty::IMPORTANCE, true);
SetProperty(child_node, AXBooleanProperty::VISIBLE_TO_USER, true);
// Add a child window with no children as well.
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* child2 = event->window_data->back().get();
child2->window_id = 5;
SetProperty(child2, AXWindowStringProperty::TITLE, "child2 window title");
CallNotifyAccessibilityEvent(event.get());
ui::AXNodeData data;
std::string name;
data = GetSerializedWindow(root->window_id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("window title", name);
EXPECT_NE(ax::mojom::Role::kRootWebArea, data.role);
EXPECT_TRUE(data.GetBoolAttribute(ax::mojom::BoolAttribute::kModal));
data = GetSerializedWindow(child->window_id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("child window title", name);
EXPECT_NE(ax::mojom::Role::kRootWebArea, data.role);
data = GetSerializedNode(node->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("node text", name);
EXPECT_EQ(ax::mojom::Role::kStaticText, data.role);
ASSERT_FALSE(data.IsIgnored());
data = GetSerializedNode(child_node->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("child node text", name);
EXPECT_NE(ax::mojom::Role::kRootWebArea, data.role);
ASSERT_FALSE(data.IsIgnored());
data = GetSerializedWindow(child2->window_id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("child2 window title", name);
EXPECT_NE(ax::mojom::Role::kRootWebArea, data.role);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceAndroidTest, ComplexTreeStructure) {
int tree_size = 4;
int num_trees = 3;
auto event = AXEventData::New();
event->source_id = 4;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
// Pick large numbers for the IDs so as not to overlap.
root_window->window_id = 1000;
SetProperty(root_window, AXWindowIntListProperty::CHILD_WINDOW_IDS,
{100, 200, 300});
// Make three non-overlapping trees rooted at the same window. One tree has
// the source_id of interest. Each subtree has a root window, which has a
// root node with one child, and that child has two leaf children.
for (int i = 0; i < num_trees; i++) {
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* child_window = event->window_data->back().get();
child_window->window_id = (i + 1) * 100;
child_window->root_node_id = i * tree_size + 1;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = i * tree_size + 1;
root->window_id = (i + 1) * 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({i * tree_size + 2}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child1 = event->node_data.back().get();
child1->id = i * tree_size + 2;
SetProperty(child1, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({i * tree_size + 3, i * tree_size + 4}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child2 = event->node_data.back().get();
child2->id = i * tree_size + 3;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child3 = event->node_data.back().get();
child3->id = i * tree_size + 4;
}
CallNotifyAccessibilityEvent(event.get());
// Check that each node subtree tree was added, and that it is correct.
std::vector<raw_ptr<ui::AXNode, VectorExperimental>> children;
for (int i = 0; i < num_trees; i++) {
children = GetChildren(event->node_data.at(i * tree_size).get()->id);
ASSERT_EQ(1U, children.size());
EXPECT_EQ(i * tree_size + 2, children[0]->id());
children.clear();
children = GetChildren(event->node_data.at(i * tree_size + 1).get()->id);
ASSERT_EQ(2U, children.size());
EXPECT_EQ(i * tree_size + 3, children[0]->id());
EXPECT_EQ(i * tree_size + 4, children[1]->id());
children.clear();
}
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceAndroidTest, GetTreeDataAppliesFocus) {
auto event = AXEventData::New();
event->source_id = 2;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root = event->window_data->back().get();
root->window_id = 5;
SetProperty(root, AXWindowIntListProperty::CHILD_WINDOW_IDS, {1});
SetProperty(root, mojom::AccessibilityWindowBooleanProperty::FOCUSED, true);
// Add a child window.
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* child = event->window_data->back().get();
child->window_id = 1;
// Add a child node.
root->root_node_id = 2;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 2;
node->window_id = 5;
SetProperty(node, AXBooleanProperty::FOCUSED, true);
SetProperty(node, AXBooleanProperty::FOCUSABLE, true);
SetProperty(node, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node, AXBooleanProperty::VISIBLE_TO_USER, true);
CallNotifyAccessibilityEvent(event.get());
ui::AXTreeData data;
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node->id, data.focus_id);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceAndroidTest, OnViewSelectedEvent) {
auto event = AXEventData::New();
event->task_id = 1;
event->event_type = AXEventType::VIEW_SELECTED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
SetProperty(root_window, mojom::AccessibilityWindowBooleanProperty::FOCUSED,
true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
root->window_id = 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* list = event->node_data.back().get();
list->id = 1;
list->window_id = 100;
SetProperty(list, AXBooleanProperty::FOCUSABLE, true);
SetProperty(list, AXBooleanProperty::IMPORTANCE, true);
SetProperty(list, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(list, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({2, 3, 4}));
// Slider.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* slider = event->node_data.back().get();
slider->id = 2;
slider->window_id = 100;
SetProperty(slider, AXBooleanProperty::FOCUSABLE, true);
SetProperty(slider, AXBooleanProperty::IMPORTANCE, true);
slider->range_info = AXRangeInfoData::New();
// Simple list item.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* simple_item = event->node_data.back().get();
simple_item->id = 3;
simple_item->window_id = 100;
SetProperty(simple_item, AXBooleanProperty::FOCUSABLE, true);
SetProperty(simple_item, AXBooleanProperty::IMPORTANCE, true);
SetProperty(simple_item, AXBooleanProperty::VISIBLE_TO_USER, true);
simple_item->collection_item_info = AXCollectionItemInfoData::New();
// This node is not focusable.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* wrap_node = event->node_data.back().get();
wrap_node->id = 4;
wrap_node->window_id = 100;
SetProperty(wrap_node, AXBooleanProperty::IMPORTANCE, true);
SetProperty(wrap_node, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(wrap_node, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({5}));
wrap_node->collection_item_info = AXCollectionItemInfoData::New();
// A list item expected to get the focus.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* item = event->node_data.back().get();
item->id = 5;
item->window_id = 100;
SetProperty(item, AXBooleanProperty::FOCUSABLE, true);
SetProperty(item, AXBooleanProperty::IMPORTANCE, true);
SetProperty(item, AXBooleanProperty::VISIBLE_TO_USER, true);
// A selected event from Slider doesn't have any specific event type.
event->source_id = slider->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kNone));
// A selected event from a collection. In Android, these event properties are
// populated by AdapterView.
event->source_id = list->id;
SetProperty(event.get(), AXEventIntProperty::ITEM_COUNT, 3);
SetProperty(event.get(), AXEventIntProperty::FROM_INDEX, 0);
SetProperty(event.get(), AXEventIntProperty::CURRENT_ITEM_INDEX, 2);
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
ui::AXTreeData data;
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(item->id, data.focus_id);
// A selected event from a collection item.
event->source_id = simple_item->id;
event->int_properties->clear();
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(2, GetDispatchedEventCount(ax::mojom::Event::kFocus));
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(simple_item->id, data.focus_id);
// An event from an invisible node is dropped.
SetProperty(simple_item, AXBooleanProperty::VISIBLE_TO_USER, false);
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(2,
GetDispatchedEventCount(ax::mojom::Event::kFocus)); // not changed
// A selected event from non collection node is dropped.
SetProperty(simple_item, AXBooleanProperty::VISIBLE_TO_USER, true);
event->source_id = item->id;
event->int_properties->clear();
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(2,
GetDispatchedEventCount(ax::mojom::Event::kFocus)); // not changed
}
TEST_F(AXTreeSourceAndroidTest, OnWindowStateChangedEvent) {
set_full_focus_mode(true);
auto event = AXEventData::New();
event->task_id = 1;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
event->window_id = 1;
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
SetProperty(root_window, mojom::AccessibilityWindowBooleanProperty::FOCUSED,
true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
root->window_id = 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
node1->window_id = 100;
SetProperty(node1, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({2, 3}));
SetProperty(node1, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node1, AXBooleanProperty::VISIBLE_TO_USER, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node2 = event->node_data.back().get();
node2->id = 2;
node2->window_id = 100;
SetProperty(node2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node2, AXStringProperty::TEXT, "sample string node2.");
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node3 = event->node_data.back().get();
node3->id = 3;
node3->window_id = 100;
SetProperty(node3, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node3, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node3, AXStringProperty::TEXT, "sample string node3.");
// Focus will be on the first accessible node (node2).
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = node1->id;
CallNotifyAccessibilityEvent(event.get());
ui::AXTreeData data;
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
// focus moved to node3 for some reason.
event->event_type = AXEventType::VIEW_FOCUSED;
SetProperty(node3, AXBooleanProperty::FOCUSED, true);
event->source_id = node3->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node3->id, data.focus_id);
// after moved the focus on the window, keep the same focus on
// WINDOW_STATE_CHANGED event.
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node3->id, data.focus_id);
// Simulate opening another window in this task.
// |root_window->window_id| can be the same as the previous one, but
// |event->window_id| of the event are always different for different window.
// This is the same as new WINDOW_STATE_CHANGED event, so focus is at the
// first accessible node (node2).
event->window_id = 2;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = node1->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
// Simulate closing the second window and coming back to the first window.
// The focus back to the last focus node, which is node3.
event->window_id = 1;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node3->id, data.focus_id);
EXPECT_EQ(5, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceAndroidTest, OnFocusEvent) {
set_full_focus_mode(true);
auto event = AXEventData::New();
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
SetProperty(root_window, mojom::AccessibilityWindowBooleanProperty::FOCUSED,
true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
root->window_id = 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1, 2}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
SetProperty(root, AXBooleanProperty::VISIBLE_TO_USER, true);
root->collection_info = AXCollectionInfoData::New();
root->collection_info->row_count = 2;
root->collection_info->column_count = 1;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
node1->window_id = 100;
SetProperty(node1, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node1, AXBooleanProperty::ACCESSIBILITY_FOCUSED, true);
SetProperty(node1, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node1, AXStringProperty::TEXT, "sample string1.");
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node2 = event->node_data.back().get();
node2->id = 2;
node2->window_id = 100;
SetProperty(node2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node2, AXBooleanProperty::FOCUSED, true);
SetProperty(node2, AXStringProperty::TEXT, "sample string2.");
// Chrome should focus to node2, even if node1 has ax focused in Android.
event->source_id = node2->id;
CallNotifyAccessibilityEvent(event.get());
ui::AXTreeData data;
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
// Chrome should focus to node1, when Android sends focus on List.
SetProperty(node2, AXBooleanProperty::FOCUSED, false);
SetProperty(root, AXBooleanProperty::FOCUSED, true);
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node1->id, data.focus_id);
// VIEW_ACCESSIBILITY_FOCUSED event also updates the focus in screen reader
// mode.
SetProperty(node1, AXBooleanProperty::ACCESSIBILITY_FOCUSED, false);
SetProperty(node2, AXBooleanProperty::ACCESSIBILITY_FOCUSED, true);
event->event_type = AXEventType::VIEW_ACCESSIBILITY_FOCUSED;
event->source_id = node2->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
EXPECT_EQ(3, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceAndroidTest, OnClearA11yFocusEvent) {
set_full_focus_mode(true);
auto event = AXEventData::New();
event->task_id = 1;
event->event_type = AXEventType::VIEW_ACCESSIBILITY_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
SetProperty(root_window, mojom::AccessibilityWindowBooleanProperty::FOCUSED,
true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
root->window_id = 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
SetProperty(root, AXBooleanProperty::VISIBLE_TO_USER, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 1;
node->window_id = 100;
SetProperty(node, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node, AXBooleanProperty::ACCESSIBILITY_FOCUSED, true);
SetProperty(node, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node, AXStringProperty::TEXT, "hello world");
// VIEW_ACCESSIBILITY_FOCUSED should sync focused node id in full focus mode.
event->source_id = node->id;
CallNotifyAccessibilityEvent(event.get());
ui::AXTreeData data;
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node->id, data.focus_id);
// Then, clear the a11y focus with an event from action property.
// This shouldn't update the focused node.
SetProperty(node, AXBooleanProperty::ACCESSIBILITY_FOCUSED, false);
event->event_type = AXEventType::VIEW_ACCESSIBILITY_FOCUS_CLEARED;
SetProperty(event.get(), AXEventIntProperty::ACTION,
static_cast<int32_t>(AXActionType::ACCESSIBILITY_FOCUS));
CallNotifyAccessibilityEvent(event.get());
data = ui::AXTreeData();
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node->id, data.focus_id);
// An a11y focus cleared event from an action unrelated to focus (e.g. scroll)
// should clear the focus, and moves the focus up to the root.
SetProperty(event.get(), AXEventIntProperty::ACTION,
static_cast<int32_t>(AXActionType::SCROLL_FORWARD));
CallNotifyAccessibilityEvent(event.get());
data = ui::AXTreeData();
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(root_window->window_id, data.focus_id);
}
TEST_F(AXTreeSourceAndroidTest, OnDrawerOpened) {
auto event = AXEventData::New();
event->source_id = 10; // root
event->task_id = 1;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->event_text = std::vector<std::string>({"Navigation"});
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
/* AXTree of this test:
[10] root (DrawerLayout)
--[1] node1 (not-importantForAccessibility) hidden node
--[2] node2 visible node
----[3] node3 node with text
*/
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1, 2}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
SetProperty(root, AXStringProperty::CLASS_NAME,
"androidx.drawerlayout.widget.DrawerLayout");
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
SetProperty(node1, AXBooleanProperty::VISIBLE_TO_USER, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node2 = event->node_data.back().get();
node2->id = 2;
SetProperty(node2, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({3}));
SetProperty(node2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXBooleanProperty::VISIBLE_TO_USER, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node3 = event->node_data.back().get();
node3->id = 3;
SetProperty(node3, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node3, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node3, AXStringProperty::TEXT, "sample string.");
CallNotifyAccessibilityEvent(event.get());
ui::AXNodeData data;
std::string name;
data = GetSerializedNode(node2->id);
ASSERT_EQ(ax::mojom::Role::kMenu, data.role);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("Navigation", name);
// Validate that the drawer title is cached.
event->event_text.reset();
event->event_type = AXEventType::WINDOW_CONTENT_CHANGED;
CallNotifyAccessibilityEvent(event.get());
data.RemoveStringAttribute(ax::mojom::StringAttribute::kName);
data = GetSerializedNode(node2->id);
ASSERT_EQ(ax::mojom::Role::kMenu, data.role);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("Navigation", name);
}
TEST_F(AXTreeSourceAndroidTest, SerializeAndUnserialize) {
auto event = AXEventData::New();
event->source_id = 10;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
SetProperty(node1, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({2}));
// An ignored node.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node2 = event->node_data.back().get();
node2->id = 2;
// |node2| is ignored by default because
// AXBooleanProperty::IMPORTANCE has a default false value.
set_full_focus_mode(true);
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
ExpectTree(
"id=100 window FOCUSABLE child_ids=10 (0, 0)-(0, 0) modal=true\n"
" id=10 genericContainer IGNORED INVISIBLE child_ids=1 (0, 0)-(0, 0) "
"restriction=disabled\n"
" id=1 genericContainer IGNORED INVISIBLE child_ids=2 (0, 0)-(0, 0) "
"restriction=disabled\n"
" id=2 genericContainer IGNORED INVISIBLE (0, 0)-(0, 0) "
"restriction=disabled\n");
EXPECT_EQ(0U, tree()->GetFromId(100)->GetUnignoredChildCount());
#if DCHECK_IS_ON()
EXPECT_DEATH_IF_SUPPORTED(tree()->GetFromId(10)->GetUnignoredChildCount(),
"Called unignored method on ignored node");
#else
EXPECT_EQ(0U, tree()->GetFromId(10)->GetUnignoredChildCount());
#endif
// An unignored node.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node3 = event->node_data.back().get();
node3->id = 3;
SetProperty(node3, AXStringProperty::CONTENT_DESCRIPTION, "some text");
SetProperty(node3, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({3}));
// |node3| is unignored since it has some text.
CallNotifyAccessibilityEvent(event.get());
ExpectTree(
"id=100 window FOCUSABLE child_ids=10 (0, 0)-(0, 0) modal=true\n"
" id=10 genericContainer INVISIBLE child_ids=1 (0, 0)-(0, 0) "
"restriction=disabled\n"
" id=1 genericContainer IGNORED INVISIBLE child_ids=2 (0, 0)-(0, 0) "
"restriction=disabled\n"
" id=2 genericContainer IGNORED INVISIBLE child_ids=3 (0, 0)-(0, 0) "
"restriction=disabled\n"
" id=3 genericContainer INVISIBLE name=some text "
"name_from=attribute (0, 0)-(0, 0) restriction=disabled\n");
EXPECT_EQ(1U, tree()->GetFromId(10)->GetUnignoredChildCount());
}
TEST_F(AXTreeSourceAndroidTest, SerializeVirtualNode) {
auto event = AXEventData::New();
event->source_id = 10;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
// Add a webview node.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* webview = event->node_data.back().get();
webview->id = 1;
SetProperty(webview, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(webview, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({2, 3}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* button1 = event->node_data.back().get();
button1->id = 2;
button1->bounds_in_screen = gfx::Rect(0, 0, 50, 50);
button1->is_virtual_node = true;
SetProperty(button1, AXStringProperty::CLASS_NAME, ui::kAXButtonClassname);
SetProperty(button1, AXBooleanProperty::VISIBLE_TO_USER, true);
AddStandardAction(button1, AXActionType::NEXT_HTML_ELEMENT);
AddStandardAction(button1, AXActionType::FOCUS);
SetProperty(button1, AXStringProperty::CONTENT_DESCRIPTION, "button1");
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* button2 = event->node_data.back().get();
button2->id = 3;
button2->bounds_in_screen = gfx::Rect(0, 0, 100, 100);
button2->is_virtual_node = true;
SetProperty(button2, AXStringProperty::CLASS_NAME, ui::kAXButtonClassname);
SetProperty(button2, AXBooleanProperty::VISIBLE_TO_USER, true);
AddStandardAction(button2, AXActionType::NEXT_HTML_ELEMENT);
AddStandardAction(button2, AXActionType::FOCUS);
SetProperty(button2, AXStringProperty::CONTENT_DESCRIPTION, "button2");
CallNotifyAccessibilityEvent(event.get());
ui::AXNodeData data;
data = GetSerializedNode(webview->id);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, data.role);
// Node inside a WebView is not ignored even if it's not set importance.
data = GetSerializedNode(button1->id);
ASSERT_FALSE(data.IsIgnored());
data = GetSerializedNode(button2->id);
ASSERT_FALSE(data.IsIgnored());
// Children are not reordered under WebView.
std::vector<raw_ptr<ui::AXNode, VectorExperimental>> children;
children = GetChildren(webview->id);
ASSERT_EQ(2U, children.size());
EXPECT_EQ(button1->id, children[0]->id());
EXPECT_EQ(button2->id, children[1]->id());
}
TEST_F(AXTreeSourceAndroidTest, SyncFocus) {
auto event = AXEventData::New();
event->source_id = 1;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
SetProperty(root_window, mojom::AccessibilityWindowBooleanProperty::FOCUSED,
true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
root->window_id = 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1, 2}));
// Add child nodes.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
node1->window_id = 100;
SetProperty(node1, AXBooleanProperty::FOCUSABLE, true);
SetProperty(node1, AXBooleanProperty::FOCUSED, true);
SetProperty(node1, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node1, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node1, AXStringProperty::CONTENT_DESCRIPTION, "node1");
node1->bounds_in_screen = gfx::Rect(0, 0, 50, 50);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node2 = event->node_data.back().get();
node2->id = 2;
node2->window_id = 100;
SetProperty(node2, AXBooleanProperty::FOCUSABLE, true);
SetProperty(node2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXBooleanProperty::VISIBLE_TO_USER, true);
// Add a child node to |node1|, but it's not an important node.
SetProperty(node1, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({3}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node3 = event->node_data.back().get();
node3->id = 3;
node3->window_id = 100;
// Initially |node1| has focus.
CallNotifyAccessibilityEvent(event.get());
ui::AXTreeData data;
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node1->id, data.focus_id);
// Focus event from a non-important node. The ancestry important node |node1|
// gets focus instead.
event->source_id = node3->id;
event->event_type = AXEventType::VIEW_FOCUSED;
CallNotifyAccessibilityEvent(event.get());
data = ui::AXTreeData();
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node1->id, data.focus_id);
// Focus event from a non-focused node. Focus won't be updated.
event->source_id = node2->id;
event->event_type = AXEventType::VIEW_FOCUSED;
CallNotifyAccessibilityEvent(event.get());
data = ui::AXTreeData();
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node1->id, data.focus_id);
// When the focused node disappeared from the tree, reset the focus to the
// root.
root->int_list_properties->clear();
event->node_data.resize(1);
event->event_type = AXEventType::WINDOW_CONTENT_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
data = ui::AXTreeData();
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(root_window->window_id, data.focus_id);
}
TEST_F(AXTreeSourceAndroidTest, StateDescriptionChangedEvent) {
auto event = AXEventData::New();
event->source_id = 11;
event->task_id = 1;
event->event_type = AXEventType::WINDOW_CONTENT_CHANGED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root_node = event->node_data.back().get();
root_node->id = 10;
root_node->window_id = 100;
SetProperty(root_node, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({11}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* range_widget = event->node_data.back().get();
range_widget->range_info = AXRangeInfoData::New();
range_widget->id = 11;
// State description changed event from range widget.
std::vector<int> content_change_types = {
static_cast<int>(mojom::ContentChangeType::TEXT),
static_cast<int>(mojom::ContentChangeType::STATE_DESCRIPTION)};
SetProperty(event.get(), AXEventIntListProperty::CONTENT_CHANGE_TYPES,
content_change_types);
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(last_dispatched_events().empty());
// State description changed event from non range widget.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* not_range_widget = event->node_data.back().get();
not_range_widget->id = 12;
event->source_id = 12;
SetProperty(root_node, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({11, 12}));
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(last_dispatched_events().empty());
}
TEST_F(AXTreeSourceAndroidTest, EventWithWrongSourceId) {
auto event = AXEventData::New();
event->source_id = 99999; // This doesn't exist in serialized nodes.
event->task_id = 1;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 10;
// This test only verifies that wrong source id won't make Chrome crash.
event->event_type = AXEventType::VIEW_FOCUSED;
CallNotifyAccessibilityEvent(event.get());
event->event_type = AXEventType::VIEW_SELECTED;
CallNotifyAccessibilityEvent(event.get());
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->event_text = std::vector<std::string>({"test text."});
SetProperty(event.get(), AXEventIntListProperty::CONTENT_CHANGE_TYPES,
{static_cast<int>(mojom::ContentChangeType::STATE_DESCRIPTION)});
CallNotifyAccessibilityEvent(event.get());
event->event_type = AXEventType::WINDOW_CONTENT_CHANGED;
CallNotifyAccessibilityEvent(event.get());
}
TEST_F(AXTreeSourceAndroidTest, EnsureNodeIdMapCleared) {
auto event = AXEventData::New();
event->source_id = 1;
event->task_id = 1;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 2;
root_window->root_node_id = 1;
SetProperty(root_window, mojom::AccessibilityWindowBooleanProperty::FOCUSED,
true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 1;
node->window_id = 2;
event->event_type = AXEventType::VIEW_SELECTED;
CallNotifyAccessibilityEvent(event.get());
// Ensures that the first event is dropped while handling it.
EXPECT_EQ(0, GetDispatchedEventCount(ax::mojom::Event::kFocus));
EXPECT_EQ(0, GetDispatchedEventCount(ax::mojom::Event::kValueChanged));
event->event_type = AXEventType::WINDOW_CONTENT_CHANGED;
// Swaps ids of node and root_window.
event->source_id = 2;
root_window->window_id = 1;
root_window->root_node_id = 2;
node->id = 2;
// If the previous node id mapping remains, this will enter infinite loop.
CallNotifyAccessibilityEvent(event.get());
}
TEST_F(AXTreeSourceAndroidTest, ControlWithoutNameReceivesFocus) {
auto event = AXEventData::New();
event->source_id = 1;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
SetProperty(root_window, AXWindowBooleanProperty::FOCUSED, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root_node = event->node_data.back().get();
root_node->id = 10;
root_node->window_id = 100;
SetProperty(root_node, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 1;
node->window_id = 100;
SetProperty(node, AXStringProperty::CLASS_NAME, ui::kAXSeekBarClassname);
SetProperty(node, AXStringProperty::TEXT, "");
SetProperty(node, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node, AXBooleanProperty::FOCUSABLE, true);
SetProperty(node, AXBooleanProperty::FOCUSED, true);
SetProperty(node, AXBooleanProperty::IMPORTANCE, true);
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
ui::AXNodeData data;
std::string name;
data = GetSerializedNode(node->id);
ASSERT_FALSE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ(ax::mojom::Role::kSlider, data.role);
ui::AXTreeData tree_data;
EXPECT_TRUE(CallGetTreeData(&tree_data));
EXPECT_EQ(node->id, tree_data.focus_id);
}
TEST_F(AXTreeSourceAndroidTest, AutoComplete) {
auto event = AXEventData::New();
event->task_id = 1;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
root->window_id = 100;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* editable = event->node_data.back().get();
editable->id = 1;
editable->window_id = 100;
SetProperty(editable, AXBooleanProperty::IMPORTANCE, true);
SetProperty(editable, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(editable, AXBooleanProperty::EDITABLE, true);
SetProperty(editable, AXStringProperty::CLASS_NAME,
"android.widget.MultiAutoCompleteTextView");
// Check basic properties.
event->event_type = AXEventType::WINDOW_CONTENT_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
ui::AXNodeData data;
data = GetSerializedNode(editable->id);
ASSERT_EQ(ax::mojom::Role::kTextField, data.role);
std::string attribute;
ASSERT_TRUE(data.GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete,
&attribute));
EXPECT_EQ("list", attribute);
EXPECT_TRUE(data.HasState(ax::mojom::State::kCollapsed));
// Add a sub-window anchoring the editable.
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* popup_window = event->window_data->back().get();
popup_window->window_id = 200;
popup_window->root_node_id = 20;
SetProperty(popup_window, AXWindowIntProperty::ANCHOR_NODE_ID, editable->id);
SetProperty(root_window, AXWindowIntListProperty::CHILD_WINDOW_IDS, {200});
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* candidate_list = event->node_data.back().get();
candidate_list->id = 20;
candidate_list->window_id = 200;
SetProperty(candidate_list, AXBooleanProperty::IMPORTANCE, true);
SetProperty(candidate_list, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(candidate_list, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({21}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* list_item = event->node_data.back().get();
list_item->id = 21;
list_item->window_id = 200;
SetProperty(list_item, AXBooleanProperty::IMPORTANCE, true);
SetProperty(list_item, AXBooleanProperty::VISIBLE_TO_USER, true);
list_item->collection_item_info = AXCollectionItemInfoData::New();
// Verify the relationship of window and editable.
event->event_type = AXEventType::WINDOWS_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(editable->id);
EXPECT_TRUE(data.HasState(ax::mojom::State::kExpanded));
std::vector<int32_t> controlled_ids;
ASSERT_TRUE(data.GetIntListAttribute(
ax::mojom::IntListAttribute::kControlsIds, &controlled_ids));
ASSERT_EQ(1U, controlled_ids.size());
ASSERT_EQ(popup_window->window_id, controlled_ids[0]);
// Invoke a selection event from the list.
SetProperty(list_item, AXBooleanProperty::SELECTED, true);
// Verify that active descendant is updated.
event->event_type = AXEventType::VIEW_SELECTED;
event->source_id = list_item->id;
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(editable->id);
int32_t active_descendant;
ASSERT_TRUE(data.GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
&active_descendant));
ASSERT_EQ(list_item->id, active_descendant);
// Delete popup window.
event->node_data.pop_back();
event->node_data.pop_back();
event->window_data->pop_back();
SetProperty(root_window, AXWindowIntListProperty::CHILD_WINDOW_IDS, {});
// Verify autocomplete properties are still populated.
event->event_type = AXEventType::WINDOWS_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(editable->id);
ASSERT_TRUE(data.GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete,
&attribute));
EXPECT_EQ("list", attribute);
EXPECT_TRUE(data.HasState(ax::mojom::State::kCollapsed));
}
TEST_F(AXTreeSourceAndroidTest, EventFrom) {
auto event = AXEventData::New();
event->source_id = 1;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node = event->node_data.back().get();
node->id = 10;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root = event->window_data->back().get();
root->window_id = 1;
root->root_node_id = node->id;
// By default, event_from and event_from_action are None.
CallNotifyAccessibilityEvent(event.get());
ui::AXEvent actual = last_dispatched_events()[0];
EXPECT_EQ(ax::mojom::EventFrom::kNone, actual.event_from);
EXPECT_EQ(ax::mojom::Action::kNone, actual.event_from_action);
// With |Action| field, event_from and event_from_action are populated.
SetProperty(
event.get(), AXEventIntProperty::ACTION,
static_cast<int32_t>(ax::android::mojom::AccessibilityActionType::CLICK));
CallNotifyAccessibilityEvent(event.get());
actual = last_dispatched_events()[0];
EXPECT_EQ(ax::mojom::EventFrom::kAction, actual.event_from);
EXPECT_EQ(ax::mojom::Action::kDoDefault, actual.event_from_action);
}
TEST_F(AXTreeSourceAndroidTest, UpdateChangeFromNameMergedNode) {
set_full_focus_mode(true);
auto event = AXEventData::New();
event->source_id = 10; // root
event->task_id = 1;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
/* AXTree of this test:
[10] root
--[1] node1 clickable container
----[2] node2 text node, not clickable
----[3] node3 button node, clickable
*/
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1}));
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
SetProperty(node1, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node1, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node1, AXBooleanProperty::CLICKABLE, true);
SetProperty(node1, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({2, 3}));
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node2 = event->node_data.back().get();
node2->id = 2;
SetProperty(node2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node2, AXStringProperty::TEXT, "text");
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node3 = event->node_data.back().get();
node3->id = 3;
SetProperty(node3, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node3, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node3, AXBooleanProperty::CLICKABLE, true);
SetProperty(node3, AXStringProperty::TEXT, "button");
CallNotifyAccessibilityEvent(event.get());
ui::AXNodeData data;
std::string name;
// (Precondition) First, check name computation from children.
data = GetSerializedNode(node1->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("text", name);
data = GetSerializedNode(node2->id);
ASSERT_TRUE(data.IsIgnored());
data = GetSerializedNode(node3->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("button", name);
// Update button text.
event->source_id = node3->id;
event->event_type = AXEventType::VIEW_TEXT_CHANGED;
SetProperty(node3, AXStringProperty::TEXT, "button2");
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(node3->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("button2", name);
// Update text in node2.
// This text is used by node1, but event source is node2.
// AXTreeSourceAndroid should handle and update the tree.
event->source_id = node2->id;
event->event_type = AXEventType::VIEW_TEXT_CHANGED;
SetProperty(node2, AXStringProperty::TEXT, "text2");
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(node1->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("text2", name);
}
} // namespace ax::android