test_ax_tree_update_json_reader.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. // Copyright 2022 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. #include "ui/accessibility/test_ax_tree_update_json_reader.h"
  5. #include "base/containers/contains.h"
  6. #include "base/containers/flat_set.h"
  7. #include "base/numerics/clamped_math.h"
  8. #include "base/strings/string_split.h"
  9. #include "ui/accessibility/ax_enum_util.h"
  10. namespace {
  11. using RoleConversions = const std::map<std::string, ax::mojom::Role>;
  12. // The 3 lists below include all terms that are not parsed now if they are in a
  13. // JSON file. Since this class is only used for testing, we will only encounter
  14. // errors regarding that in the following two cases:
  15. // - We add a new JSON file (or modify one) for testing that includes different
  16. // unsupported.
  17. // - There is a new property added to AXNode that is not covered in the existing
  18. // JSON parsor and a test relies on it.
  19. // In both above cases, existing tests will catch the issue and warn about the
  20. // missing/changed property.
  21. const base::flat_set<std::string> kUnusedAxNodeProperties = {
  22. "controls", "describedby", "details", "disabled", "editable",
  23. "focused", "hidden", "hiddenRoot", "live", "multiline",
  24. "readonly", "relevant", "required", "settable"};
  25. const base::flat_set<std::string> kUnusedAxNodeItems = {
  26. "frameId", "ignoredReasons", "parentId"};
  27. const base::flat_set<std::string> kUnusedStyles = {
  28. "background-image", "background-size", "clip", "font-style",
  29. "margin-bottom", "margin-left", "margin-right", "margin-top",
  30. "opacity", "padding-bottom", "padding-left", "padding-right",
  31. "padding-top", "position", "text-align", "text-decoration",
  32. "z-index"};
  33. int GetAsInt(const base::Value& value) {
  34. if (value.is_int())
  35. return value.GetInt();
  36. if (value.is_string())
  37. return atoi(value.GetString().c_str());
  38. NOTREACHED() << "Unexpected: " << value;
  39. return 0;
  40. }
  41. double GetAsDouble(const base::Value& value) {
  42. if (value.is_double())
  43. return value.GetDouble();
  44. if (value.is_int())
  45. return value.GetInt();
  46. if (value.is_string())
  47. return atof(value.GetString().c_str());
  48. NOTREACHED() << "Unexpected: " << value;
  49. return 0;
  50. }
  51. bool GetAsBoolean(const base::Value& value) {
  52. if (value.is_bool())
  53. return value.GetBool();
  54. if (value.is_string()) {
  55. if (value.GetString() == "false")
  56. return false;
  57. if (value.GetString() == "true")
  58. return true;
  59. }
  60. NOTREACHED() << "Unexpected: " << value;
  61. return false;
  62. }
  63. void GetTypeAndValue(const base::Value& node,
  64. std::string& type,
  65. std::string& value) {
  66. type = node.GetDict().Find("type")->GetString();
  67. value = node.GetDict().Find("value")->GetString();
  68. }
  69. ui::AXNodeID AddNode(ui::AXTreeUpdate& tree_update,
  70. const base::Value& node,
  71. RoleConversions* role_conversions);
  72. void ParseAxNodeChildIds(ui::AXNodeData& node_data,
  73. const base::Value& child_ids) {
  74. for (const auto& item : child_ids.GetList())
  75. node_data.child_ids.push_back(GetAsInt(item));
  76. }
  77. void ParseAxNodeDescription(ui::AXNodeData& node_data,
  78. const base::Value& description) {
  79. std::string type, value;
  80. GetTypeAndValue(description, type, value);
  81. DCHECK_EQ(type, "computedString");
  82. node_data.SetDescription(value);
  83. }
  84. void ParseAxNodeName(ui::AXNodeData& node_data, const base::Value& name) {
  85. std::string type, value;
  86. GetTypeAndValue(name, type, value);
  87. DCHECK_EQ(type, "computedString");
  88. node_data.SetName(value);
  89. }
  90. void ParseAxNodeProperties(ui::AXNodeData& node_data,
  91. const base::Value& properties) {
  92. if (properties.is_list()) {
  93. for (const auto& item : properties.GetList())
  94. ParseAxNodeProperties(node_data, item);
  95. return;
  96. }
  97. const std::string prop_type = properties.GetDict().Find("name")->GetString();
  98. const base::Value* prop_value =
  99. properties.GetDict().Find("value")->GetDict().Find("value");
  100. if (prop_type == "atomic") {
  101. node_data.AddBoolAttribute(
  102. ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot,
  103. !GetAsBoolean(*prop_value));
  104. } else if (prop_type == "focusable") {
  105. if (GetAsBoolean(*prop_value))
  106. node_data.AddState(ax::mojom::State::kFocusable);
  107. } else if (prop_type == "expanded") {
  108. if (GetAsBoolean(*prop_value))
  109. node_data.AddState(ax::mojom::State::kExpanded);
  110. } else if (prop_type == "hasPopup") {
  111. node_data.SetHasPopup(
  112. ui::ParseAXEnum<ax::mojom::HasPopup>(prop_value->GetString().c_str()));
  113. } else if (prop_type == "invalid") {
  114. node_data.SetInvalidState(GetAsBoolean(*prop_value)
  115. ? ax::mojom::InvalidState::kTrue
  116. : ax::mojom::InvalidState::kFalse);
  117. } else if (prop_type == "level") {
  118. node_data.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
  119. GetAsInt(*prop_value));
  120. } else {
  121. DCHECK(base::Contains(kUnusedAxNodeProperties, prop_type)) << prop_type;
  122. }
  123. }
  124. ax::mojom::Role RoleFromString(std::string role,
  125. RoleConversions* role_conversions) {
  126. const auto& item = role_conversions->find(role);
  127. DCHECK(item != role_conversions->end()) << role;
  128. return item->second;
  129. }
  130. void ParseAxNodeRole(ui::AXNodeData& node_data,
  131. const base::Value& role,
  132. RoleConversions* role_conversions) {
  133. const std::string role_type = role.GetDict().Find("type")->GetString();
  134. std::string role_value = role.GetDict().Find("value")->GetString();
  135. DCHECK(role_type == "role" || role_type == "internalRole");
  136. node_data.role = RoleFromString(role_value, role_conversions);
  137. }
  138. void ParseAxNode(ui::AXNodeData& node_data,
  139. const base::Value& ax_node,
  140. RoleConversions* role_conversions) {
  141. // Store the name and set it at the end because |AXNodeData::SetName|
  142. // expects a valid role to have already been set prior to calling it.
  143. base::Value name_value;
  144. for (const auto item : ax_node.GetDict()) {
  145. if (item.first == "backendDOMNodeId") {
  146. node_data.AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId,
  147. GetAsInt(item.second));
  148. } else if (item.first == "childIds") {
  149. ParseAxNodeChildIds(node_data, item.second);
  150. } else if (item.first == "description") {
  151. ParseAxNodeDescription(node_data, item.second);
  152. } else if (item.first == "ignored") {
  153. DCHECK(item.second.is_bool());
  154. if (item.second.GetBool())
  155. node_data.AddState(ax::mojom::State::kIgnored);
  156. } else if (item.first == "name") {
  157. name_value = item.second.Clone();
  158. } else if (item.first == "nodeId") {
  159. node_data.id = GetAsInt(item.second);
  160. } else if (item.first == "properties") {
  161. ParseAxNodeProperties(node_data, item.second);
  162. } else if (item.first == "role") {
  163. ParseAxNodeRole(node_data, item.second, role_conversions);
  164. } else {
  165. DCHECK(base::Contains(kUnusedAxNodeItems, item.first)) << item.first;
  166. }
  167. }
  168. if (!name_value.is_none())
  169. ParseAxNodeName(node_data, name_value);
  170. }
  171. void ParseChildren(ui::AXTreeUpdate& tree_update,
  172. const base::Value& children,
  173. RoleConversions* role_conversions) {
  174. for (const auto& child : children.GetList())
  175. AddNode(tree_update, child, role_conversions);
  176. }
  177. // Converts "rgb(R,G,B)" or "rgba(R,G,B,A)" to one ARGB integer where R,G, and B
  178. // are integers and A is float < 1.
  179. uint32_t ConvertRgbaStringToArgbInt(const std::string& argb_string) {
  180. std::vector<std::string> values = base::SplitString(
  181. argb_string, ",()", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  182. uint32_t a, r, g, b;
  183. if (values.size() == 4 && values[0] == "rgb") {
  184. a = 0;
  185. } else if (values.size() == 5 && values[0] == "rgba") {
  186. a = base::ClampRound(atof(values[4].c_str()) * 255);
  187. } else {
  188. NOTREACHED() << "Unexpected color value: " << argb_string;
  189. return -1;
  190. }
  191. r = atoi(values[1].c_str());
  192. g = atoi(values[2].c_str());
  193. b = atoi(values[3].c_str());
  194. return (a << 24) + (r << 16) + (g << 8) + b;
  195. }
  196. void ParseStyle(ui::AXNodeData& node_data, const base::Value& style) {
  197. const std::string& name = style.GetDict().Find("name")->GetString();
  198. const std::string& value = style.GetDict().Find("value")->GetString();
  199. if (name == "color") {
  200. node_data.AddIntAttribute(ax::mojom::IntAttribute::kColor,
  201. ConvertRgbaStringToArgbInt(value));
  202. } else if (name == "direction") {
  203. node_data.AddIntAttribute(
  204. ax::mojom::IntAttribute::kTextDirection,
  205. static_cast<int>(
  206. ui::ParseAXEnum<ax::mojom::WritingDirection>(value.c_str())));
  207. } else if (name == "display") {
  208. node_data.AddStringAttribute(ax::mojom::StringAttribute::kDisplay, value);
  209. } else if (name == "font-size") {
  210. // Drop the 'px' at the end of font size.
  211. DCHECK(style.GetDict().Find("value")->is_string());
  212. node_data.AddFloatAttribute(
  213. ax::mojom::FloatAttribute::kFontSize,
  214. atof(value.substr(0, value.length() - 2).c_str()));
  215. } else if (name == "font-weight") {
  216. DCHECK(style.GetDict().Find("value")->is_string());
  217. node_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize,
  218. atof(value.c_str()));
  219. } else if (name == "list-style-type") {
  220. node_data.AddIntAttribute(
  221. ax::mojom::IntAttribute::kListStyle,
  222. static_cast<int>(ui::ParseAXEnum<ax::mojom::ListStyle>(value.c_str())));
  223. } else if (name == "visibility") {
  224. if (value == "hidden")
  225. node_data.AddState(ax::mojom::State::kInvisible);
  226. else
  227. DCHECK_EQ(value, "visible");
  228. } else {
  229. DCHECK(base::Contains(kUnusedStyles, name)) << name;
  230. }
  231. }
  232. void ParseExtras(ui::AXNodeData& node_data, const base::Value& extras) {
  233. for (const auto extra : extras.GetDict()) {
  234. const base::Value::List& items = extra.second.GetList();
  235. if (extra.first == "bounds") {
  236. node_data.relative_bounds.bounds.set_x(GetAsDouble(items[0]));
  237. node_data.relative_bounds.bounds.set_y(GetAsDouble(items[1]));
  238. node_data.relative_bounds.bounds.set_width(GetAsDouble(items[2]));
  239. node_data.relative_bounds.bounds.set_height(GetAsDouble(items[3]));
  240. } else if (extra.first == "styles") {
  241. for (const auto& style : items)
  242. ParseStyle(node_data, style);
  243. } else {
  244. NOTREACHED() << "Unexpected: " << extra.first;
  245. }
  246. }
  247. }
  248. // Adds a node and returns its id.
  249. ui::AXNodeID AddNode(ui::AXTreeUpdate& tree_update,
  250. const base::Value& node,
  251. RoleConversions* role_conversions) {
  252. ui::AXNodeData node_data;
  253. // Store the string and set it at the end because |AXNodeData::SetName|
  254. // expects a valid role to have already been set prior to calling it.
  255. std::string name_string;
  256. for (const auto item : node.GetDict()) {
  257. if (item.first == "axNode") {
  258. ParseAxNode(node_data, item.second, role_conversions);
  259. } else if (item.first == "backendDomId") {
  260. node_data.AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId,
  261. GetAsInt(item.second));
  262. } else if (item.first == "children") {
  263. ParseChildren(tree_update, item.second, role_conversions);
  264. } else if (item.first == "description") {
  265. node_data.SetDescription(item.second.GetString());
  266. } else if (item.first == "extras") {
  267. ParseExtras(node_data, item.second);
  268. } else if (item.first == "interesting") {
  269. // Not used yet, boolean.
  270. } else if (item.first == "name") {
  271. name_string = item.second.GetString();
  272. } else if (item.first == "role") {
  273. node_data.role =
  274. RoleFromString(item.second.GetString(), role_conversions);
  275. } else {
  276. NOTREACHED() << "Unexpected: " << item.first;
  277. }
  278. }
  279. node_data.SetName(name_string);
  280. tree_update.nodes.push_back(node_data);
  281. return node_data.id;
  282. }
  283. } // namespace
  284. namespace ui {
  285. AXTreeUpdate AXTreeUpdateFromJSON(const base::Value& json,
  286. RoleConversions* role_conversions) {
  287. AXTreeUpdate tree_update;
  288. // Input should be a list with one item, which is the root node.
  289. DCHECK(json.is_list() && json.GetList().size() == 1);
  290. tree_update.root_id =
  291. AddNode(tree_update, json.GetList().front(), role_conversions);
  292. return tree_update;
  293. }
  294. } // namespace ui