| // 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 |