chromium/ui/accessibility/platform/fuchsia/semantic_provider_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 "semantic_provider_impl.h"

#include <fidl/fuchsia.accessibility.semantics/cpp/fidl.h>
#include <fidl/fuchsia.ui.views/cpp/hlcpp_conversion.h>
#include <lib/async/default.h>

#include <algorithm>
#include <memory>

#include "base/fuchsia/fidl_event_handler.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ui {
namespace {

using fuchsia_accessibility_semantics::Node;

class AXFuchsiaSemanticProviderDelegate
    : public AXFuchsiaSemanticProvider::Delegate {
 public:
  AXFuchsiaSemanticProviderDelegate() = default;
  ~AXFuchsiaSemanticProviderDelegate() override = default;

  bool OnSemanticsManagerConnectionClosed(zx_status_t status) override {
    on_semantics_manager_connection_closed_called_ = true;
    return true;
  }

  bool OnAccessibilityAction(
      uint32_t node_id,
      fuchsia_accessibility_semantics::Action action) override {
    on_accessibility_action_called_ = true;
    on_accessibility_action_node_id_ = node_id;
    on_accessibility_action_action_ = std::move(action);
    return true;
  }

  void OnHitTest(fuchsia_math::PointF point,
                 HitTestCallback callback) override {
    on_hit_test_called_ = true;
    on_hit_test_point_ = std::move(point);
    std::move(callback).Run({});
  }

  void OnSemanticsEnabled(bool enabled) override {
    on_semantics_enabled_called_ = true;
  }

  bool on_semantics_manager_connection_closed_called_;
  bool on_accessibility_action_called_;
  uint32_t on_accessibility_action_node_id_ = 10000000;
  fuchsia_accessibility_semantics::Action on_accessibility_action_action_;
  bool on_hit_test_called_;
  fuchsia_math::PointF on_hit_test_point_;
  bool on_semantics_enabled_called_;
};

// Returns a semantic tree of the form:
// (0 (1 2 (3 4 (5))))
std::vector<Node> TreeNodes() {
  Node node_0;
  node_0.node_id(0u);
  node_0.child_ids({{1u, 2u}});

  Node node_1;
  node_1.node_id(1u);

  Node node_2;
  node_2.node_id(2u);
  node_2.child_ids({{3u, 4u}});

  Node node_3;
  node_3.node_id(3u);

  Node node_4;
  node_4.node_id(4u);
  node_4.child_ids({{5u}});

  Node node_5;
  node_5.node_id(5u);

  std::vector<Node> update;
  update.push_back(std::move(node_0));
  update.push_back(std::move(node_1));
  update.push_back(std::move(node_2));
  update.push_back(std::move(node_3));
  update.push_back(std::move(node_4));
  update.push_back(std::move(node_5));
  return update;
}

class AXFuchsiaSemanticProviderTest
    : public ::testing::Test,
      public fidl::Server<fuchsia_accessibility_semantics::SemanticsManager>,
      public fidl::Server<fuchsia_accessibility_semantics::SemanticTree> {
 public:
  AXFuchsiaSemanticProviderTest()
      : semantics_manager_bindings_(test_context_.additional_services(), this),
        semantic_listener_error_handler_(
            base::BindRepeating([](fidl::UnbindInfo info) { ADD_FAILURE(); })) {
  }
  ~AXFuchsiaSemanticProviderTest() override = default;
  AXFuchsiaSemanticProviderTest(const AXFuchsiaSemanticProviderTest&) = delete;
  AXFuchsiaSemanticProviderTest& operator=(
      const AXFuchsiaSemanticProviderTest&) = delete;
  void SetUp() override {
    fuchsia::ui::views::ViewRefControl view_ref_control;
    fuchsia::ui::views::ViewRef view_ref;
    auto status = zx::eventpair::create(
        /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
    CHECK_EQ(ZX_OK, status);
    view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);

    delegate_ = std::make_unique<AXFuchsiaSemanticProviderDelegate>();

    semantic_provider_ = std::make_unique<AXFuchsiaSemanticProviderImpl>(
        fidl::HLCPPToNatural(std::move(view_ref)), delegate_.get());

    // Spin the loop to allow registration with the SemanticsManager to be
    // processed.
    base::RunLoop().RunUntilIdle();
  }

 protected:
  // fuchsia_accessibility_semantics::SemanticsManager implementation.
  void RegisterViewForSemantics(
      AXFuchsiaSemanticProviderTest::RegisterViewForSemanticsRequest& request,
      AXFuchsiaSemanticProviderTest::RegisterViewForSemanticsCompleter::Sync&
          completer) final {
    semantic_listener_.Bind(std::move(request.listener()),
                            async_get_default_dispatcher(),
                            &semantic_listener_error_handler_);
    semantic_tree_binding_.emplace(async_get_default_dispatcher(),
                                   std::move(request.semantic_tree_request()),
                                   this, fidl::kIgnoreBindingClosure);
    semantic_listener_->OnSemanticsModeChanged({{.updates_enabled = true}})
        .Then(
            [](fidl::Result<fuchsia_accessibility_semantics::SemanticListener::
                                OnSemanticsModeChanged>& result) {
              ASSERT_TRUE(result.is_ok());
            });
  }

  // fuchsia_accessibility_semantics::SemanticTree implementation.
  void UpdateSemanticNodes(
      AXFuchsiaSemanticProviderTest::UpdateSemanticNodesRequest& request,
      AXFuchsiaSemanticProviderTest::UpdateSemanticNodesCompleter::Sync&
          ignored_completer) final {
    num_update_semantic_nodes_called_++;
    node_updates_.push_back(std::move(request.nodes()));
  }
  void DeleteSemanticNodes(
      AXFuchsiaSemanticProviderTest::DeleteSemanticNodesRequest& request,
      AXFuchsiaSemanticProviderTest::DeleteSemanticNodesCompleter::Sync&
          ignored_completer) final {
    num_delete_semantic_nodes_called_++;
  }
  void CommitUpdates(
      AXFuchsiaSemanticProviderTest::CommitUpdatesCompleter::Sync& completer)
      final {
    completer.Reply();
  }
  void SendSemanticEvent(
      AXFuchsiaSemanticProviderTest::SendSemanticEventRequest& request,
      AXFuchsiaSemanticProviderTest::SendSemanticEventCompleter::Sync&
          completer) override {
    completer.Reply();
  }

  const std::vector<std::vector<fuchsia_accessibility_semantics::Node>>&
  node_updates() {
    return node_updates_;
  }

  // Required because of |test_context_|.
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::MainThreadType::IO};
  base::TestComponentContextForProcess test_context_;
  // Binding to fake Semantics Manager Fuchsia service, implemented by this test
  // class.
  base::ScopedNaturalServiceBinding<
      fuchsia_accessibility_semantics::SemanticsManager>
      semantics_manager_bindings_;

