[fuchsia] Add support for a11y sliders

* Add sliders to role conversions
* Add increment and decrement actions
* Update data structures in FakeSemanticTree
* Add waits for node updates in FakeSemanticTree
* Add test only action/event pair in browsertests so waiting for actions
  is more reliable

(cherry picked from commit 53a16cf1df91a19c839336d92338124ce0471d24)

Test: AccessibilityBridgeTest.Slider
Bug: fuchsia:56295, 1122806, 1136974
Change-Id: Id0658ada5b15da1c128b69c1346aee1d39f2a6f7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2314758
Commit-Queue: Sharon Yang <[email protected]>
Reviewed-by: Sergey Ulanov <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#812919}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2466320
Reviewed-by: Sharon Yang <[email protected]>
Commit-Queue: Tess Eisenberger <[email protected]>
Cr-Commit-Position: refs/branch-heads/4280@{#245}
Cr-Branched-From: ea420fb963f9658c9969b6513c56b8f47efa1a2a-refs/heads/master@{#812852}
diff --git a/fuchsia/engine/browser/accessibility_bridge.cc b/fuchsia/engine/browser/accessibility_bridge.cc
index a7d4106..b45b4966 100644
--- a/fuchsia/engine/browser/accessibility_bridge.cc
+++ b/fuchsia/engine/browser/accessibility_bridge.cc
@@ -148,7 +148,8 @@
       // Run the pending callback with the hit.
       pending_hit_test_callbacks_[event.action_request_id](std::move(hit));
       pending_hit_test_callbacks_.erase(event.action_request_id);
-    } else if (event_received_callback_for_test_) {
+    } else if (event_received_callback_for_test_ &&
+               event.event_type == ax::mojom::Event::kEndOfTest) {
       std::move(event_received_callback_for_test_).Run();
     }
   }
@@ -185,6 +186,13 @@
 
   web_contents_->GetMainFrame()->AccessibilityPerformAction(action_data);
   callback(true);
+
+  if (event_received_callback_for_test_) {
+    // Perform an action with a corresponding event to signal the action has
+    // been pumped through.
+    action_data.action = ax::mojom::Action::kSignalEndOfTest;
+    web_contents_->GetMainFrame()->AccessibilityPerformAction(action_data);
+  }
 }
 
 void AccessibilityBridge::HitTest(fuchsia::math::PointF local_point,
diff --git a/fuchsia/engine/browser/accessibility_bridge_browsertest.cc b/fuchsia/engine/browser/accessibility_bridge_browsertest.cc
index 5375922c..87d73b8 100644
--- a/fuchsia/engine/browser/accessibility_bridge_browsertest.cc
+++ b/fuchsia/engine/browser/accessibility_bridge_browsertest.cc
@@ -33,6 +33,8 @@
 const char kOffscreenNodeName[] = "offscreen node";
 const size_t kPage1NodeCount = 9;
 const size_t kPage2NodeCount = 190;
+const size_t kInitialRangeValue = 51;
+const size_t kStepSize = 3;
 
 fuchsia::math::PointF GetCenterOfBox(fuchsia::ui::gfx::BoundingBox box) {
   fuchsia::math::PointF center;
@@ -307,3 +309,37 @@
 
   EXPECT_FALSE(is_offscreen);
 }
+
+IN_PROC_BROWSER_TEST_F(AccessibilityBridgeTest, Slider) {
+  GURL page_url(embedded_test_server()->GetURL(kPage1Path));
+  ASSERT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
+      navigation_controller_.get(), fuchsia::web::LoadUrlParams(),
+      page_url.spec()));
+  navigation_listener_.RunUntilUrlAndTitleEquals(page_url, kPage1Title);
+  semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
+
+  fuchsia::accessibility::semantics::Node* node =
+      semantics_manager_.semantic_tree()->GetNodeFromRole(
+          fuchsia::accessibility::semantics::Role::SLIDER);
+  EXPECT_TRUE(node);
+  EXPECT_TRUE(node->has_states() && node->states().has_range_value());
+  EXPECT_EQ(node->states().range_value(), kInitialRangeValue);
+
+  AccessibilityBridge* bridge = frame_impl_->accessibility_bridge_for_test();
+  base::RunLoop run_loop;
+  bridge->set_event_received_callback_for_test(run_loop.QuitClosure());
+  semantics_manager_.RequestAccessibilityAction(
+      node->node_id(), fuchsia::accessibility::semantics::Action::INCREMENT);
+  semantics_manager_.RunUntilNumActionsHandledEquals(1);
+  run_loop.Run();
+
+  // Wait for the slider node to be updated, then check the value.
+  base::RunLoop run_loop2;
+  semantics_manager_.semantic_tree()->SetNodeUpdatedCallback(
+      node->node_id(), run_loop2.QuitClosure());
+  run_loop2.Run();
+
+  node = semantics_manager_.semantic_tree()->GetNodeWithId(node->node_id());
+  EXPECT_TRUE(node->has_states() && node->states().has_range_value());
+  EXPECT_EQ(node->states().range_value(), kInitialRangeValue + kStepSize);
+}
diff --git a/fuchsia/engine/browser/ax_tree_converter.cc b/fuchsia/engine/browser/ax_tree_converter.cc
index b07b328..7d1547cb1 100644
--- a/fuchsia/engine/browser/ax_tree_converter.cc
+++ b/fuchsia/engine/browser/ax_tree_converter.cc
@@ -26,6 +26,8 @@
     return fuchsia::accessibility::semantics::Role::HEADER;
   if (role == ax::mojom::Role::kImage)
     return fuchsia::accessibility::semantics::Role::IMAGE;
