1 // Copyright 2019 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 "fuchsia/engine/browser/ax_tree_converter.h"
6
7 #include <lib/ui/scenic/cpp/commands.h>
8 #include <vector>
9
10 #include "testing/gtest/include/gtest/gtest.h"
11 #include "ui/gfx/transform.h"
12
13 namespace {
14
15 using fuchsia::accessibility::semantics::Action;
16 using fuchsia::accessibility::semantics::Attributes;
17 using fuchsia::accessibility::semantics::CheckedState;
18 using fuchsia::accessibility::semantics::Node;
19 using fuchsia::accessibility::semantics::Role;
20 using fuchsia::accessibility::semantics::States;
21
22 const char kLabel1[] = "label nodes, not people";
23 const char kLabel2[] = "fancy stickers";
24 const char kDescription1[] = "this node does some stuff";
25 const char kValue1[] = "user entered value";
26 const int32_t kChildId1 = 23901;
27 const int32_t kChildId2 = 484345;
28 const int32_t kChildId3 = 4156877;
29 const int32_t kRectX = 1;
30 const int32_t kRectY = 2;
31 const int32_t kRectWidth = 7;
32 const int32_t kRectHeight = 8;
33 const std::array<float, 16> k4DIdentityMatrix = {1, 0, 0, 0, 0, 1, 0, 0,
34 0, 0, 1, 0, 0, 0, 0, 1};
35
36 class MockNodeIDMapper : public NodeIDMapper {
37 public:
38 MockNodeIDMapper() = default;
39 ~MockNodeIDMapper() override = default;
40
ToFuchsiaNodeID(const ui::AXTreeID & ax_tree_id,int32_t ax_node_id,bool is_tree_root)41 uint32_t ToFuchsiaNodeID(const ui::AXTreeID& ax_tree_id,
42 int32_t ax_node_id,
43 bool is_tree_root) override {
44 return base::checked_cast<uint32_t>(ax_node_id);
45 }
46 };
47
CreateAXNodeData(ax::mojom::Role role,ax::mojom::Action action,std::vector<int32_t> child_ids,ui::AXRelativeBounds relative_bounds,base::StringPiece name,base::StringPiece description,ax::mojom::CheckedState checked_state)48 ui::AXNodeData CreateAXNodeData(ax::mojom::Role role,
49 ax::mojom::Action action,
50 std::vector<int32_t> child_ids,
51 ui::AXRelativeBounds relative_bounds,
52 base::StringPiece name,
53 base::StringPiece description,
54 ax::mojom::CheckedState checked_state) {
55 ui::AXNodeData node;
56 node.id = 2;
57 node.role = role;
58 node.AddAction(action);
59 node.AddIntAttribute(ax::mojom::IntAttribute::kCheckedState,
60 static_cast<int32_t>(checked_state));
61 node.child_ids = child_ids;
62 node.relative_bounds = relative_bounds;
63 if (!name.empty())
64 node.AddStringAttribute(ax::mojom::StringAttribute::kName, name.data());
65 if (!description.empty()) {
66 node.AddStringAttribute(ax::mojom::StringAttribute::kDescription,
67 description.data());
68 }
69 return node;
70 }
71
CreateSemanticNode(uint32_t id,Role role,Attributes attributes,States states,std::vector<Action> actions,std::vector<uint32_t> child_ids,fuchsia::ui::gfx::BoundingBox location,fuchsia::ui::gfx::mat4 transform)72 Node CreateSemanticNode(uint32_t id,
73 Role role,
74 Attributes attributes,
75 States states,
76 std::vector<Action> actions,
77 std::vector<uint32_t> child_ids,
78 fuchsia::ui::gfx::BoundingBox location,
79 fuchsia::ui::gfx::mat4 transform) {
80 Node node;
81 node.set_node_id(id);
82 node.set_role(role);
83 node.set_attributes(std::move(attributes));
84 node.set_states(std::move(states));
85 node.set_actions(actions);
86 node.set_child_ids(child_ids);
87 node.set_location(location);
88 node.set_transform(transform);
89 return node;
90 }
91
92 // Create an AXNodeData and a Fuchsia node that represent the same information.
CreateSemanticNodeAllFieldsSet()93 std::pair<ui::AXNodeData, Node> CreateSemanticNodeAllFieldsSet() {
94 ui::AXRelativeBounds relative_bounds = ui::AXRelativeBounds();
95 relative_bounds.bounds = gfx::RectF(kRectX, kRectY, kRectWidth, kRectHeight);
96 relative_bounds.transform =
97 std::make_unique<gfx::Transform>(gfx::Transform::kSkipInitialization);
98 relative_bounds.transform->MakeIdentity();
99 auto ax_node_data = CreateAXNodeData(
100 ax::mojom::Role::kButton, ax::mojom::Action::kFocus,
101 std::vector<int32_t>{kChildId1, kChildId2, kChildId3}, relative_bounds,
102 kLabel1, kDescription1, ax::mojom::CheckedState::kMixed);
103 ax_node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, false);
104 ax_node_data.AddIntAttribute(ax::mojom::IntAttribute::kScrollX, 10);
105 ax_node_data.RemoveState(ax::mojom::State::kIgnored);
106 ax_node_data.AddIntAttribute(ax::mojom::IntAttribute::kScrollY, 20);
107 ax_node_data.id = kChildId1;
108
109 Attributes attributes;
110 attributes.set_label(kLabel1);
111 attributes.set_secondary_label(kDescription1);
112 fuchsia::ui::gfx::BoundingBox box;
113 box.min = scenic::NewVector3({kRectX, kRectY, 0.0f});
114 box.max =
115 scenic::NewVector3({kRectX + kRectWidth, kRectY + kRectHeight, 0.0f});
116 fuchsia::ui::gfx::Matrix4Value mat =
117 scenic::NewMatrix4Value(k4DIdentityMatrix);
118 States states;
119 states.set_checked_state(CheckedState::MIXED);
120 states.set_hidden(false);
121 states.set_selected(false);
122 states.set_viewport_offset({10, 20});
123 MockNodeIDMapper mapper;
124 auto fuchsia_node = CreateSemanticNode(
125 mapper.ToFuchsiaNodeID(ui::AXTreeID::CreateNewAXTreeID(), ax_node_data.id,
126 false),
127 Role::BUTTON, std::move(attributes), std::move(states),
128 std::vector<Action>{Action::SET_FOCUS},
129 std::vector<uint32_t>{kChildId1, kChildId2, kChildId3}, box, mat.value);
130
131 return std::make_pair(std::move(ax_node_data), std::move(fuchsia_node));
132 }
133
134 class AXTreeConverterTest : public testing::Test {
135 public:
136 AXTreeConverterTest() = default;
137 ~AXTreeConverterTest() override = default;
138
139 DISALLOW_COPY_AND_ASSIGN(AXTreeConverterTest);
140 };
141
TEST_F(AXTreeConverterTest,AllFieldsSetAndEqual)142 TEST_F(AXTreeConverterTest, AllFieldsSetAndEqual) {
143 auto nodes = CreateSemanticNodeAllFieldsSet();
144 auto& source_node_data = nodes.first;
145 auto& expected_node = nodes.second;
146
147 MockNodeIDMapper mapper;
148 auto converted_node = AXNodeDataToSemanticNode(
149 source_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
150 EXPECT_TRUE(fidl::Equals(converted_node, expected_node));
151 }
152
TEST_F(AXTreeConverterTest,SomeFieldsSetAndEqual)153 TEST_F(AXTreeConverterTest, SomeFieldsSetAndEqual) {
154 ui::AXNodeData source_node_data;
155 source_node_data.id = 0;
156 source_node_data.AddAction(ax::mojom::Action::kFocus);
157 source_node_data.AddAction(ax::mojom::Action::kSetValue);
158 source_node_data.child_ids = std::vector<int32_t>{kChildId1};
159 source_node_data.role = ax::mojom::Role::kImage;
160 source_node_data.AddStringAttribute(ax::mojom::StringAttribute::kValue,
161 kValue1);
162
163 MockNodeIDMapper mapper;
164 auto converted_node = AXNodeDataToSemanticNode(
165 source_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
166
167 Node expected_node;
168 expected_node.set_node_id(0);
169 expected_node.set_actions(
170 std::vector<Action>{Action::SET_FOCUS, Action::SET_VALUE});
171 expected_node.set_child_ids(std::vector<uint32_t>{kChildId1});
172 expected_node.set_role(Role::IMAGE);
173 States states;
174 states.set_hidden(false);
175 states.set_value(kValue1);
176 expected_node.set_states(std::move(states));
177 Attributes attributes;
178 expected_node.set_attributes(std::move(attributes));
179 fuchsia::ui::gfx::BoundingBox box;
180 expected_node.set_location(std::move(box));
181
182 EXPECT_TRUE(fidl::Equals(converted_node, expected_node));
183 }
184
TEST_F(AXTreeConverterTest,FieldMismatch)185 TEST_F(AXTreeConverterTest, FieldMismatch) {
186 ui::AXRelativeBounds relative_bounds = ui::AXRelativeBounds();
187 relative_bounds.bounds = gfx::RectF(kRectX, kRectY, kRectWidth, kRectHeight);
188 relative_bounds.transform =
189 std::make_unique<gfx::Transform>(gfx::Transform::kSkipInitialization);
190 relative_bounds.transform->MakeIdentity();
191 auto source_node_data = CreateAXNodeData(
192 ax::mojom::Role::kHeader, ax::mojom::Action::kSetValue,
193 std::vector<int32_t>{kChildId1, kChildId2, kChildId3}, relative_bounds,
194 kLabel1, kDescription1, ax::mojom::CheckedState::kFalse);
195
196 MockNodeIDMapper mapper;
197 auto converted_node = AXNodeDataToSemanticNode(
198 source_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
199
200 Attributes attributes;
201 attributes.set_label(kLabel1);
202 attributes.set_secondary_label(kDescription1);
203 States states;
204 states.set_hidden(false);
205 states.set_checked_state(CheckedState::UNCHECKED);
206 fuchsia::ui::gfx::BoundingBox box;
207 box.min = scenic::NewVector3({kRectX, kRectY, 0.0f});
208 box.max =
209 scenic::NewVector3({kRectX + kRectWidth, kRectY + kRectHeight, 0.0f});
210 fuchsia::ui::gfx::Matrix4Value mat =
211 scenic::NewMatrix4Value(k4DIdentityMatrix);
212 auto expected_node = CreateSemanticNode(
213 source_node_data.id, Role::HEADER, std::move(attributes),
214 std::move(states), std::vector<Action>{Action::SET_VALUE},
215 std::vector<uint32_t>{kChildId1, kChildId2, kChildId3}, box, mat.value);
216
217 // Start with nodes being equal.
218 EXPECT_TRUE(fidl::Equals(converted_node, expected_node));
219
220 // Make a copy of |source_node_data| and change the name attribute. Check that
221 // the resulting |converted_node| is different from |expected_node|.
222 auto modified_node_data = source_node_data;
223 modified_node_data.AddStringAttribute(ax::mojom::StringAttribute::kName,
224 kLabel2);
225
226 converted_node = AXNodeDataToSemanticNode(
227 modified_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
228 EXPECT_FALSE(fidl::Equals(converted_node, expected_node));
229
230 // The same as above, this time changing |child_ids|.
231 modified_node_data = source_node_data;
232 modified_node_data.child_ids = std::vector<int32_t>{};
233 converted_node = AXNodeDataToSemanticNode(
234 modified_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
235 EXPECT_FALSE(fidl::Equals(converted_node, expected_node));
236 }
237
TEST_F(AXTreeConverterTest,LocationFieldRespectsTypeInvariants)238 TEST_F(AXTreeConverterTest, LocationFieldRespectsTypeInvariants) {
239 ui::AXRelativeBounds relative_bounds = ui::AXRelativeBounds();
240 relative_bounds.bounds = gfx::RectF(kRectX, kRectY, kRectWidth, kRectHeight);
241 relative_bounds.transform =
242 std::make_unique<gfx::Transform>(gfx::Transform::kSkipInitialization);
243 relative_bounds.transform->MakeIdentity();
244 auto source_node_data = CreateAXNodeData(
245 ax::mojom::Role::kHeader, ax::mojom::Action::kSetValue,
246 std::vector<int32_t>{kChildId1, kChildId2, kChildId3}, relative_bounds,
247 kLabel1, kDescription1, ax::mojom::CheckedState::kFalse);
248
249 MockNodeIDMapper mapper;
250 auto converted_node = AXNodeDataToSemanticNode(
251 source_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
252
253 // The type definition of the location field requires that in order to be
254 // interpreted as having non-zero length in a dimension, the min must be less
255 // than the max in that dimension.
256 EXPECT_LE(converted_node.location().min.x, converted_node.location().max.x);
257 EXPECT_LE(converted_node.location().min.y, converted_node.location().max.y);
258 EXPECT_LE(converted_node.location().min.z, converted_node.location().max.z);
259 }
260
TEST_F(AXTreeConverterTest,DefaultAction)261 TEST_F(AXTreeConverterTest, DefaultAction) {
262 auto nodes = CreateSemanticNodeAllFieldsSet();
263 auto& source_node_data = nodes.first;
264 auto& expected_node = nodes.second;
265
266 // Default action verb on an AXNodeData is equivalent to Action::DEFAULT on a
267 // Fuchsia semantic node.
268 source_node_data.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
269 expected_node.mutable_actions()->insert(
270 expected_node.mutable_actions()->begin(),
271 fuchsia::accessibility::semantics::Action::DEFAULT);
272
273 MockNodeIDMapper mapper;
274 auto converted_node = AXNodeDataToSemanticNode(
275 source_node_data, ui::AXTreeID::CreateNewAXTreeID(), false, &mapper);
276
277 EXPECT_TRUE(fidl::Equals(converted_node, expected_node));
278 }
279
TEST_F(AXTreeConverterTest,MapsNodeIDs)280 TEST_F(AXTreeConverterTest, MapsNodeIDs) {
281 NodeIDMapper mapper;
282 const ui::AXTreeID tree_id_1 = ui::AXTreeID::CreateNewAXTreeID();
283 const ui::AXTreeID tree_id_2 = ui::AXTreeID::CreateNewAXTreeID();
284 const ui::AXTreeID tree_id_3 = ui::AXTreeID::CreateNewAXTreeID();
285
286 auto id = mapper.ToFuchsiaNodeID(tree_id_1, 1, false);
287 EXPECT_EQ(id, 1u);
288
289 id = mapper.ToFuchsiaNodeID(tree_id_2, 1, false);
290 EXPECT_EQ(id, 2u);
291
292 const auto result_1 = mapper.ToAXNodeID(1u);
293 EXPECT_TRUE(result_1);
294 EXPECT_EQ(result_1->first, tree_id_1);
295 EXPECT_EQ(result_1->second, 1);
296
297 const auto result_2 = mapper.ToAXNodeID(2u);
298 EXPECT_TRUE(result_2);
299 EXPECT_EQ(result_2->first, tree_id_2);
300 EXPECT_EQ(result_2->second, 1);
301
302 // Set the root.
303 id = mapper.ToFuchsiaNodeID(tree_id_1, 2, true);
304 EXPECT_EQ(id, 0u);
305
306 // Update the root. The old root should receive a new value.
307 id = mapper.ToFuchsiaNodeID(tree_id_1, 1, true);
308 EXPECT_EQ(id, 0u);
309 const auto result_3 = mapper.ToAXNodeID(3u);
310 EXPECT_TRUE(result_3);
311 EXPECT_EQ(result_3->first, tree_id_1);
312 EXPECT_EQ(result_3->second, 2); // First root's ID.
313
314 mapper.UpdateAXTreeIDForCachedNodeIDs(tree_id_1, tree_id_3);
315 const auto result_4 = mapper.ToAXNodeID(3u);
316 EXPECT_TRUE(result_4);
317 EXPECT_EQ(result_4->first, tree_id_3);
318 EXPECT_EQ(result_4->second, 2);
319 }
320
TEST_F(AXTreeConverterTest,ConvertRoles)321 TEST_F(AXTreeConverterTest, ConvertRoles) {
322 MockNodeIDMapper mapper;
323 ui::AXNodeData node;
324 node.id = 0;
325 node.role = ax::mojom::Role::kButton;
326 EXPECT_EQ(fuchsia::accessibility::semantics::Role::BUTTON,
327 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
328 false, &mapper)
329 .role());
330
331 node.role = ax::mojom::Role::kCheckBox;
332 EXPECT_EQ(fuchsia::accessibility::semantics::Role::CHECK_BOX,
333 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
334 false, &mapper)
335 .role());
336
337 node.role = ax::mojom::Role::kHeader;
338 EXPECT_EQ(fuchsia::accessibility::semantics::Role::HEADER,
339 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
340 false, &mapper)
341 .role());
342
343 node.role = ax::mojom::Role::kImage;
344 EXPECT_EQ(fuchsia::accessibility::semantics::Role::IMAGE,
345 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
346 false, &mapper)
347 .role());
348
349 node.role = ax::mojom::Role::kLink;
350 EXPECT_EQ(fuchsia::accessibility::semantics::Role::LINK,
351 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
352 false, &mapper)
353 .role());
354
355 node.role = ax::mojom::Role::kRadioButton;
356 EXPECT_EQ(fuchsia::accessibility::semantics::Role::RADIO_BUTTON,
357 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
358 false, &mapper)
359 .role());
360
361 node.role = ax::mojom::Role::kSlider;
362 EXPECT_EQ(fuchsia::accessibility::semantics::Role::SLIDER,
363 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
364 false, &mapper)
365 .role());
366
367 node.role = ax::mojom::Role::kTextField;
368 EXPECT_EQ(fuchsia::accessibility::semantics::Role::TEXT_FIELD,
369 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
370 false, &mapper)
371 .role());
372
373 node.role = ax::mojom::Role::kStaticText;
374 EXPECT_EQ(fuchsia::accessibility::semantics::Role::STATIC_TEXT,
375 AXNodeDataToSemanticNode(node, ui::AXTreeID::CreateNewAXTreeID(),
376 false, &mapper)
377 .role());
378 }
379
380 } // namespace
381