chromium/chrome/browser/ash/crosapi/clipboard_history_ash_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/crosapi/clipboard_history_ash.h"

#include <memory>
#include <string>
#include <vector>

#include "ash/clipboard/test_support/mock_clipboard_history_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/unguessable_token.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom-shared.h"
#include "chromeos/ui/clipboard_history/clipboard_history_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/events/event_constants.h"

namespace crosapi {

namespace {

using ::base::test::InvokeFuture;
using ::testing::ElementsAre;

// Matchers --------------------------------------------------------------------

MATCHER_P2(DescriptorMatches, display_format, display_text, "") {
  return arg->display_format == display_format &&
         arg->display_text == display_text;
}

// Helper classes --------------------------------------------------------------

// A mocked client to check the descriptors received from Ash.
class MockedClipboardHistoryClient
    : public crosapi::mojom::ClipboardHistoryClient {
 public:
  // crosapi::mojom::ClipboardHistoryClient:
  MOCK_METHOD(void,
              SetClipboardHistoryItemDescriptors,
              (std::vector<crosapi::mojom::ClipboardHistoryItemDescriptorPtr>),
              (override));

  mojo::Receiver<crosapi::mojom::ClipboardHistoryClient> receiver_{this};
};

}  // namespace

class ClipboardHistoryAshTest : public testing::Test {
 public:
  ClipboardHistoryAsh clipboard_history_ash_;
  ash::MockClipboardHistoryController clipboard_history_controller_;
};

// Verifies that `ClipboardHistoryAsh` uses `ash::ClipboardHistoryController` to
// paste clipboard items by ID.
TEST_F(ClipboardHistoryAshTest, PasteClipboardItemById) {
  struct {
    base::UnguessableToken item_id;
    int event_flags;
    crosapi::mojom::ClipboardHistoryControllerShowSource paste_source;
  } test_params[] = {{base::UnguessableToken::Create(), ui::EF_NONE,
                      crosapi::mojom::ClipboardHistoryControllerShowSource::
                          kRenderViewContextMenu},
                     {base::UnguessableToken::Create(), ui::EF_MOUSE_BUTTON,
                      crosapi::mojom::ClipboardHistoryControllerShowSource::
                          kTextfieldContextMenu}};

  for (const auto& [id, event_flags, paste_source] : test_params) {
    EXPECT_CALL(
        clipboard_history_controller_,
        PasteClipboardItemById(id.ToString(), event_flags, paste_source));
    clipboard_history_ash_.PasteClipboardItemById(id, event_flags,
                                                  paste_source);
  }
}

class ClipboardHistoryAshWithClientTest : public ash::AshTestBase {
 public:
  // ash::AshTestBase:
  void SetUp() override {
    // Enable the clipboard history refresh feature.
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{chromeos::features::kClipboardHistoryRefresh,
                              chromeos::features::kJelly},
        /*disabled_features=*/{});
    ash::AshTestBase::SetUp();

    // `clipboard_history_ash_` should be created after Ash.
    clipboard_history_ash_ = std::make_unique<ClipboardHistoryAsh>();
  }

  // Waits until `mock_client_` receives descriptors. Returns the received
  // descriptors.
  std::vector<mojom::ClipboardHistoryItemDescriptorPtr>
  WaitForReceivedDescriptors() {
    base::test::TestFuture<
        std::vector<mojom::ClipboardHistoryItemDescriptorPtr>>
        future;
    EXPECT_CALL(mock_client_, SetClipboardHistoryItemDescriptors)
        .WillOnce(InvokeFuture(future));

    return future.Take();
  }

  void RegisterClient() {
    clipboard_history_ash_->RegisterClient(
        mock_client_.receiver_.BindNewPipeAndPassRemote());
  }

  base::test::ScopedFeatureList scoped_feature_list_;
  MockedClipboardHistoryClient mock_client_;
  std::unique_ptr<ClipboardHistoryAsh> clipboard_history_ash_;
};

