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