  uint32_t num_update_semantic_nodes_called_ = 0;
  uint32_t num_delete_semantic_nodes_called_ = 0;

  base::RepeatingClosure on_commit_;

  fidl::Client<fuchsia_accessibility_semantics::SemanticListener>
      semantic_listener_;
  base::FidlErrorEventHandler<fuchsia_accessibility_semantics::SemanticListener>
      semantic_listener_error_handler_;
  std::optional<
      fidl::ServerBinding<fuchsia_accessibility_semantics::SemanticTree>>
      semantic_tree_binding_;
  std::unique_ptr<AXFuchsiaSemanticProviderDelegate> delegate_;
  std::unique_ptr<AXFuchsiaSemanticProviderImpl> semantic_provider_;

  // Node updates batched per API call to UpdateSemanticNodes().
  std::vector<std::vector<fuchsia_accessibility_semantics::Node>> node_updates_;
};

TEST_F(AXFuchsiaSemanticProviderTest, HandlesOnSemanticsConnectionClosed) {
  ASSERT_TRUE(semantic_tree_binding_.has_value());
  semantic_tree_binding_->Close(ZX_ERR_PEER_CLOSED);

  // Spin the loop to allow the channel-close to be handled.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(delegate_->on_semantics_manager_connection_closed_called_);
}

TEST_F(AXFuchsiaSemanticProviderTest, HandlesOnAccessibilityAction) {
  bool action_handled = false;
  semantic_listener_
      ->OnAccessibilityActionRequested({{
          .node_id = 1u,
          .action = fuchsia_accessibility_semantics::Action::kDefault,
      }})
      .Then([&action_handled](
                fidl::Result<fuchsia_accessibility_semantics::SemanticListener::
                                 OnAccessibilityActionRequested>& result) {
        ASSERT_TRUE(result.is_ok());
        action_handled = result->handled();
      });

  // Spin the loop to handle the request, and receive the response.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(action_handled);
  EXPECT_TRUE(delegate_->on_accessibility_action_called_);
  EXPECT_EQ(delegate_->on_accessibility_action_node_id_, 1u);
  EXPECT_EQ(delegate_->on_accessibility_action_action_,
            fuchsia_accessibility_semantics::Action::kDefault);
}

