| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/test_ax_tree_update.h" |
| |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| |
| namespace ui { |
| |
| int PlusSignCount(const std::string& s) { |
| int count = 0; |
| |
| for (auto& character : s) { |
| if (character == '+') { |
| count++; |
| } |
| } |
| |
| return count; |
| } |
| |
| bool IsSpaceOrTab(const char c) { |
| return c == ' ' || c == '\t'; |
| } |
| |
| void RemoveLeadingAndTrailingWhitespace(std::string& s) { |
| if (s.empty()) { |
| return; |
| } |
| auto front_it = s.begin(); |
| auto back_it = s.end() - 1; |
| |
| while (IsSpaceOrTab(*front_it) || IsSpaceOrTab(*back_it)) { |
| if (front_it > back_it) { |
| return; |
| } |
| if (front_it == back_it) { |
| if (IsSpaceOrTab(*front_it)) { |
| s.erase(front_it); |
| } |
| return; |
| } |
| if (IsSpaceOrTab(*front_it)) { |
| s.erase(front_it); |
| |
| // Iterators get invalidated when erasing. |
| front_it = s.begin(); |
| back_it = s.end() - 1; |
| } |
| if (IsSpaceOrTab(*back_it)) { |
| s.erase(back_it); |
| |
| // Iterators get invalidated when erasing. |
| back_it = s.end() - 1; |
| front_it = s.begin(); |
| } |
| } |
| } |
| |
| bool StringToBool(const std::string& s) { |
| if (s == "true" || s == "True" || s == "1") { |
| return true; |
| } |
| if (s == "false" || s == "False" || s == "0") { |
| return false; |
| } |
| |
| // TODO: Specify which node this error was found at. |
| NOTREACHED_IN_MIGRATION() << "Invalid value passed to StringToBool: " << s; |
| return false; |
| } |
| |
| void ParseAndAddNodeProperties( |
| AXNodeData& node_data, |
| const std::vector<std::string>& node_line) { |
| // At this point, the vector of strings we receive should be just a vector of |
| // properties of the format: |
| // [<property>=<value>] where value depends on the property. |
| |
| DCHECK(node_line.size() >= 1) |
| << "Error in formatting of the tree structure. Possibly extra whiespace."; |
| for (auto& prop : node_line) { |
| std::vector<std::string> property_vector = base::SplitStringUsingSubstr( |
| prop, "=", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| DCHECK(property_vector.size() == 2) |
| << "Properties should always be formed as " |
| "<SupportedProperty>=<PropertyValue/s>"; |
| std::string property = property_vector[0]; |
| |
| if (property == "name" || property == "Name") { |
| // Since the structure is passed in as an unformatted string, |
| // and the name has the format "<name>", |
| // the string adds escape characters before the quotes. |
| // Therefore when we set the name we must remove the `\"` character. |
| |
| std::string name = property_vector[1]; |
| DCHECK(name.front() == '\"' && name.back() == '\"'); |
| name.erase(name.begin()); |
| name.erase(--name.end()); |
| |
| node_data.SetName(name); |
| } else if (property == "states" || property == "state") { |
| std::vector<std::string> state_values = base::SplitStringUsingSubstr( |
| property_vector[1], ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| DCHECK(state_values.size() >= 1) |
| << "State values should be at least one, and they should be " |
| "separated by commas."; |
| for (auto& value : state_values) { |
| node_data.AddState(StringToState(value)); |
| } |
| |
| } else if (property == "intAttribute" || property == "IntAttribute" || |
| property == "intattribute") { |
| std::vector<std::string> int_attribute_vector = |
| base::SplitStringUsingSubstr(property_vector[1], ",", |
| base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| DCHECK(int_attribute_vector.size() == 2) |
| << "Int attribute in string must always be formed: " |
| "intAttribute=<intAttributeType>,<intAttributeValue>"; |
| int int_value = 0; |
| base::StringToInt(int_attribute_vector[1], &int_value); |
| DCHECK(int_value) << "Formatting error or non integer passed as node ID."; |
| node_data.AddIntAttribute(StringToIntAttribute(int_attribute_vector[0]), |
| int_value); |
| |
| } else if (property == "stringAttribute" || property == "StringAttribute" || |
| property == "stringattribute") { |
| std::vector<std::string> string_attribute_vector = |
| base::SplitStringUsingSubstr(property_vector[1], ",", |
| base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| DCHECK(string_attribute_vector.size() == 2) |
| << "String attribute in string must always be formed: " |
| "stringAttribute=<stringAttributeType>,<stringAttributeValue>"; |
| node_data.AddStringAttribute( |
| StringToStringAttribute(string_attribute_vector[0]), |
| string_attribute_vector[1]); |
| } else if (property == "boolAttribute" || property == "BoolAttribute" || |
| property == "boolattribute") { |
| std::vector<std::string> bool_attribute_vector = |
| base::SplitStringUsingSubstr(property_vector[1], ",", |
| base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| DCHECK(bool_attribute_vector.size() == 2) |
| << "Bool attribute in string must always be formed: " |
| "boolAttribute=<boolAttributeType>,<boolAttributeValue>"; |
| node_data.AddBoolAttribute( |
| StringToBoolAttribute(bool_attribute_vector[0]), |
| StringToBool(bool_attribute_vector[1])); |
| |
| } else { |
| // TODO: Will extend to more properties here. |
| NOTREACHED_IN_MIGRATION() |
| << "Either an invalid property was specified, or this function does " |
| "not currently support the specified property."; |
| } |
| } |
| } |
| |
| AXNodeData ParseNodeInfo(std::vector<std::string>& node_line, |
| std::set<int>& found_ids) { |
| AXNodeData data; |
| |
| // Must at the very least have id and role. |
| DCHECK(node_line.size() >= 2) << "Error, a node must have an id and role."; |
| int int_value = 0; |
| base::StringToInt(node_line[0], &int_value); |
| DCHECK(int_value) << "Formatting error or non integer passed as node ID."; |
| data.id = int_value; |
| |
| DCHECK(found_ids.find(data.id) == found_ids.end()) |
| << "Error, can't have duplicate IDs."; |
| found_ids.insert(data.id); |
| ax::mojom::Role role = StringToRole(node_line[1]); |
| |
| data.role = role; |
| |
| data.child_ids = {}; |
| |
| if (node_line.size() >= 3) { |
| node_line.erase(node_line.begin()); |
| node_line.erase(node_line.begin()); |
| ParseAndAddNodeProperties(data, node_line); |
| } |
| |
| return data; |
| } |
| |
| TestAXTreeUpdateNode::TestAXTreeUpdateNode(const TestAXTreeUpdateNode&) = |
| default; |
| |
| TestAXTreeUpdateNode::TestAXTreeUpdateNode(TestAXTreeUpdateNode&&) = default; |
| |
| TestAXTreeUpdateNode::~TestAXTreeUpdateNode() = default; |
| |
| TestAXTreeUpdateNode::TestAXTreeUpdateNode( |
| ax::mojom::Role role, |
| const std::vector<TestAXTreeUpdateNode>& children) |
| : children(children) { |
| DCHECK_NE(role, ax::mojom::Role::kUnknown); |
| data.role = role; |
| } |
| |
| TestAXTreeUpdateNode::TestAXTreeUpdateNode( |
| ax::mojom::Role role, |
| ax::mojom::State state, |
| const std::vector<TestAXTreeUpdateNode>& children) |
| : children(children) { |
| DCHECK_NE(role, ax::mojom::Role::kUnknown); |
| DCHECK_NE(state, ax::mojom::State::kNone); |
| data.role = role; |
| data.AddState(state); |
| } |
| |
| TestAXTreeUpdateNode::TestAXTreeUpdateNode(const std::string& text) { |
| data.role = ax::mojom::Role::kStaticText; |
| data.SetName(text); |
| } |
| |
| TestAXTreeUpdate::TestAXTreeUpdate(const TestAXTreeUpdateNode& root) { |
| root_id = SetSubtree(root); |
| } |
| |
| AXNodeID TestAXTreeUpdate::SetSubtree(const TestAXTreeUpdateNode& node) { |
| size_t node_index = nodes.size(); |
| nodes.push_back(node.data); |
| nodes[node_index].id = node_index + 1; |
| std::vector<AXNodeID> child_ids; |
| for (const auto& child : node.children) { |
| child_ids.push_back(SetSubtree(child)); |
| } |
| nodes[node_index].child_ids = child_ids; |
| return nodes[node_index].id; |
| } |
| |
| TestAXTreeUpdate::TestAXTreeUpdate(const std::string& tree_structure) { |
| std::vector<AXNodeData> node_data_vector; |
| std::vector<std::string> tree_structure_vector = base::SplitStringUsingSubstr( |
| tree_structure, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| |
| // Test authors might accidentally include whitespace when declaring the non |
| // formatted string. |
| RemoveLeadingAndTrailingWhitespace(*tree_structure_vector.begin()); |
| RemoveLeadingAndTrailingWhitespace(*--tree_structure_vector.end()); |
| |
| // With how we pass in the non formatted string, the first and last elements |
| // of the vector should be empty. |
| DCHECK(tree_structure_vector.front().empty() && |
| tree_structure_vector.back().empty()) |
| << "Error in parsing the tree structure, double check the formatting."; |
| tree_structure_vector.erase(tree_structure_vector.begin()); |
| tree_structure_vector.erase(--tree_structure_vector.end()); |
| |
| node_data_vector.reserve(tree_structure_vector.size()); |
| |
| RemoveLeadingAndTrailingWhitespace(tree_structure_vector[0]); |
| |
| int root_pluses = PlusSignCount(tree_structure_vector[0]); |
| DCHECK_EQ(root_pluses, 2) |
| << "The first line of the test needs to start with 2 '+' sign, not " |
| << root_pluses; |
| |
| // We remove the plus signs from the string |
| tree_structure_vector[0].erase(0, root_pluses); |
| std::vector<std::string> root_line = |
| base::SplitStringUsingSubstr(tree_structure_vector[0], " ", |
| base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| |
| // Set to keep track of found IDs and make sure we don't have duplicates. |
| std::set<int> found_ids; |
| |
| node_data_vector.push_back(ParseNodeInfo(root_line, found_ids)); |
| |
| // This maps the number of plus signs to the last index that number of plus |
| // signs was found at in the `tree_structure_vector` This will be useful for |
| // determining descendants. |
| std::unordered_map<int, int> last_index_appearance_of_plus_count; |
| |
| // The first (zeroth) row always has two plus signs |
| last_index_appearance_of_plus_count[2] = 0; |
| int last_count = 2; |
| int greatest_count = 2; |
| for (size_t i = 1; i < tree_structure_vector.size(); i++) { |
| // Test authors might have added whitespace when passing in the tree |
| // structure |
| RemoveLeadingAndTrailingWhitespace(tree_structure_vector[i]); |
| |
| int plus_count = PlusSignCount(tree_structure_vector[i]); |
| DCHECK(plus_count % 2 == 0) |
| << "Error in plus sign count, can't have an odd number of plus signs"; |
| DCHECK(plus_count <= greatest_count + 2) |
| << "Error in plus signs count at tree structure line number " << i |
| << ", it can't have more than two more plus signs than the previous " |
| "element."; |
| |
| if (plus_count > greatest_count) { |
| greatest_count = plus_count; |
| } |
| |
| auto elem = last_index_appearance_of_plus_count.find(plus_count); |
| if (elem == last_index_appearance_of_plus_count.end() || |
| (int)i > elem->second) { |
| last_index_appearance_of_plus_count[plus_count] = i; |
| } |
| |
| // We remove the plus signs from the string. |
| tree_structure_vector[i].erase(0, plus_count); |
| std::vector<std::string> node_line = base::SplitStringUsingSubstr( |
| tree_structure_vector[i], " ", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| |
| AXNodeData curr_node = ParseNodeInfo(node_line, found_ids); |
| node_data_vector.push_back(curr_node); |
| |
| // Two cases. Either This line's plus count is greater than the last line's, |
| // which would make `curr_node` the direct child of the last node. Or the |
| // current plus count is <= last one, which would mean the parent is higher |
| // up. |
| if (plus_count > last_count) { |
| node_data_vector[i - 1].child_ids.push_back(curr_node.id); |
| } else { |
| elem = last_index_appearance_of_plus_count.find(plus_count - 2); |
| CHECK(elem != last_index_appearance_of_plus_count.end()) |
| << "Error in plus sign count."; |
| int parent_index = elem->second; |
| node_data_vector[parent_index].child_ids.push_back(curr_node.id); |
| } |
| |
| last_count = plus_count; |
| } |
| |
| DCHECK(node_data_vector.size() >= 1); |
| |
| tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| tree_data.focused_tree_id = tree_data.tree_id; |
| tree_data.parent_tree_id = ui::AXTreeIDUnknown(); |
| tree_data = tree_data; |
| has_tree_data = true; |
| root_id = node_data_vector[0].id; |
| |
| for (auto& node : node_data_vector) { |
| nodes.push_back(node); |
| } |
| } |
| |
| } // namespace ui |