chromium/ui/accessibility/platform/fuchsia/browser_accessibility_manager_fuchsia_unittest.cc

// Copyright 2021 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/platform/fuchsia/browser_accessibility_manager_fuchsia.h"

#include <map>
#include <vector>

#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_node_id_forward.h"
#include "ui/accessibility/platform/ax_platform_tree_manager.h"
#include "ui/accessibility/platform/ax_platform_tree_manager_delegate.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/accessibility/platform/fuchsia/browser_accessibility_fuchsia.h"
#include "ui/accessibility/platform/fuchsia/accessibility_bridge_fuchsia.h"
#include "ui/accessibility/platform/fuchsia/accessibility_bridge_fuchsia_registry.h"
#include "ui/accessibility/platform/test_ax_node_id_delegate.h"
#include "ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h"

namespace ui {
namespace {

class MockBrowserAccessibilityDelegate
    : public TestAXPlatformTreeManagerDelegate {
 public:
  void AccessibilityPerformAction(const AXActionData& data) override {
    last_action_data_ = data;
  }

  void AccessibilityHitTest(
      const gfx::Point& point_in_frame_pixels,
      const ax::mojom::Event& opt_event_to_fire,
      int opt_request_id,
      base::OnceCallback<void(AXPlatformTreeManager* hit_manager,
                              AXNodeID hit_node_id)> opt_callback) override {
    last_hit_test_point_ = point_in_frame_pixels;
    last_request_id_ = opt_request_id;
  }

  const std::optional<AXActionData>& last_action_data() {
    return last_action_data_;
  }

  const std::optional<int>& last_request_id() { return last_request_id_; }

  const std::optional<gfx::Point>& last_hit_test_point() {
    return last_hit_test_point_;
  }

 private:
  std::optional<AXActionData> last_action_data_;
  std::optional<int> last_request_id_;
  std::optional<gfx::Point> last_hit_test_point_;
};

class MockAccessibilityBridge : public AccessibilityBridgeFuchsia {
 public:
  MockAccessibilityBridge() = default;
  ~MockAccessibilityBridge() override = default;

  // AccessibilityBridgeFuchsia overrides.
  void UpdateNode(fuchsia_accessibility_semantics::Node node) override {
    node_updates_.push_back(std::move(node));
  }

  void DeleteNode(uint32_t node_id) override {
    node_deletions_.push_back(node_id);
  }

  void OnAccessibilityHitTestResult(int hit_test_request_id,
                                    std::optional<uint32_t> result) override {
    hit_test_results_[hit_test_request_id] = result;
  }

  void SetRootID(uint32_t root_node_id) override {
    root_node_id_ = root_node_id;
  }

  inspect::Node GetInspectNode() override { return inspect::Node(); }

  float GetDeviceScaleFactor() override { return device_scale_factor_; }

  void SetDeviceScaleFactor(float device_scale_factor) {
    device_scale_factor_ = device_scale_factor;
  }

  const std::vector<fuchsia_accessibility_semantics::Node>& node_updates() {
    return node_updates_;
  }
  const std::vector<uint32_t>& node_deletions() { return node_deletions_; }
  const std::map<int, std::optional<uint32_t>>& hit_test_results() {
    return hit_test_results_;
  }

  const std::optional<uint32_t>& old_focus() { return old_focus_; }

  const std::optional<uint32_t>& new_focus() { return new_focus_; }

  const std::optional<uint32_t>& root_node_id() { return root_node_id_; }

  void reset() {
    node_updates_.clear();
    node_deletions_.clear();
    hit_test_results_.clear();
  }

 private:
  float device_scale_factor_ = 1.f;
  std::vector<fuchsia_accessibility_semantics::Node> node_updates_;
  std::vector<uint32_t> node_deletions_;
  std::map<int /* hit test request id */,
           std::optional<uint32_t> /* hit test result */>
      hit_test_results_;
  std::optional<uint32_t> old_focus_;
  std::optional<uint32_t> new_focus_;
  std::optional<uint32_t> root_node_id_;
};

class BrowserAccessibilityManagerFuchsiaTest : public testing::Test {
 public:
  BrowserAccessibilityManagerFuchsiaTest() = default;
  ~BrowserAccessibilityManagerFuchsiaTest() override = default;

