| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/chromeos/arc/accessibility/ax_tree_source_arc.h" |
| |
| #include "base/stl_util.h" |
| #include "chrome/browser/chromeos/arc/accessibility/accessibility_node_info_data_wrapper.h" |
| #include "chrome/browser/chromeos/arc/accessibility/accessibility_window_info_data_wrapper.h" |
| #include "components/arc/mojom/accessibility_helper.mojom.h" |
| #include "extensions/browser/api/automation_internal/automation_event_router.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/platform/ax_android_constants.h" |
| |
| namespace arc { |
| |
| using AXBooleanProperty = mojom::AccessibilityBooleanProperty; |
| using AXCollectionInfoData = mojom::AccessibilityCollectionInfoData; |
| using AXCollectionItemInfoData = mojom::AccessibilityCollectionItemInfoData; |
| using AXEventData = mojom::AccessibilityEventData; |
| using AXEventType = mojom::AccessibilityEventType; |
| using AXIntListProperty = mojom::AccessibilityIntListProperty; |
| using AXIntProperty = mojom::AccessibilityIntProperty; |
| using AXNodeInfoData = mojom::AccessibilityNodeInfoData; |
| using AXRangeInfoData = mojom::AccessibilityRangeInfoData; |
| using AXStringListProperty = mojom::AccessibilityStringListProperty; |
| using AXStringProperty = mojom::AccessibilityStringProperty; |
| using AXWindowInfoData = mojom::AccessibilityWindowInfoData; |
| using AXWindowIntListProperty = mojom::AccessibilityWindowIntListProperty; |
| using AXWindowStringProperty = mojom::AccessibilityWindowStringProperty; |
| |
| void SetProperty(AXNodeInfoData* node, AXBooleanProperty prop, bool value) { |
| if (!node->boolean_properties) { |
| node->boolean_properties = base::flat_map<AXBooleanProperty, bool>(); |
| } |
| auto& prop_map = node->boolean_properties.value(); |
| base::EraseIf(prop_map, [prop](auto it) { return it.first == prop; }); |
| prop_map.insert(std::make_pair(prop, value)); |
| } |
| |
| void SetProperty(AXNodeInfoData* node, |
| AXStringProperty prop, |
| const std::string& value) { |
| if (!node->string_properties) { |
| node->string_properties = base::flat_map<AXStringProperty, std::string>(); |
| } |
| auto& prop_map = node->string_properties.value(); |
| base::EraseIf(prop_map, [prop](auto it) { return it.first == prop; }); |
| prop_map.insert(std::make_pair(prop, value)); |
| } |
| |
| void SetProperty(AXNodeInfoData* node, AXIntProperty prop, int32_t value) { |
| if (!node->int_properties) { |
| node->int_properties = base::flat_map<AXIntProperty, int>(); |
| } |
| auto& prop_map = node->int_properties.value(); |
| base::EraseIf(prop_map, [prop](auto it) { return it.first == prop; }); |
| prop_map.insert(std::make_pair(prop, value)); |
| } |
| |
| void SetProperty(AXWindowInfoData* window, |
| AXWindowStringProperty prop, |
| const std::string& value) { |
| if (!window->string_properties) { |
| window->string_properties = |
| base::flat_map<AXWindowStringProperty, std::string>(); |
| } |
| auto& prop_map = window->string_properties.value(); |
| base::EraseIf(prop_map, [prop](auto it) { return it.first == prop; }); |
| prop_map.insert(std::make_pair(prop, value)); |
| } |
| |
| void SetProperty(AXNodeInfoData* node, |
| AXIntListProperty prop, |
| const std::vector<int>& value) { |
| if (!node->int_list_properties) { |
| node->int_list_properties = |
| base::flat_map<AXIntListProperty, std::vector<int>>(); |
| } |
| auto& prop_map = node->int_list_properties.value(); |
| base::EraseIf(prop_map, [prop](auto it) { return it.first == prop; }); |
| prop_map.insert(std::make_pair(prop, value)); |
| } |
| |
| void SetProperty(AXWindowInfoData* window, |
| AXWindowIntListProperty prop, |
| const std::vector<int>& value) { |
| if (!window->int_list_properties) { |
| window->int_list_properties = |
| base::flat_map<AXWindowIntListProperty, std::vector<int>>(); |
| } |
| auto& prop_map = window->int_list_properties.value(); |
| base::EraseIf(prop_map, [prop](auto it) { return it.first == prop; }); |
| prop_map.insert(std::make_pair(prop, value)); |
| } |
| |
| class MockAutomationEventRouter |
| : public extensions::AutomationEventRouterInterface { |
| public: |
| MockAutomationEventRouter() {} |
| ~MockAutomationEventRouter() override = default; |
| |
| void DispatchAccessibilityEvents( |
| const ExtensionMsg_AccessibilityEventBundleParams& events) override { |
| for (auto&& event : events.events) { |
| event_count_[event.event_type]++; |
| } |
| } |
| |
| void DispatchAccessibilityLocationChange( |
| const ExtensionMsg_AccessibilityLocationChangeParams& params) override {} |
| |
| void DispatchTreeDestroyedEvent( |
| ui::AXTreeID tree_id, |
| content::BrowserContext* browser_context) override {} |
| |
| void DispatchActionResult( |
| const ui::AXActionData& data, |
| bool result, |
| content::BrowserContext* browser_context = nullptr) override {} |
| |
| void DispatchGetTextLocationDataResult( |
| const ui::AXActionData& data, |
| const base::Optional<gfx::Rect>& rect) override {} |
| |
| std::map<ax::mojom::Event, int> event_count_; |
| }; |
| |
| class AXTreeSourceArcTest : public testing::Test, |
| public AXTreeSourceArc::Delegate { |
| public: |
| class TestAXTreeSourceArc : public AXTreeSourceArc { |
| public: |
| TestAXTreeSourceArc(AXTreeSourceArc::Delegate* delegate, |
| MockAutomationEventRouter* router) |
| : AXTreeSourceArc(delegate), router_(router) {} |
| |
| private: |
| extensions::AutomationEventRouterInterface* GetAutomationEventRouter() |
| const override { |
| return router_; |
| } |
| |
| MockAutomationEventRouter* const router_; |
| }; |
| |
| AXTreeSourceArcTest() |
| : router_(new MockAutomationEventRouter()), |
| tree_(new TestAXTreeSourceArc(this, router_.get())) {} |
| |
| protected: |
| void CallNotifyAccessibilityEvent(AXEventData* event_data) { |
| tree_->NotifyAccessibilityEvent(event_data); |
| } |
| |
| void CallGetChildren( |
| AXNodeInfoData* node, |
| std::vector<ArcAccessibilityInfoData*>* out_children) const { |
| AccessibilityNodeInfoDataWrapper node_data(tree_.get(), node); |
| tree_->GetChildren(&node_data, out_children); |
| } |
| |
| void CallSerializeNode(AXNodeInfoData* node, |
| std::unique_ptr<ui::AXNodeData>* out_data) const { |
| ASSERT_TRUE(out_data); |
| AccessibilityNodeInfoDataWrapper node_data(tree_.get(), node); |
| *out_data = std::make_unique<ui::AXNodeData>(); |
| tree_->SerializeNode(&node_data, out_data->get()); |
| } |
| |
| void CallSerializeWindow(AXWindowInfoData* window, |
| std::unique_ptr<ui::AXNodeData>* out_data) const { |
| ASSERT_TRUE(out_data); |
| AccessibilityWindowInfoDataWrapper window_data(tree_.get(), window); |
| *out_data = std::make_unique<ui::AXNodeData>(); |
| tree_->SerializeNode(&window_data, out_data->get()); |
| } |
| |
| ArcAccessibilityInfoData* CallGetFromId(int32_t id) const { |
| return tree_->GetFromId(id); |
| } |
| |
| bool CallGetTreeData(ui::AXTreeData* data) { |
| return tree_->GetTreeData(data); |
| } |
| |
| MockAutomationEventRouter* GetRouter() const { return router_.get(); } |
| |
| int GetDispatchedEventCount(ax::mojom::Event type) { |
| return router_->event_count_[type]; |
| } |
| |
| private: |
| void OnAction(const ui::AXActionData& data) const override {} |
| |
| const std::unique_ptr<MockAutomationEventRouter> router_; |
| const std::unique_ptr<AXTreeSourceArc> tree_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AXTreeSourceArcTest); |
| }; |
| |
| TEST_F(AXTreeSourceArcTest, ReorderChildrenByLayout) { |
| auto event = AXEventData::New(); |
| event->source_id = 0; |
| 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 = 0; |
| |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* root = event->node_data.back().get(); |
| root->id = 0; |
| SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, |
| std::vector<int>({1, 2})); |
| |
| // Add child button. |
| 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); |
| |
| // Add another child button. |
| 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); |
| |
| // 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<ArcAccessibilityInfoData*> top_to_bottom; |
| CallGetChildren(root, &top_to_bottom); |
| ASSERT_EQ(2U, top_to_bottom.size()); |
| EXPECT_EQ(2, top_to_bottom[0]->GetId()); |
| EXPECT_EQ(1, top_to_bottom[1]->GetId()); |
| |
| // 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.clear(); |
| CallGetChildren(event->node_data[0].get(), &top_to_bottom); |
| ASSERT_EQ(2U, top_to_bottom.size()); |
| EXPECT_EQ(1, top_to_bottom[0]->GetId()); |
| EXPECT_EQ(2, top_to_bottom[1]->GetId()); |
| |
| // 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<ArcAccessibilityInfoData*> left_to_right; |
| CallGetChildren(root, &left_to_right); |
| ASSERT_EQ(2U, left_to_right.size()); |
| EXPECT_EQ(2, left_to_right[0]->GetId()); |
| EXPECT_EQ(1, left_to_right[1]->GetId()); |
| |
| // 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.clear(); |
| CallGetChildren(event->node_data[0].get(), &left_to_right); |
| ASSERT_EQ(2U, left_to_right.size()); |
| EXPECT_EQ(1, left_to_right[0]->GetId()); |
| EXPECT_EQ(2, left_to_right[1]->GetId()); |
| |
| // 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.clear(); |
| CallGetChildren(event->node_data[0].get(), &top_to_bottom); |
| ASSERT_EQ(2U, top_to_bottom.size()); |
| EXPECT_EQ(2, top_to_bottom[0]->GetId()); |
| EXPECT_EQ(1, top_to_bottom[1]->GetId()); |
| |
| // 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.clear(); |
| CallGetChildren(event->node_data[0].get(), &top_to_bottom); |
| ASSERT_EQ(2U, top_to_bottom.size()); |
| EXPECT_EQ(1, top_to_bottom[0]->GetId()); |
| EXPECT_EQ(2, top_to_bottom[1]->GetId()); |
| |
| // 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<ArcAccessibilityInfoData*> dimension; |
| CallGetChildren(event->node_data[0].get(), &dimension); |
| ASSERT_EQ(2U, dimension.size()); |
| EXPECT_EQ(2, dimension[0]->GetId()); |
| EXPECT_EQ(1, dimension[1]->GetId()); |
| |
| button1->bounds_in_screen = gfx::Rect(100, 100, 10, 100); |
| button2->bounds_in_screen = gfx::Rect(100, 100, 100, 100); |
| CallNotifyAccessibilityEvent(event.get()); |
| dimension.clear(); |
| CallGetChildren(event->node_data[0].get(), &dimension); |
| ASSERT_EQ(2U, dimension.size()); |
| EXPECT_EQ(2, dimension[0]->GetId()); |
| EXPECT_EQ(1, dimension[1]->GetId()); |
| |
| // 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.clear(); |
| CallGetChildren(event->node_data[0].get(), &dimension); |
| ASSERT_EQ(2U, dimension.size()); |
| EXPECT_EQ(1, dimension[0]->GetId()); |
| EXPECT_EQ(2, dimension[1]->GetId()); |
| |
| button1->bounds_in_screen = gfx::Rect(100, 100, 100, 100); |
| button2->bounds_in_screen = gfx::Rect(100, 100, 10, 100); |
| CallNotifyAccessibilityEvent(event.get()); |
| dimension.clear(); |
| CallGetChildren(event->node_data[0].get(), &dimension); |
| ASSERT_EQ(2U, dimension.size()); |
| EXPECT_EQ(1, dimension[0]->GetId()); |
| EXPECT_EQ(2, dimension[1]->GetId()); |
| |
| EXPECT_EQ(10, GetDispatchedEventCount(ax::mojom::Event::kFocus)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, AccessibleNameComputationTextField) { |
| auto event = AXEventData::New(); |
| event->source_id = 0; |
| event->task_id = 1; |
| event->event_type = AXEventType::VIEW_FOCUSED; |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* root = event->node_data.back().get(); |
| root->id = 0; |
| |
| std::unique_ptr<ui::AXNodeData> data; |
| SetProperty(root, AXStringProperty::CLASS_NAME, ""); |
| SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({1})); |
| |
| // Child. |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child1 = event->node_data.back().get(); |
| child1->id = 1; |
| SetProperty(child1, AXIntListProperty::CHILD_NODE_IDS, |
| std::vector<int>({2, 3})); |
| |
| // Second child. |
| // This test requires two children. |
| // see SerializeNode function in arc/a11y/ax_tree_source_arc.cc |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child2 = event->node_data.back().get(); |
| child2->id = 2; |
| |
| // Third child. |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child3 = event->node_data.back().get(); |
| child3->id = 3; |
| |
| // Populate the tree source with the data. |
| CallNotifyAccessibilityEvent(event.get()); |
| |
| SetProperty(child2, AXBooleanProperty::EDITABLE, true); |
| SetProperty(child2, AXStringProperty::TEXT, "foo@example.com"); |
| SetProperty(child2, AXStringProperty::CONTENT_DESCRIPTION, |
| "Type your email here."); |
| |
| CallSerializeNode(child2, &data); |
| |
| std::string prop; |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &prop)); |
| EXPECT_EQ("Type your email here.", prop); |
| |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kValue, &prop)); |
| EXPECT_EQ("foo@example.com", prop); |
| |
| ASSERT_FALSE(data->GetStringAttribute( |
| ax::mojom::StringAttribute::kDescription, &prop)); |
| |
| // Case for when text property is empty. |
| SetProperty(child3, AXBooleanProperty::EDITABLE, true); |
| SetProperty(child3, AXStringProperty::CONTENT_DESCRIPTION, |
| "Type your email here."); |
| |
| CallSerializeNode(child3, &data); |
| |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &prop)); |
| EXPECT_EQ("Type your email here.", prop); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kValue, &prop)); |
| ASSERT_FALSE(data->GetStringAttribute( |
| ax::mojom::StringAttribute::kDescription, &prop)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) { |
| auto event = AXEventData::New(); |
| event->source_id = 0; |
| event->task_id = 1; |
| event->event_type = AXEventType::VIEW_FOCUSED; |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* root = event->node_data.back().get(); |
| root->id = 0; |
| SetProperty(root, AXStringProperty::CLASS_NAME, ""); |
| SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, |
| std::vector<int>({1, 2})); |
| |
| // Add child node. |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child1 = event->node_data.back().get(); |
| child1->id = 1; |
| |
| // Add another child. |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child2 = event->node_data.back().get(); |
| child2->id = 2; |
| |
| // Populate the tree source with the data. |
| CallNotifyAccessibilityEvent(event.get()); |
| |
| // No attributes. |
| std::unique_ptr<ui::AXNodeData> data; |
| CallSerializeNode(root, &data); |
| std::string name; |
| ASSERT_FALSE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| |
| // Text (empty). |
| SetProperty(root, AXStringProperty::TEXT, ""); |
| |
| CallSerializeNode(root, &data); |
| // With crrev/1786363, empty text on node will not set the name. |
| ASSERT_FALSE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| |
| // Text (non-empty). |
| root->string_properties->clear(); |
| SetProperty(root, AXStringProperty::TEXT, "label text"); |
| |
| CallSerializeNode(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("label text", name); |
| |
| // Content description (empty), text (non-empty). |
| SetProperty(root, AXStringProperty::CONTENT_DESCRIPTION, ""); |
| |
| CallSerializeNode(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("label text", name); |
| |
| // Content description (non-empty), text (non-empty). |
| root->string_properties.value()[AXStringProperty::CONTENT_DESCRIPTION] = |
| "label content description"; |
| |
| CallSerializeNode(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("label content description label text", name); |
| |
| // Name from contents. |
| |
| // Root node has no name, but has descendants with name. |
| root->string_properties->clear(); |
| // Name from contents only happens if a node is clickable. |
| SetProperty(root, AXBooleanProperty::CLICKABLE, true); |
| SetProperty(child1, AXStringProperty::TEXT, "child1 label text"); |
| SetProperty(child2, AXStringProperty::TEXT, "child2 label text"); |
| |
| CallSerializeNode(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| ASSERT_EQ("child1 label text child2 label text", name); |
| |
| // If the node has a name, it should override the contents. |
| SetProperty(root, AXStringProperty::TEXT, "root label text"); |
| |
| CallSerializeNode(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| ASSERT_EQ("root label text", name); |
| |
| // Clearing both clickable and name from root, the name should not be |
| // populated. |
| root->boolean_properties->clear(); |
| root->string_properties->clear(); |
| CallSerializeNode(root, &data); |
| ASSERT_FALSE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| |
| EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, AccessibleNameComputationWindow) { |
| auto event = AXEventData::New(); |
| event->source_id = 0; |
| 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 = 0; |
| |
| CallNotifyAccessibilityEvent(event.get()); |
| |
| // Live edit name related attributes. |
| |
| // No attributes. |
| std::unique_ptr<ui::AXNodeData> data; |
| CallSerializeWindow(root, &data); |
| std::string name; |
| ASSERT_FALSE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| |
| // Title attribute |
| SetProperty(root, AXWindowStringProperty::TITLE, "window title"); |
| CallSerializeWindow(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("window title", name); |
| |
| EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, 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 = 0; |
| 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"); |
| |
| // 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"); |
| |
| // 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()); |
| std::unique_ptr<ui::AXNodeData> data; |
| std::string name; |
| |
| CallSerializeWindow(root, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("window title", name); |
| EXPECT_NE(ax::mojom::Role::kRootWebArea, data->role); |
| |
| CallSerializeWindow(child, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("child window title", name); |
| EXPECT_NE(ax::mojom::Role::kRootWebArea, data->role); |
| |
| CallSerializeNode(node, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("node text", name); |
| EXPECT_EQ(ax::mojom::Role::kGenericContainer, data->role); |
| EXPECT_TRUE(data->GetBoolAttribute(ax::mojom::BoolAttribute::kModal)); |
| |
| CallSerializeNode(child_node, &data); |
| ASSERT_TRUE( |
| data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); |
| EXPECT_EQ("child node text", name); |
| EXPECT_NE(ax::mojom::Role::kRootWebArea, data->role); |
| |
| CallSerializeWindow(child2, &data); |
| 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)); |
| } |
| |
| // TODO(katie): Maybe remove this test when adding AccessibilityWindowInfoData |
| // support per go/a11y-arc++-window-mapping if it is no longer needed. This |
| // depends on if we can assume that each tree has exactly one root. |
| TEST_F(AXTreeSourceArcTest, MultipleNodeSubtrees) { |
| // Run several times to try source_id from root, middle, or leaf of the tree. |
| int tree_size = 4; |
| for (int i = 0; i < 4; i++) { |
| auto event = AXEventData::New(); |
| event->source_id = i + tree_size; |
| event->task_id = 1; |
| event->event_type = AXEventType::VIEW_FOCUSED; |
| |
| // Make three non-overlapping trees. The middle tree in the list has the |
| // source_id of interest. Each tree has a root with one child, and that |
| // child has two leaf children. |
| int num_trees = 3; |
| for (int j = 0; j < num_trees; j++) { |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* root = event->node_data.back().get(); |
| root->id = j * tree_size; |
| SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, |
| std::vector<int>({j * tree_size + 1})); |
| |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child1 = event->node_data.back().get(); |
| child1->id = j * tree_size + 1; |
| SetProperty(child1, AXIntListProperty::CHILD_NODE_IDS, |
| std::vector<int>({j * tree_size + 2, j * tree_size + 3})); |
| |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child2 = event->node_data.back().get(); |
| child2->id = j * tree_size + 2; |
| |
| event->node_data.push_back(AXNodeInfoData::New()); |
| AXNodeInfoData* child3 = event->node_data.back().get(); |
| child3->id = j * tree_size + 3; |
| } |
| |
| CallNotifyAccessibilityEvent(event.get()); |
| |
| // Check that only the middle tree was added, and that it is correct. |
| std::vector<ArcAccessibilityInfoData*> children; |
| CallGetChildren(event->node_data.at(tree_size).get(), &children); |
| ASSERT_EQ(1U, children.size()); |
| EXPECT_EQ(5, children[0]->GetId()); |
| children.clear(); |
| CallGetChildren(event->node_data.at(tree_size + 1).get(), &children); |
| ASSERT_EQ(2U, children.size()); |
| EXPECT_EQ(6, children[0]->GetId()); |
| EXPECT_EQ(7, children[1]->GetId()); |
| |
| // The first and third roots are not part of the tree. |
| EXPECT_EQ(nullptr, CallGetFromId(0)); |
| EXPECT_EQ(nullptr, CallGetFromId(1)); |
| EXPECT_EQ(nullptr, CallGetFromId(2)); |
| EXPECT_EQ(nullptr, CallGetFromId(3)); |
| EXPECT_EQ(nullptr, CallGetFromId(8)); |
| EXPECT_EQ(nullptr, CallGetFromId(9)); |
| EXPECT_EQ(nullptr, CallGetFromId(10)); |
| EXPECT_EQ(nullptr, CallGetFromId(11)); |
| } |
| |
| EXPECT_EQ(4, GetDispatchedEventCount(ax::mojom::Event::kFocus)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, 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<ArcAccessibilityInfoData*> children; |
| for (int i = 0; i < num_trees; i++) { |
| CallGetChildren(event->node_data.at(i * tree_size).get(), &children); |
| ASSERT_EQ(1U, children.size()); |
| EXPECT_EQ(i * tree_size + 2, children[0]->GetId()); |
| children.clear(); |
| CallGetChildren(event->node_data.at(i * tree_size + 1).get(), &children); |
| ASSERT_EQ(2U, children.size()); |
| EXPECT_EQ(i * tree_size + 3, children[0]->GetId()); |
| EXPECT_EQ(i * tree_size + 4, children[1]->GetId()); |
| children.clear(); |
| } |
| EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, GetTreeDataAppliesFocus) { |
| auto event = AXEventData::New(); |
| event->source_id = 5; |
| 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}); |
| |
| // Add a child window. |
| event->window_data->push_back(AXWindowInfoData::New()); |
| AXWindowInfoData* child = event->window_data->back().get(); |
| child->window_id = 1; |
| |
| CallNotifyAccessibilityEvent(event.get()); |
| ui::AXTreeData data; |
| |
| // Nothing should be focused when there are no nodes. |
| EXPECT_TRUE(CallGetTreeData(&data)); |
| EXPECT_EQ(ui::AXNode::kInvalidAXID, data.focus_id); |
| |
| // 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; |
| |
| CallNotifyAccessibilityEvent(event.get()); |
| |
| EXPECT_TRUE(CallGetTreeData(&data)); |
| EXPECT_EQ(2, data.focus_id); |
| |
| EXPECT_EQ(2, GetDispatchedEventCount(ax::mojom::Event::kFocus)); |
| } |
| |
| TEST_F(AXTreeSourceArcTest, EventTypeForViewSelected) { |
| auto event = AXEventData::New(); |
| event->source_id = 0; |
| event->task_id = 1; |
| event->event_type = AXEventType::VIEW_SELECTED; |
| |
| event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>(); |
| event->window_data->emplace_back(AXWindowInfoData::New()); |
| AXWindowInfoData* root_window = event->window_data->back().get(); |
| root_window->window_id = 100; |
| root_window->root_node_id = 0; |
| |
| event->node_data.emplace_back(AXNodeInfoData::New()); |
| AXNodeInfoData* root = event->node_data.back().get(); |
| root->id = 0; |
| SetProperty(root, AXIntListProperty::CHILD_NODE_IDS, |
| std::vector<int>({1, 2})); |
| |
| // Add child node. |
| event->node_data.emplace_back(AXNodeInfoData::New()); |
| AXNodeInfoData* button1 = event->node_data.back().get(); |
| button1->id = 1; |
| SetProperty(button1, AXBooleanProperty::FOCUSABLE, true); |
| |
| // Add another child with range_info. |
| event->node_data.emplace_back(AXNodeInfoData::New()); |
| AXNodeInfoData* button2 = event->node_data.back().get(); |
| button2->id = 2; |
| button2->range_info = AXRangeInfoData::New(); |
| SetProperty(button2, AXBooleanProperty::FOCUSABLE, true); |
| |
| // Without range_info, kSelection event should be emitted. Usually this event |
| // is fired from AdapterView. |
| SetProperty(button1, AXBooleanProperty::FOCUSED, true); |
| SetProperty(button2, AXBooleanProperty::FOCUSED, false); |
| CallNotifyAccessibilityEvent(event.get()); |
| EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kSelection)); |
| |
| // Set range_info. Should be kValueChanged. |
| SetProperty(button1, AXBooleanProperty::FOCUSED, false); |
| SetProperty(button2, AXBooleanProperty::FOCUSED, true); |
| CallNotifyAccessibilityEvent(event.get()); |
| EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kValueChanged)); |
| } |
| |
| } // namespace arc |