+  if (role == ax::mojom::Role::kSlider)
+    return fuchsia::accessibility::semantics::Role::SLIDER;
   if (role == ax::mojom::Role::kTextField)
     return fuchsia::accessibility::semantics::Role::TEXT_FIELD;
 
@@ -47,6 +49,23 @@
     attributes.set_secondary_label(description.substr(0, MAX_LABEL_SIZE));
   }
 
+  if (node.IsRangeValueSupported()) {
+    fuchsia::accessibility::semantics::RangeAttributes range_attributes;
+    if (node.HasFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange)) {
+      range_attributes.set_min_value(
+          node.GetFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange));
+    }
+    if (node.HasFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange)) {
+      range_attributes.set_max_value(
+          node.GetFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange));
+    }
+    if (node.HasFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange)) {
+      range_attributes.set_step_delta(node.GetFloatAttribute(
+          ax::mojom::FloatAttribute::kStepValueForRange));
+    }
+    attributes.set_range(std::move(range_attributes));
+  }
+
   return attributes;
 }
 
@@ -96,6 +115,12 @@
     states.set_value(value.substr(0, MAX_LABEL_SIZE));
   }
 
+  // The value a range element currently has.
+  if (node.HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange)) {
+    states.set_range_value(
+        node.GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange));
+  }
+
   return states;
 }
 
@@ -178,6 +203,12 @@
     case fuchsia::accessibility::semantics::Action::DEFAULT:
       *mojom_action = ax::mojom::Action::kDoDefault;
       return true;
+    case fuchsia::accessibility::semantics::Action::DECREMENT:
+      *mojom_action = ax::mojom::Action::kDecrement;
+      return true;
+    case fuchsia::accessibility::semantics::Action::INCREMENT:
+      *mojom_action = ax::mojom::Action::kIncrement;
+      return true;
     case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
       *mojom_action = ax::mojom::Action::kScrollToMakeVisible;
       return true;
diff --git a/fuchsia/engine/browser/fake_semantic_tree.cc b/fuchsia/engine/browser/fake_semantic_tree.cc
index b0cc8f7..5bb8d1c5 100644
--- a/fuchsia/engine/browser/fake_semantic_tree.cc
+++ b/fuchsia/engine/browser/fake_semantic_tree.cc
@@ -35,6 +35,7 @@
     fuchsia::accessibility::semantics::Node* child = GetNodeWithId(c);
     if (!child)
       return false;
+
     is_valid &= IsTreeValid(child, tree_size);
   }
   return is_valid;
@@ -60,29 +61,33 @@
   run_loop.Run();
 }
 