  // testing::Test.
  void SetUp() override {
    mock_browser_accessibility_delegate_ =
        std::make_unique<MockBrowserAccessibilityDelegate>();
    mock_accessibility_bridge_ = std::make_unique<MockAccessibilityBridge>();
    manager_ = std::unique_ptr<BrowserAccessibilityManager>(
        BrowserAccessibilityManager::Create(
            node_id_delegate_, mock_browser_accessibility_delegate_.get()));
    static_cast<BrowserAccessibilityManagerFuchsia*>(manager_.get())
        ->SetAccessibilityBridgeForTest(mock_accessibility_bridge_.get());
  }

 protected:
  std::unique_ptr<MockBrowserAccessibilityDelegate>
      mock_browser_accessibility_delegate_;
  std::unique_ptr<MockAccessibilityBridge> mock_accessibility_bridge_;
  const base::test::SingleThreadTaskEnvironment task_environment_;
  TestAXNodeIdDelegate node_id_delegate_;
  std::unique_ptr<BrowserAccessibilityManager> manager_;
};

TEST_F(BrowserAccessibilityManagerFuchsiaTest, TestEmitNodeUpdates) {
  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.root_id = 1;
  initial_state.nodes.resize(1);
  initial_state.nodes[0].id = 1;

  manager_->ax_tree()->Unserialize(initial_state);

  {
    const auto& node_updates = mock_accessibility_bridge_->node_updates();
    ASSERT_EQ(node_updates.size(), 1u);

    BrowserAccessibilityFuchsia* node_1 =
        ToBrowserAccessibilityFuchsia(manager_->GetFromID(1));
    ASSERT_TRUE(node_1);

    EXPECT_EQ(node_updates[0].node_id(), node_1->GetFuchsiaNodeID());

    // Verify that the the accessibility bridge root ID was set to node 1's
    // unique ID.
    ASSERT_TRUE(mock_accessibility_bridge_->root_node_id().has_value());
    EXPECT_EQ(*mock_accessibility_bridge_->root_node_id(),
              static_cast<uint32_t>(node_1->GetFuchsiaNodeID()));

    const auto& node_deletions = mock_accessibility_bridge_->node_deletions();
    // The initial empty document root is the only node that was deleted.
    EXPECT_EQ(node_deletions.size(), 1u);
  }

  // Send another update for node 1, and verify that it was passed to the
  // accessibility bridge.
  AXTreeUpdate updated_state;
  updated_state.root_id = 1;
  updated_state.nodes.resize(2);
  updated_state.nodes[0].id = 1;
  updated_state.nodes[0].child_ids.push_back(2);
  updated_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(updated_state);

  {
    const auto& node_updates = mock_accessibility_bridge_->node_updates();
    ASSERT_EQ(node_updates.size(), 3u);

    BrowserAccessibilityFuchsia* node_1 =
        ToBrowserAccessibilityFuchsia(manager_->GetFromID(1));
    ASSERT_TRUE(node_1);
    BrowserAccessibilityFuchsia* node_2 =
        ToBrowserAccessibilityFuchsia(manager_->GetFromID(2));
    ASSERT_TRUE(node_2);

    // Node 1 is the root of the root tree, so its fuchsia ID should be 0.
    EXPECT_EQ(node_updates[1].node_id(), node_1->GetFuchsiaNodeID());
    ASSERT_EQ(node_updates[1].child_ids()->size(), 1u);
    EXPECT_EQ(node_updates[1].child_ids().value()[0],
              node_2->GetFuchsiaNodeID());

    // Node 2 is NOT the root, so its fuchsia ID should be its AXUniqueID.
    EXPECT_EQ(node_updates[2].node_id().value(), node_2->GetFuchsiaNodeID());
  }
}

TEST_F(BrowserAccessibilityManagerFuchsiaTest, TestDeleteNodes) {
  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.push_back(2);
  initial_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(initial_state);

  // Verify that no deletions were received.
  {
    const auto& node_deletions = mock_accessibility_bridge_->node_deletions();
    // Only the initial empty document root has been deleted.
    EXPECT_EQ(node_deletions.size(), 1u);
  }

  // Get the fuchsia IDs for nodes 1 and 2 before they are deleted.
  BrowserAccessibilityFuchsia* node_1 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(1));
  ASSERT_TRUE(node_1);
  uint32_t node_1_fuchsia_id = node_1->GetFuchsiaNodeID();
  BrowserAccessibilityFuchsia* node_2 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(2));
  ASSERT_TRUE(node_2);
  uint32_t node_2_fuchsia_id = node_2->GetFuchsiaNodeID();