// Verifies that the clipboard history client receives clipboard history item
// descriptors from Ash as expected.
TEST_F(ClipboardHistoryAshWithClientTest, Basics) {
  RegisterClient();
  std::vector<std::vector<mojom::ClipboardHistoryItemDescriptor>> test_cases = {
      // Test case 1: The injected descriptors have different formats.
      {
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kText, u"dummy text",
           /*file_count=*/0u},
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kPng, u"dummy png",
           /*file_count=*/0u},
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kHtml, u"dummy html",
           /*file_count=*/0u},
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kFile, u"dummy file",
           /*file_count=*/1u},
      },
      // Test case 2: The injected descriptors are text descriptors with special
      // contents.
      {
          // Element 1: A text descriptor with 100 null characters.
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kText,
           std::u16string(100, '\0'),
           /*file_count=*/0u},
          // Element 2: A text descriptor with 100 control characters.
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kText, std::u16string(100, 1),
           /*file_count=*/0u},
      },
      // Test case 3: The injected descriptors contains the descriptors of
      // unknown types.
      {
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kText, u"dummy text",
           /*file_count=*/0u},
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kUnknown, u"dummy text2",
           /*file_count=*/0u},
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kUnknown, u"dummy text3",
           /*file_count=*/0u},
      },
      // Test case 4: The injected descriptor array is empty.
      {}};

  for (const auto& input_descriptors : test_cases) {
    // Inject the test input through the custom description query callback.
    chromeos::clipboard_history::SetQueryItemDescriptorsImpl(
        chromeos::clipboard_history::QueryItemDescriptorsImpl());
    chromeos::clipboard_history::SetQueryItemDescriptorsImpl(
        base::BindLambdaForTesting(
            [&input_descriptors]() { return input_descriptors; }));

    // Trigger the update to the client.
    clipboard_history_ash_->UpdateRemoteDescriptorsForTesting();

    // Check the clipboard history item descriptors that the client receives.
    auto received_ptrs = WaitForReceivedDescriptors();
    std::vector<mojom::ClipboardHistoryItemDescriptor> received_descriptors;
    for (const auto& ptr : received_ptrs) {
      received_descriptors.emplace_back(ptr->item_id, ptr->display_format,
                                        ptr->display_text, ptr->file_count);
    }
    EXPECT_EQ(input_descriptors, received_descriptors);
  }
}

// Verifies that the client should receive the expected descriptors when
// clipboard history availability is toggled.
TEST_F(ClipboardHistoryAshWithClientTest, ClipboardHistoryAvailabilityToggled) {
  RegisterClient();
  {
    ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
    scw.WriteText(u"A");
  }

  EXPECT_THAT(WaitForReceivedDescriptors(),
              ElementsAre(DescriptorMatches(
                  crosapi::mojom::ClipboardHistoryDisplayFormat::kText, u"A")));

  {
    ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
    scw.WriteText(u"B");
  }

  EXPECT_THAT(
      WaitForReceivedDescriptors(),
      ElementsAre(
          DescriptorMatches(
              crosapi::mojom::ClipboardHistoryDisplayFormat::kText, u"B"),
          DescriptorMatches(
              crosapi::mojom::ClipboardHistoryDisplayFormat::kText, u"A")));

  // Lock the screen. Clipboard history is no longer available, and its clients
  // should be updated accordingly.
  ash::TestSessionControllerClient* test_session_client =
      GetSessionControllerClient();
  test_session_client->SetSessionState(session_manager::SessionState::LOCKED);

  // `mock_client` should receive an empty array of descriptors.
  EXPECT_TRUE(WaitForReceivedDescriptors().empty());

  // Activate the session. `mock_client` should receive the descriptors that
  // were received before the session lock.
  test_session_client->SetSessionState(session_manager::SessionState::ACTIVE);
  EXPECT_THAT(
      WaitForReceivedDescriptors(),
      ElementsAre(
          DescriptorMatches(
              crosapi::mojom::ClipboardHistoryDisplayFormat::kText, u"B"),
          DescriptorMatches(
              crosapi::mojom::ClipboardHistoryDisplayFormat::kText, u"A")));
}

// Verifies that the client should receive descriptors after registration
// if there are clipboard history items.
TEST_F(ClipboardHistoryAshWithClientTest, RegisterWithNonEmptyHistoryItems) {
  const std::vector<mojom::ClipboardHistoryItemDescriptor> initial_descriptors =
      {
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kText, u"dummy text",
           /*file_count=*/0u},
          {base::UnguessableToken::Create(),
           mojom::ClipboardHistoryDisplayFormat::kPng, u"dummy png",
           /*file_count=*/0u},
      };

  chromeos::clipboard_history::SetQueryItemDescriptorsImpl(
      chromeos::clipboard_history::QueryItemDescriptorsImpl());
  chromeos::clipboard_history::SetQueryItemDescriptorsImpl(
      base::BindLambdaForTesting(
          [&initial_descriptors]() { return initial_descriptors; }));

  RegisterClient();
  EXPECT_THAT(
      WaitForReceivedDescriptors(),
      ElementsAre(
          DescriptorMatches(
              crosapi::mojom::ClipboardHistoryDisplayFormat::kText,
              u"dummy text"),
          DescriptorMatches(crosapi::mojom::ClipboardHistoryDisplayFormat::kPng,
                            u"dummy png")));
}

// Verifies that the client should not receive any remote calls from Ash on
// descriptor update after registration if clipboard history is empty.
TEST_F(ClipboardHistoryAshWithClientTest, RegisterWithEmptyHistoryItems) {
  EXPECT_CALL(mock_client_, SetClipboardHistoryItemDescriptors).Times(0);
  RegisterClient();
  clipboard_history_ash_->FlushForTesting();
}

}  // namespace crosapi