1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/accessibility/test_ax_node_helper.h"
6 
7 #include <map>
8 #include <utility>
9 
10 #include "base/numerics/ranges.h"
11 #include "base/stl_util.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "ui/accessibility/ax_action_data.h"
14 #include "ui/accessibility/ax_role_properties.h"
15 #include "ui/accessibility/ax_table_info.h"
16 #include "ui/accessibility/ax_tree_observer.h"
17 #include "ui/gfx/geometry/rect_conversions.h"
18 
19 namespace ui {
20 
21 namespace {
22 
23 // A global map from AXNodes to TestAXNodeHelpers.
24 std::map<AXNode::AXID, TestAXNodeHelper*> g_node_id_to_helper_map;
25 
26 // A simple implementation of AXTreeObserver to catch when AXNodes are
27 // deleted so we can delete their helpers.
28 class TestAXTreeObserver : public AXTreeObserver {
29  private:
OnNodeDeleted(AXTree * tree,int32_t node_id)30   void OnNodeDeleted(AXTree* tree, int32_t node_id) override {
31     const auto iter = g_node_id_to_helper_map.find(node_id);
32     if (iter != g_node_id_to_helper_map.end()) {
33       TestAXNodeHelper* helper = iter->second;
34       delete helper;
35       g_node_id_to_helper_map.erase(node_id);
36     }
37   }
38 };
39 
40 TestAXTreeObserver g_ax_tree_observer;
41 
42 }  // namespace
43 
44 // static
GetOrCreate(AXTree * tree,AXNode * node)45 TestAXNodeHelper* TestAXNodeHelper::GetOrCreate(AXTree* tree, AXNode* node) {
46   if (!tree || !node)
47     return nullptr;
48 
49   if (!tree->HasObserver(&g_ax_tree_observer))
50     tree->AddObserver(&g_ax_tree_observer);
51   auto iter = g_node_id_to_helper_map.find(node->id());
52   if (iter != g_node_id_to_helper_map.end())
53     return iter->second;
54   TestAXNodeHelper* helper = new TestAXNodeHelper(tree, node);
55   g_node_id_to_helper_map[node->id()] = helper;
56   return helper;
57 }
58 
TestAXNodeHelper(AXTree * tree,AXNode * node)59 TestAXNodeHelper::TestAXNodeHelper(AXTree* tree, AXNode* node)
60     : tree_(tree), node_(node) {}
61 
62 TestAXNodeHelper::~TestAXNodeHelper() = default;
63 
GetBoundsRect(const AXCoordinateSystem coordinate_system,const AXClippingBehavior clipping_behavior,AXOffscreenResult * offscreen_result) const64 gfx::Rect TestAXNodeHelper::GetBoundsRect(
65     const AXCoordinateSystem coordinate_system,
66     const AXClippingBehavior clipping_behavior,
67     AXOffscreenResult* offscreen_result) const {
68   switch (coordinate_system) {
69     case AXCoordinateSystem::kScreenPhysicalPixels:
70       // For unit testing purposes, assume a device scale factor of 1 and fall
71       // through.
72     case AXCoordinateSystem::kScreenDIPs: {
73       // We could optionally add clipping here if ever needed.
74       gfx::RectF bounds = GetLocation();
75 
76       // For test behavior only, for bounds that are offscreen we currently do
77       // not apply clipping to the bounds but we still return the offscreen
78       // status.
79       if (offscreen_result) {
80         *offscreen_result = DetermineOffscreenResult(bounds);
81       }
82 
83       return gfx::ToEnclosingRect(bounds);
84     }
85     case AXCoordinateSystem::kRootFrame:
86     case AXCoordinateSystem::kFrame:
87       NOTIMPLEMENTED();
88       return gfx::Rect();
89   }
90 }
91 
GetInnerTextRangeBoundsRect(const int start_offset,const int end_offset,const AXCoordinateSystem coordinate_system,const AXClippingBehavior clipping_behavior,AXOffscreenResult * offscreen_result) const92 gfx::Rect TestAXNodeHelper::GetInnerTextRangeBoundsRect(
93     const int start_offset,
94     const int end_offset,
95     const AXCoordinateSystem coordinate_system,
96     const AXClippingBehavior clipping_behavior,
97     AXOffscreenResult* offscreen_result) const {
98   switch (coordinate_system) {
99     case AXCoordinateSystem::kScreenPhysicalPixels:
100     // For unit testing purposes, assume a device scale factor of 1 and fall
101     // through.
102     case AXCoordinateSystem::kScreenDIPs: {
103       gfx::RectF bounds = GetLocation();
104       // This implementation currently only deals with text node that has role
105       // kInlineTextBox and kStaticText.
106       // For test purposes, assume node with kStaticText always has a single
107       // child with role kInlineTextBox.
108       if (GetData().role == ax::mojom::Role::kInlineTextBox) {
109         bounds = GetInlineTextRect(start_offset, end_offset);
110       } else if (GetData().role == ax::mojom::Role::kStaticText &&
111                  InternalChildCount() > 0) {
112         TestAXNodeHelper* child = InternalGetChild(0);
113         if (child != nullptr &&
114             child->GetData().role == ax::mojom::Role::kInlineTextBox) {
115           bounds = child->GetInlineTextRect(start_offset, end_offset);
116         }
117       }
118 
119       // For test behavior only, for bounds that are offscreen we currently do
120       // not apply clipping to the bounds but we still return the offscreen
121       // status.
122       if (offscreen_result) {
123         *offscreen_result = DetermineOffscreenResult(bounds);
124       }
125 
126       return gfx::ToEnclosingRect(bounds);
127     }
128     case AXCoordinateSystem::kRootFrame:
129     case AXCoordinateSystem::kFrame:
130       NOTIMPLEMENTED();
131       return gfx::Rect();
132   }
133 }
134 
GetData() const135 const AXNodeData& TestAXNodeHelper::GetData() const {
136   return node_->data();
137 }
138 
GetLocation() const139 gfx::RectF TestAXNodeHelper::GetLocation() const {
140   return GetData().relative_bounds.bounds;
141 }
142 
InternalChildCount() const143 int TestAXNodeHelper::InternalChildCount() const {
144   return int{node_->GetUnignoredChildCount()};
145 }
146 
InternalGetChild(int index) const147 TestAXNodeHelper* TestAXNodeHelper::InternalGetChild(int index) const {
148   CHECK_GE(index, 0);
149   CHECK_LT(index, InternalChildCount());
150   return GetOrCreate(tree_, node_->GetUnignoredChildAtIndex(size_t{index}));
151 }
152 
GetInlineTextRect(const int start_offset,const int end_offset) const153 gfx::RectF TestAXNodeHelper::GetInlineTextRect(const int start_offset,
154                                                const int end_offset) const {
155   DCHECK(start_offset >= 0 && end_offset >= 0 && start_offset <= end_offset);
156   const std::vector<int32_t>& character_offsets = GetData().GetIntListAttribute(
157       ax::mojom::IntListAttribute::kCharacterOffsets);
158   gfx::RectF location = GetLocation();
159   gfx::RectF bounds;
160 
161   switch (static_cast<ax::mojom::WritingDirection>(
162       GetData().GetIntAttribute(ax::mojom::IntAttribute::kTextDirection))) {
163     // Currently only kNone and kLtr are supported text direction.
164     case ax::mojom::WritingDirection::kNone:
165     case ax::mojom::WritingDirection::kLtr: {
166       int start_pixel_offset =
167           start_offset > 0 ? character_offsets[start_offset - 1] : location.x();
168       int end_pixel_offset =
169           end_offset > 0 ? character_offsets[end_offset - 1] : location.x();
170       bounds =
171           gfx::RectF(start_pixel_offset, location.y(),
172                      end_pixel_offset - start_pixel_offset, location.height());
173       break;
174     }
175     default:
176       NOTIMPLEMENTED();
177   }
178   return bounds;
179 }
180 
DetermineOffscreenResult(gfx::RectF bounds) const181 AXOffscreenResult TestAXNodeHelper::DetermineOffscreenResult(
182     gfx::RectF bounds) const {
183   if (!tree_ || !tree_->root())
184     return AXOffscreenResult::kOnscreen;
185 
186   const AXNodeData& root_web_area_node_data = tree_->root()->data();
187   gfx::RectF root_web_area_bounds =
188       root_web_area_node_data.relative_bounds.bounds;
189 
190   // For testing, we only look at the current node's bound relative to the root
191   // web area bounds to determine offscreen status. We currently do not look at
192   // the bounds of the immediate parent of the node for determining offscreen
193   // status.
194   // We only determine offscreen result if the root web area bounds is actually
195   // set in the test. We default the offscreen result of every other situation
196   // to AXOffscreenResult::kOnscreen.
197   if (!root_web_area_bounds.IsEmpty()) {
198     bounds.Intersect(root_web_area_bounds);
199     if (bounds.IsEmpty())
200       return AXOffscreenResult::kOffscreen;
201   }
202   return AXOffscreenResult::kOnscreen;
203 }
204 }  // namespace ui
205