  // Delete node 2.
  AXTreeUpdate updated_state;
  updated_state.nodes.resize(1);
  updated_state.nodes[0].id = 1;

  manager_->ax_tree()->Unserialize(updated_state);

  // Verify that the accessibility bridge received a deletion for node 2.
  {
    const auto& node_deletions = mock_accessibility_bridge_->node_deletions();
    // The initial empty document root has also been deleted, ignore that.
    ASSERT_EQ(node_deletions.size(), 2u);
    EXPECT_EQ(node_deletions[1], static_cast<uint32_t>(node_2_fuchsia_id));
  }

  // Destroy manager. Doing so should force the remainder of the tree to be
  // deleted.
  manager_.reset();

  // Verify that the accessibility bridge received a deletion for node 1.
  {
    const auto& node_deletions = mock_accessibility_bridge_->node_deletions();
    // The initial empty document root has also been deleted, ignore that as
    // well as the previous node that had been deleted.
    ASSERT_EQ(node_deletions.size(), 3u);
    EXPECT_EQ(node_deletions[2], node_1_fuchsia_id);
  }
}

TEST_F(BrowserAccessibilityManagerFuchsiaTest, TestLocationChange) {
  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.push_back(2);
  initial_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(initial_state);

  {
    const std::vector<fuchsia_accessibility_semantics::Node>& node_updates =
        mock_accessibility_bridge_->node_updates();
    ASSERT_EQ(node_updates.size(), 2u);
  }

  // Send location update for node 2.
  std::vector<AXLocationChanges> changes;
  AXRelativeBounds relative_bounds;
  relative_bounds.bounds =
      gfx::RectF(/*x=*/1, /*y=*/2, /*width=*/3, /*height=*/4);
  AXLocationChanges change;
  change.id = 2;
  change.ax_tree_id = tree_id;
  change.new_location = relative_bounds;
  changes.push_back(change);
  manager_->OnLocationChanges(std::move(changes));

  {
    BrowserAccessibilityFuchsia* node_2 =
        ToBrowserAccessibilityFuchsia(manager_->GetFromID(2));
    ASSERT_TRUE(node_2);

    const std::vector<fuchsia_accessibility_semantics::Node>& node_updates =
        mock_accessibility_bridge_->node_updates();
    ASSERT_EQ(node_updates.size(), 3u);
    const fuchsia_accessibility_semantics::Node& node_update =
        node_updates.back();
    EXPECT_EQ(node_update.node_id(),
              static_cast<uint32_t>(node_2->GetFuchsiaNodeID()));
    ASSERT_TRUE(node_update.location());
    const fuchsia_ui_gfx::BoundingBox& location =
        node_update.location().value();
    EXPECT_EQ(location.min().x(), 1);
    EXPECT_EQ(location.min().y(), 2);
    EXPECT_EQ(location.max().x(), 4);
    EXPECT_EQ(location.max().y(), 6);
  }
}

