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