+void FakeSemanticTree::SetNodeUpdatedCallback(
+    uint32_t node_id,
+    base::OnceClosure node_updated_callback) {
+  node_wait_id_ = node_id;
+  on_node_updated_callback_ = std::move(node_updated_callback);
+}
+
 fuchsia::accessibility::semantics::Node* FakeSemanticTree::GetNodeWithId(
     uint32_t id) {
-  for (auto& node : nodes_) {
-    if (node.has_node_id() && node.node_id() == id) {
-      return &node;
-    }
-  }
-  return nullptr;
+  auto it = nodes_.find(id);
+  return it == nodes_.end() ? nullptr : &it->second;
 }
 
 fuchsia::accessibility::semantics::Node* FakeSemanticTree::GetNodeFromLabel(
     base::StringPiece label) {
   fuchsia::accessibility::semantics::Node* to_return = nullptr;
-  for (auto& node : nodes_) {
-    if (node.has_attributes() && node.attributes().has_label() &&
-        node.attributes().label() == label) {
+  for (auto& n : nodes_) {
+    auto* node = &n.second;
+    if (node->has_attributes() && node->attributes().has_label() &&
+        node->attributes().label() == label) {
       // There are sometimes multiple semantic nodes with the same label. Hit
       // testing should return the node with the smallest node ID so behaviour
       // is consistent with the hit testing API being called.
       if (!to_return) {
-        to_return = &node;
-      } else if (node.node_id() < to_return->node_id()) {
-        to_return = &node;
+        to_return = node;
+      } else if (node->node_id() < to_return->node_id()) {
+        to_return = node;
       }
     }
   }
@@ -90,24 +95,34 @@
   return to_return;
 }
 
+fuchsia::accessibility::semantics::Node* FakeSemanticTree::GetNodeFromRole(
+    fuchsia::accessibility::semantics::Role role) {
+  for (auto& n : nodes_) {
+    auto* node = &n.second;
+    if (node->has_role() && node->role() == role)
+      return node;
+  }
+
+  return nullptr;
+}
+
 void FakeSemanticTree::UpdateSemanticNodes(
     std::vector<fuchsia::accessibility::semantics::Node> nodes) {
-  nodes_.reserve(nodes.size() + nodes_.size());
+  bool wait_node_updated = false;
   for (auto& node : nodes) {
-    // Delete an existing node that's being updated to avoid having duplicate
-    // nodes.
-    DeleteSemanticNodes({node.node_id()});
-    nodes_.push_back(std::move(node));
+    if (node.node_id() == node_wait_id_ && on_node_updated_callback_)
+      wait_node_updated = true;
+
+    nodes_[node.node_id()] = std::move(node);
   }
+
+  if (wait_node_updated)
+    std::move(on_node_updated_callback_).Run();
 }
 
 void FakeSemanticTree::DeleteSemanticNodes(std::vector<uint32_t> node_ids) {
-  for (auto id : node_ids) {
-    for (uint i = 0; i < nodes_.size(); i++) {
-      if (nodes_.at(i).node_id() == id)
-        nodes_.erase(nodes_.begin() + i);
-    }
-  }
+  for (auto id : node_ids)
+    nodes_.erase(id);
 }
 
 void FakeSemanticTree::CommitUpdates(CommitUpdatesCallback callback) {
diff --git a/fuchsia/engine/browser/fake_semantic_tree.h b/fuchsia/engine/browser/fake_semantic_tree.h
index 89432f0..981f6619 100644
--- a/fuchsia/engine/browser/fake_semantic_tree.h
+++ b/fuchsia/engine/browser/fake_semantic_tree.h
@@ -8,6 +8,7 @@
 #include <fuchsia/accessibility/semantics/cpp/fidl.h>
 #include <fuchsia/accessibility/semantics/cpp/fidl_test_base.h>
 #include <lib/fidl/cpp/binding.h>
+#include <unordered_map>
 
 #include "base/callback.h"
 
@@ -35,9 +36,16 @@
   void Disconnect();
 
   void RunUntilNodeCountAtLeast(size_t count);
+  void SetNodeUpdatedCallback(uint32_t node_id,
+                              base::OnceClosure node_updated_callback);
   fuchsia::accessibility::semantics::Node* GetNodeWithId(uint32_t id);
+
+  // For both functions below, it is possible there are multiple nodes with the
+  // same identifier.
   fuchsia::accessibility::semantics::Node* GetNodeFromLabel(
       base::StringPiece label);
+  fuchsia::accessibility::semantics::Node* GetNodeFromRole(
+      fuchsia::accessibility::semantics::Role role);
 
   // fuchsia::accessibility::semantics::SemanticTree implementation.
   void UpdateSemanticNodes(
@@ -50,8 +58,11 @@
  private:
   fidl::Binding<fuchsia::accessibility::semantics::SemanticTree>
       semantic_tree_binding_;
-  std::vector<fuchsia::accessibility::semantics::Node> nodes_;
+  std::unordered_map<uint32_t, fuchsia::accessibility::semantics::Node> nodes_;
   base::RepeatingClosure on_commit_updates_;
+
+  uint32_t node_wait_id_;
+  base::OnceClosure on_node_updated_callback_;
 };
 
 #endif  // FUCHSIA_ENGINE_BROWSER_FAKE_SEMANTIC_TREE_H_
diff --git a/fuchsia/engine/test/data/ax1.html b/fuchsia/engine/test/data/ax1.html
index 0f2f8aa..99de9c41 100644
--- a/fuchsia/engine/test/data/ax1.html
+++ b/fuchsia/engine/test/data/ax1.html
@@ -7,6 +7,7 @@
     <p>a third paragraph</p>
     <button>another button</button>
     <button>button 3</button>
+    <input type="range" min="0" max="100" value="51" step="3" class="slider" id="myRange">
     <div style='height:1000px; width:1000px;'></div>
     <p>offscreen node</p>
     <button>button 4</button>