TEST_F(BrowserAccessibilityManagerFuchsiaTest, TestFocusChange) {
  // We need to specify that this is the root frame; otherwise, no focus events
  // will be fired. Likewise, we need to ensure that events are not suppressed.
  mock_browser_accessibility_delegate_->is_root_frame_ = true;
  BrowserAccessibilityManager::NeverSuppressOrDelayEventsForTesting();

  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.tree_data.parent_tree_id = AXTreeIDUnknown();
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.push_back(2);
  initial_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(initial_state);

  BrowserAccessibilityFuchsia* node_1 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(1));
  ASSERT_TRUE(node_1);

  BrowserAccessibilityFuchsia* node_2 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(2));
  ASSERT_TRUE(node_2);

  // Set focus to node 1, and check that the focus was updated from null to
  // node 1.
  {
    AXUpdatesAndEvents event;
    AXTreeUpdate updated_state;
    updated_state.tree_data.tree_id = tree_id;
    updated_state.has_tree_data = true;
    updated_state.tree_data.focused_tree_id = tree_id;
    updated_state.tree_data.focus_id = 1;
    event.ax_tree_id = tree_id;
    event.updates.push_back(std::move(updated_state));
    EXPECT_TRUE(manager_->OnAccessibilityEvents(event));
  }

  {
    const std::vector<fuchsia_accessibility_semantics::Node>& node_updates =
        mock_accessibility_bridge_->node_updates();
    ASSERT_FALSE(node_updates.empty());
    EXPECT_EQ(node_updates.back().node_id().value(),
              node_1->GetFuchsiaNodeID());
    ASSERT_TRUE(node_updates.back().states());
    ASSERT_TRUE(node_updates.back().states()->has_input_focus().has_value());
    EXPECT_TRUE(node_updates.back().states()->has_input_focus().value());
  }

  // Set focus to node 2, and check that focus was updated from node 1 to node
  // 2.
  {
    AXUpdatesAndEvents event;
    AXTreeUpdate updated_state;
    updated_state.tree_data.tree_id = tree_id;
    updated_state.has_tree_data = true;
    updated_state.tree_data.focused_tree_id = tree_id;
    updated_state.tree_data.focus_id = 2;
    event.ax_tree_id = tree_id;
    event.updates.push_back(std::move(updated_state));
    EXPECT_TRUE(manager_->OnAccessibilityEvents(event));
  }

  {
    const std::vector<fuchsia_accessibility_semantics::Node>& node_updates =
        mock_accessibility_bridge_->node_updates();
    ASSERT_GT(node_updates.size(), 2u);
    const fuchsia_accessibility_semantics::Node& old_focus_node =
        node_updates[node_updates.size() - 2];
    EXPECT_EQ(old_focus_node.node_id().value(), node_1->GetFuchsiaNodeID());
    ASSERT_TRUE(old_focus_node.states());
    ASSERT_TRUE(old_focus_node.states()->has_input_focus().has_value());
    EXPECT_FALSE(old_focus_node.states()->has_input_focus().value());
    EXPECT_EQ(node_updates.back().node_id().value(),
              node_2->GetFuchsiaNodeID());
    ASSERT_TRUE(node_updates.back().states());
    ASSERT_TRUE(node_updates.back().states()->has_input_focus().has_value());
    EXPECT_TRUE(node_updates.back().states()->has_input_focus().value());
  }
}