TEST_F(AXFuchsiaSemanticProviderTest, HandlesOnHitTest) {
  semantic_provider_->SetPixelScale(2.f);

  // Note that the point is sent here and will be converted according to the
  // device scale used. Only then it gets sent to the handler, which receives
  // the value already with the proper scaling.
  semantic_listener_
      ->HitTest({{
          .local_point = {{.x = 4, .y = 6}},
      }})
      .Then([](fidl::Result<
                fuchsia_accessibility_semantics::SemanticListener::HitTest>&
                   result) { ASSERT_TRUE(result.is_ok()); });

  // Spin the loop to allow the call to be processed.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(delegate_->on_hit_test_called_);
  EXPECT_EQ(delegate_->on_hit_test_point_.x(), 8.0);
  EXPECT_EQ(delegate_->on_hit_test_point_.y(), 12.0);
}

// Verify that the AXFuchsiaSemanticProviderImpl constructor triggered the call
// chain RegisterViewForSemantics -> OnSemanticsModeChanged ->
// OnSemanticsEnabled.
TEST_F(AXFuchsiaSemanticProviderTest, HandlesOnSemanticsEnabled) {
  EXPECT_TRUE(delegate_->on_semantics_enabled_called_);
}

TEST_F(AXFuchsiaSemanticProviderTest, SendsRootOnly) {
  Node root;
  root.node_id(0u);
  EXPECT_TRUE(semantic_provider_->Update(std::move(root)));

  // Spin the loop to process the update call.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, SendsNodesFromRootToLeaves) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, SendsNodesFromLeavesToRoot) {
  auto nodes = TreeNodes();
  std::reverse(nodes.begin(), nodes.end());
  for (auto& node : nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest,
       SendsNodesOnlyAfterParentNoLongerPointsToDeletedChild) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());

  // Deletes node 5, which is a child of 4.
  EXPECT_TRUE(semantic_provider_->Delete(5u));

  // Spin the loop to process the deletion call.
  base::RunLoop().RunUntilIdle();

  // Commit is pending, because the parent still points to the child.
  EXPECT_TRUE(semantic_provider_->HasPendingUpdates());

  Node node_4;
  node_4.node_id(4u);
  node_4.child_ids({});
  EXPECT_TRUE(semantic_provider_->Update(std::move(node_4)));

  // Spin the loop to process the node update.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 2u);
  EXPECT_EQ(num_delete_semantic_nodes_called_, 1u);

  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest,
       SendsNodesOnlyAfterDanglingChildIsDeleted) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());

  Node node_4;
  node_4.node_id(4u);
  node_4.child_ids({});  // This removes child 5.
  EXPECT_TRUE(semantic_provider_->Update(std::move(node_4)));

  // Spin the loop to process the update call.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(semantic_provider_->HasPendingUpdates());

  EXPECT_TRUE(semantic_provider_->Delete(5u));

  // Spin the loop to process the deletion.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 2u);
  EXPECT_EQ(num_delete_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, ReparentsNodeWithADeletion) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());

  // Deletes node 4 to reparent its child (5).
  EXPECT_TRUE(semantic_provider_->Delete(4u));

  // Spin the loop to process the deletion.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(semantic_provider_->HasPendingUpdates());

  // Add child 5 to another node.
  Node node_1;
  node_1.node_id(1u);
  node_1.child_ids({{5u}});
  EXPECT_TRUE(semantic_provider_->Update(std::move(node_1)));

  // Spin the loop to process the update.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(semantic_provider_->HasPendingUpdates());

  Node node_4;
  node_4.node_id(4u);
  node_4.child_ids({});
  EXPECT_TRUE(semantic_provider_->Update(std::move(node_4)));

  // Spin the loop to process the update.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 2u);
  EXPECT_EQ(num_delete_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, ReparentsNodeWithAnUpdate) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());

  // Add child 5 to another node. Note that 5 will have two parents, and the
  // commit must be held until it has only one.
  Node node_1;
  node_1.node_id(1u);
  node_1.child_ids({{5u}});
  EXPECT_TRUE(semantic_provider_->Update(std::move(node_1)));

  // Spin the loop to process the update.
  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(semantic_provider_->HasPendingUpdates());

  // Updates node 4 to no longer point to 5.
  Node node_4;
  node_4.node_id(4u);
  node_4.child_ids({});
  EXPECT_TRUE(semantic_provider_->Update(std::move(node_4)));

  // Spin the loop to process the update.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 2u);
  EXPECT_EQ(num_delete_semantic_nodes_called_, 0u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, ChangesRoot) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued updated calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());

  Node new_root;
  new_root.node_id(0u);
  new_root.child_ids({{1u, 2u}});
  EXPECT_TRUE(semantic_provider_->Update(std::move(new_root)));

  // Spin the loop to process the update.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 2u);
  EXPECT_EQ(num_delete_semantic_nodes_called_, 0u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, BatchesUpdates) {
  std::vector<Node> updates;
  for (uint32_t i = 0; i < 30; ++i) {
    Node node;
    node.node_id(i);
    node.child_ids({{i + 1}});
    updates.push_back(std::move(node));
  }
  updates.back().child_ids()->clear();

  for (auto& node : updates) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  // 30 nodes in batches of 16 (default value of maximum nodes per update call),
  // should result in two update calls to the semantics API.
  EXPECT_EQ(num_update_semantic_nodes_called_, 2u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, ClearsTree) {
  auto tree_nodes = TreeNodes();
  for (auto& node : tree_nodes) {
    EXPECT_TRUE(semantic_provider_->Update(std::move(node)));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());

  semantic_provider_->Clear();

  // Spin the loop to process the clear-tree call.
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(num_update_semantic_nodes_called_, 1u);
  EXPECT_EQ(num_delete_semantic_nodes_called_, 1u);
  EXPECT_FALSE(semantic_provider_->HasPendingUpdates());
}

TEST_F(AXFuchsiaSemanticProviderTest, UpdateScaleFactor) {
  // Send an initial root node update. At this point, the pixel scale is 1, the
  // root node's transform will be the identity matrix. Thus, the resulting
  // update sent to fuchsia should not contain a transform.
  {
    Node node;
    node.node_id(0u);
    // Set child_ids to make sure they're not overwritten later.
    node.child_ids({{1u}});
    semantic_provider_->Update(std::move(node));
    Node child;
    child.node_id(1u);
    semantic_provider_->Update(std::move(child));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  // Check that the first update sent to fuchsia reflects a pixel scale of 1.
  {
    ASSERT_EQ(node_updates().size(), 1u);
    const auto& first_update_batch = node_updates()[0];
    ASSERT_EQ(first_update_batch.size(), 2u);
    EXPECT_EQ(first_update_batch[0].node_id(), 0u);
    const fuchsia_accessibility_semantics::Node& node = first_update_batch[0];
    ASSERT_TRUE(node.node_to_container_transform().has_value());
    const auto& transform = node.node_to_container_transform()->matrix();
    EXPECT_EQ(transform[0], 1.f);
    EXPECT_EQ(transform[5], 1.f);
    ASSERT_EQ(node.child_ids()->size(), 1u);
    EXPECT_EQ(node.child_ids().value()[0], 1u);
  }

  // Now, set a new pixel scale != 1. This step should force an update to
  // fuchsia.
  const auto kPixelScale = 0.5f;
  semantic_provider_->SetPixelScale(kPixelScale);

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  // Check that the root node's node_to_conatiner_transform field was set when
  // the pixel scale was updated.
  {
    ASSERT_EQ(node_updates().size(), 2u);
    const auto& second_update_batch = node_updates()[1];
    ASSERT_EQ(second_update_batch.size(), 1u);
    const fuchsia_accessibility_semantics::Node& node = second_update_batch[0];
    EXPECT_EQ(node.node_id(), 0u);
    ASSERT_TRUE(node.node_to_container_transform().has_value());
    const auto& transform = node.node_to_container_transform()->matrix();
    EXPECT_EQ(transform[0], 1.f / kPixelScale);
    EXPECT_EQ(transform[5], 1.f / kPixelScale);
    ASSERT_EQ(node.child_ids()->size(), 1u);
    EXPECT_EQ(node.child_ids().value()[0], 1u);
  }

  // Finally, send one more update, and verify that the semantic provider
  // accounted for the new pixel scale in the root node's transform.
  {
    Node node;
    node.node_id(0u);
    node.child_ids({{1u}});
    semantic_provider_->Update(std::move(node));
  }

  // Spin the loop to process the queued update calls.
  base::RunLoop().RunUntilIdle();

  // Check that the root node's node_to_conatiner_transform field was set using
  // the new pixel scale.
  {
    ASSERT_EQ(node_updates().size(), 3u);
    const auto& third_update_batch = node_updates()[2];
    ASSERT_EQ(third_update_batch.size(), 1u);
    const fuchsia_accessibility_semantics::Node& node = third_update_batch[0];
    EXPECT_EQ(node.node_id(), 0u);
    ASSERT_TRUE(node.node_to_container_transform().has_value());
    const auto& transform = node.node_to_container_transform()->matrix();
    EXPECT_EQ(transform[0], 1.f / kPixelScale);
    EXPECT_EQ(transform[5], 1.f / kPixelScale);
    ASSERT_EQ(node.child_ids()->size(), 1u);
    EXPECT_EQ(node.child_ids().value()[0], 1u);
  }
}

}  // namespace
}  // namespace ui