TEST_F(BrowserAccessibilityManagerFuchsiaTest, HitTest) {
  mock_browser_accessibility_delegate_->is_root_frame_ = true;

  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.push_back(2);
  initial_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(initial_state);

  BrowserAccessibilityFuchsia* node_1 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(1));
  ASSERT_TRUE(node_1);
  BrowserAccessibilityFuchsia* node_2 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(2));
  ASSERT_TRUE(node_2);

  // Set the hit test action data. Note that we will later hard-code the result
  // of the hit test, so the geometry doesn't matter. We just need to verify
  // that the target point specified here matches the target point received by
  // the delegate.
  AXActionData action_data;
  action_data.action = ax::mojom::Action::kHitTest;
  action_data.target_point.set_x(1);
  action_data.target_point.set_y(2);
  action_data.request_id = 3;

  AXPlatformNodeFuchsia* platform_node = static_cast<AXPlatformNodeFuchsia*>(
      AXPlatformNodeBase::GetFromUniqueId(node_1->GetFuchsiaNodeID()));
  ASSERT_TRUE(platform_node);

  platform_node->PerformAction(action_data);

  {
    std::optional<gfx::Point> last_target =
        mock_browser_accessibility_delegate_->last_hit_test_point();
    ASSERT_TRUE(last_target.has_value());
    EXPECT_EQ(last_target->x(), 1);
    EXPECT_EQ(last_target->y(), 2);

    std::optional<int> last_request_id =
        mock_browser_accessibility_delegate_->last_request_id();
    ASSERT_TRUE(last_request_id.has_value());
    EXPECT_EQ(*last_request_id, action_data.request_id);
  }

  // Fire blink event to signify the hit test result.
  manager_->FireBlinkEvent(ax::mojom::Event::kHover, node_2,
                           action_data.request_id);

  {
    const std::map<int, std::optional<uint32_t>>& hit_test_results =
        mock_accessibility_bridge_->hit_test_results();
    // We should have a hit test result for request id = 3, and the result
    // should be the fuchsia ID of node 2, which is our hit result specified
    // above.
    ASSERT_TRUE(hit_test_results.count(3));
    ASSERT_TRUE(hit_test_results.at(3).has_value());
    EXPECT_EQ(*hit_test_results.at(3), node_2->GetFuchsiaNodeID());
  }
}

TEST_F(BrowserAccessibilityManagerFuchsiaTest, HitTestFails) {
  mock_browser_accessibility_delegate_->is_root_frame_ = true;

  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.push_back(2);
  initial_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(initial_state);

  BrowserAccessibilityFuchsia* node_1 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(1));
  ASSERT_TRUE(node_1);

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kHitTest;
  action_data.target_point.set_x(1);
  action_data.target_point.set_y(2);
  action_data.request_id = 4;

  AXPlatformNodeFuchsia* platform_node = static_cast<AXPlatformNodeFuchsia*>(
      AXPlatformNodeBase::GetFromUniqueId(node_1->GetFuchsiaNodeID()));
  ASSERT_TRUE(platform_node);

  platform_node->PerformAction(action_data);

  {
    std::optional<gfx::Point> last_target =
        mock_browser_accessibility_delegate_->last_hit_test_point();
    EXPECT_EQ(last_target->x(), 1);
    EXPECT_EQ(last_target->y(), 2);
  }

  // FIre blink event to signify the hit test result.
  manager_->FireBlinkEvent(ax::mojom::Event::kHover, nullptr, 4);

  {
    const std::map<int, std::optional<uint32_t>>& hit_test_results =
        mock_accessibility_bridge_->hit_test_results();

    ASSERT_FALSE(hit_test_results.empty());
    ASSERT_TRUE(hit_test_results.count(4));
    EXPECT_FALSE(hit_test_results.at(4).has_value());
  }
}

TEST_F(BrowserAccessibilityManagerFuchsiaTest, PerformAction) {
  mock_browser_accessibility_delegate_->is_root_frame_ = true;

  AXTreeUpdate initial_state;
  AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
  initial_state.tree_data.tree_id = tree_id;
  initial_state.has_tree_data = true;
  initial_state.tree_data.loaded = true;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.push_back(2);
  initial_state.nodes[1].id = 2;

  manager_->ax_tree()->Unserialize(initial_state);

  BrowserAccessibilityFuchsia* node_2 =
      ToBrowserAccessibilityFuchsia(manager_->GetFromID(2));
  ASSERT_TRUE(node_2);

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  action_data.target_node_id = 2;

  AXPlatformNodeFuchsia* platform_node = static_cast<AXPlatformNodeFuchsia*>(
      AXPlatformNodeBase::GetFromUniqueId(node_2->GetFuchsiaNodeID()));
  ASSERT_TRUE(platform_node);

  platform_node->PerformAction(action_data);

  {
    const std::optional<AXActionData> last_action_data =
        mock_browser_accessibility_delegate_->last_action_data();
    ASSERT_TRUE(last_action_data);
    EXPECT_EQ(last_action_data->action,
              ax::mojom::Action::kScrollToMakeVisible);
  }
}

}  // namespace
}  // namespace ui