chromium/ash/clipboard/clipboard_history_unittest.cc

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

#include "ash/clipboard/clipboard_history.h"

#include <list>
#include <memory>
#include <unordered_map>

#include "ash/clipboard/clipboard_history_controller_impl.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/scoped_clipboard_history_pause_impl.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/pickle.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/clipboard_non_backed.h"
#include "ui/base/clipboard/clipboard_util.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/skia_util.h"

namespace ash {

class ClipboardHistoryTest : public AshTestBase {
 public:
  ClipboardHistoryTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ClipboardHistoryTest(const ClipboardHistoryTest&) = delete;
  ClipboardHistoryTest& operator=(const ClipboardHistoryTest&) = delete;
  ~ClipboardHistoryTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    clipboard_history_ = const_cast<ClipboardHistory*>(
        Shell::Get()->clipboard_history_controller()->history());
    event_generator_ = std::make_unique<ui::test::EventGenerator>(
        ash::Shell::GetPrimaryRootWindow());
  }

  const std::list<ClipboardHistoryItem>& GetClipboardHistoryItems() {
    return clipboard_history_->GetItems();
  }

  ui::test::EventGenerator* GetEventGenerator() {
    return event_generator_.get();
  }

  // Writes |input_strings| to the clipboard buffer and ensures that
  // |expected_strings| are retained in history. If |in_same_sequence| is true,
  // writes to the buffer will be performed in the same task sequence.
  void WriteAndEnsureTextHistory(
      const std::vector<std::u16string>& input_strings,
      const std::vector<std::u16string>& expected_strings,
      bool in_same_sequence = false) {
    for (const auto& input_string : input_strings) {
      {
        ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
        scw.WriteText(input_string);
      }
      if (!in_same_sequence) {
        base::RunLoop().RunUntilIdle();
      }
    }
    if (in_same_sequence) {
      base::RunLoop().RunUntilIdle();
    }
    EnsureTextHistory(expected_strings);
  }

  void EnsureTextHistory(const std::vector<std::u16string>& expected_strings) {
    const std::list<ClipboardHistoryItem>& items = GetClipboardHistoryItems();
    EXPECT_EQ(expected_strings.size(), items.size());

    int expected_strings_index = 0;
    for (const auto& item : items) {
      EXPECT_EQ(expected_strings[expected_strings_index++],
                base::UTF8ToUTF16(item.data().text()));
    }
  }

  // Writes |input_bitmaps| to the clipboard buffer and ensures that
  // |expected_bitmaps| are retained in history. Writes to the buffer are
  // performed in different task sequences to simulate real world behavior.
  void WriteAndEnsureBitmapHistory(std::vector<SkBitmap>& input_bitmaps,
                                   std::vector<SkBitmap>& expected_bitmaps) {
    for (const auto& input_bitmap : input_bitmaps) {
      {
        ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
        scw.WriteImage(input_bitmap);
      }
      base::RunLoop().RunUntilIdle();
    }
    const std::list<ClipboardHistoryItem>& items = GetClipboardHistoryItems();
    EXPECT_EQ(expected_bitmaps.size(), items.size());

    int expected_bitmaps_index = 0;
    for (const auto& item : items) {
      // The PNG should not have yet been encoded.
      const auto& maybe_png = item.data().maybe_png();
      EXPECT_FALSE(maybe_png.has_value());

      auto maybe_bitmap = item.data().GetBitmapIfPngNotEncoded();
      EXPECT_TRUE(maybe_bitmap.has_value());
      EXPECT_TRUE(gfx::BitmapsAreEqual(
          expected_bitmaps[expected_bitmaps_index++], maybe_bitmap.value()));
    }
  }

  // Writes |input_data| to the clipboard buffer and ensures that
  // |expected_data| is retained in history. After writing to the buffer, the
  // current task sequence is run to idle to simulate real world behavior.
  void WriteAndEnsureCustomDataHistory(
      const std::unordered_map<std::u16string, std::u16string>& input_data,
      const std::unordered_map<std::u16string, std::u16string>& expected_data) {
    base::Pickle input_data_pickle;
    ui::WriteCustomDataToPickle(input_data, &input_data_pickle);

    {
      ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
      scw.WritePickledData(input_data_pickle,
                           ui::ClipboardFormatType::DataTransferCustomType());
    }
    base::RunLoop().RunUntilIdle();

    const std::list<ClipboardHistoryItem> items = GetClipboardHistoryItems();
    EXPECT_EQ(expected_data.empty() ? 0u : 1u, items.size());

    if (expected_data.empty()) {
      return;
    }

    std::optional<std::unordered_map<std::u16string, std::u16string>>
        actual_data = ui::ReadCustomDataIntoMap(base::as_bytes(
            base::span(items.front().data().GetDataTransferCustomData())));

    EXPECT_EQ(expected_data, actual_data);
  }

  ClipboardHistory* clipboard_history() { return clipboard_history_; }

 private:
  std::unique_ptr<ui::test::EventGenerator> event_generator_;
  // Owned by ClipboardHistoryControllerImpl.
  raw_ptr<ClipboardHistory, DanglingUntriaged> clipboard_history_ = nullptr;
};

// Tests that with nothing copied, nothing is shown.
TEST_F(ClipboardHistoryTest, NothingCopiedNothingSaved) {
  // When nothing is copied, nothing should be saved.
  WriteAndEnsureTextHistory(/*input_strings=*/{},
                            /*expected_strings=*/{});
}

// Tests that if one thing is copied, one thing is saved.
TEST_F(ClipboardHistoryTest, OneThingCopiedOneThingSaved) {
  std::vector<std::u16string> input_strings{u"test"};
  std::vector<std::u16string> expected_strings = input_strings;

  // Test that only one string is in history.
  WriteAndEnsureTextHistory(input_strings, expected_strings);
}

// Tests that if the same (non bitmap) thing is copied, only one of the
// duplicates is in the list.
TEST_F(ClipboardHistoryTest, DuplicateBasic) {
  std::vector<std::u16string> input_strings{u"test", u"test"};
  std::vector<std::u16string> expected_strings{u"test"};

  // Test that both things are saved.
  WriteAndEnsureTextHistory(input_strings, expected_strings);
}

// Tests that if multiple things are copied in the same task sequence, only the
// most recent thing is saved.
TEST_F(ClipboardHistoryTest, InSameSequenceBasic) {
  std::vector<std::u16string> input_strings{u"test1", u"test2", u"test3"};
  // Because |input_strings| will be copied in the same task sequence, history
  // should only retain the most recent thing.
  std::vector<std::u16string> expected_strings{u"test3"};

  // Test that only the most recent thing is saved.
  WriteAndEnsureTextHistory(input_strings, expected_strings,
                            /*in_same_sequence=*/true);
}

// Tests the ordering of history is in reverse chronological order.
TEST_F(ClipboardHistoryTest, HistoryIsReverseChronological) {
  std::vector<std::u16string> input_strings{u"test1", u"test2", u"test3",
                                            u"test4"};
  std::vector<std::u16string> expected_strings = input_strings;
  // Reverse the vector, history should match this ordering.
  std::reverse(std::begin(expected_strings), std::end(expected_strings));
  WriteAndEnsureTextHistory(input_strings, expected_strings);
}

// Tests that when a duplicate is copied, the existing duplicate item moves up
// to the front of the clipboard history.
TEST_F(ClipboardHistoryTest, DuplicatePrecedesPreviousRecord) {
  // Input holds four strings, two of which are the same.
  std::vector<std::u16string> input_strings{u"test1", u"test2", u"test1",
                                            u"test3"};
  // The result should be a reversal of the copied elements. When a duplicate
  // is copied, history will have that item moved to the front instead of adding
  // a new item.
  std::vector<std::u16string> expected_strings{u"test3", u"test1", u"test2"};

  WriteAndEnsureTextHistory(input_strings, expected_strings);
}

// Tests that nothing is saved after history is cleared.
TEST_F(ClipboardHistoryTest, ClearingClipboardHistoryClearsClipboard) {
  std::vector<std::u16string> input_strings{u"test1", u"test2", u"test1"};
  // The result should be empty due to history being cleared.
  std::vector<std::u16string> expected_strings{};

  for (const auto& input_string : input_strings) {
    ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
    scw.WriteText(input_string);
  }

  clipboard_history()->Clear();
  EnsureTextHistory(expected_strings);

  // The clipboard should also be empty after the clipboard history is cleared.
  auto* const clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
  ASSERT_TRUE(clipboard);
  ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
  ASSERT_FALSE(clipboard->GetClipboardData(&data_dst));
}

// Tests that clipboard history is cleared when the clipboard is cleared.
TEST_F(ClipboardHistoryTest, ClearingClipboardClearsClipboardHistory) {
  std::vector<std::u16string> input_strings{u"test1", u"test2"};

  std::vector<std::u16string> expected_strings_before_clear{u"test2", u"test1"};
  std::vector<std::u16string> expected_strings_after_clear{};

  for (const auto& input_string : input_strings) {
    {
      ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
      scw.WriteText(input_string);
    }
    base::RunLoop().RunUntilIdle();
  }

  EnsureTextHistory(expected_strings_before_clear);

  ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste);

  // The clipboard should be empty after being cleared.
  auto* const clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
  ASSERT_TRUE(clipboard);
  ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
  ASSERT_FALSE(clipboard->GetClipboardData(&data_dst));

  // The clipboard history should also be empty when the clipboard is cleared.
  EnsureTextHistory(expected_strings_after_clear);
}

// Tests that there is no crash when an empty clipboard is cleared with empty
// clipboard history.
TEST_F(ClipboardHistoryTest, ClearEmptyClipboard) {
  ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste);
}

// Tests that there is no crash when an empty clipboard history is cleared with
// empty clipboard.
TEST_F(ClipboardHistoryTest, ClearEmptyClipboardHistory) {
  clipboard_history()->Clear();
}

// Tests that the limit of clipboard history is respected.
TEST_F(ClipboardHistoryTest, HistoryLimit) {
  std::vector<std::u16string> input_strings{u"test1", u"test2", u"test3",
                                            u"test4", u"test5", u"test6"};

  // The result should be a reversal of the last five elements.
  std::vector<std::u16string> expected_strings{input_strings.begin() + 1,
                                               input_strings.end()};
  std::reverse(expected_strings.begin(), expected_strings.end());
  WriteAndEnsureTextHistory(input_strings, expected_strings);
}

// Tests that pausing clipboard history results in no history collected.
TEST_F(ClipboardHistoryTest, PauseHistoryBasic) {
  std::vector<std::u16string> input_strings{u"test1", u"test2", u"test1"};
  // Because history is paused, there should be nothing stored.
  std::vector<std::u16string> expected_strings{};

  ScopedClipboardHistoryPauseImpl scoped_pause(clipboard_history());
  WriteAndEnsureTextHistory(input_strings, expected_strings);
}

// Tests that pausing clipboard history with the `kAllowReorderOnPaste` pause
// behavior allows clipboard history to be modified, but clipboard data changes
// received during the pause are not recorded as copy operations.
TEST_F(ClipboardHistoryTest, PauseHistoryAllowReorders) {
  std::vector<std::u16string> input_strings{u"test1", u"test2"};
  std::vector<std::u16string> input_string1{u"test1"};
  std::vector<std::u16string> expected_strings_initial{u"test2", u"test1"};
  std::vector<std::u16string> expected_strings_reordered = input_strings;

  // Populate clipboard history to simulate paste-based reorders.
  WriteAndEnsureTextHistory(input_strings, expected_strings_initial);

  base::HistogramTester histogram_tester;
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  ScopedClipboardHistoryPauseImpl scoped_pause(
      clipboard_history(),
      clipboard_history_util::PauseBehavior::kAllowReorderOnPaste);
  WriteAndEnsureTextHistory(input_string1, expected_strings_reordered);
  // Clipboard history modifications made during a reorder-on-paste operation
  // should not count as copies (or pastes). A reorder is not a user action.
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
}

// Tests that when already-paused clipboard history is paused again with a
// different behavior, the newest behavior overrides all others for the duration
// of the pause's lifetime.
TEST_F(ClipboardHistoryTest, PauseHistoryNested) {
  std::vector<std::u16string> input_strings{u"test1", u"test2"};
  std::vector<std::u16string> input_string1{u"test1"};
  std::vector<std::u16string> input_string2{u"test2"};
  std::vector<std::u16string> input_string3{u"test3"};
  std::vector<std::u16string> expected_strings_initial{u"test2", u"test1"};
  std::vector<std::u16string> expected_strings_reordered1 = input_strings;
  std::vector<std::u16string> expected_strings_reordered2 =
      expected_strings_initial;

  // Populate clipboard history to simulate paste-based reorders.
  WriteAndEnsureTextHistory(input_strings, expected_strings_initial);

  base::HistogramTester histogram_tester;
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  // By default, pausing prevents clipboard history modifications.
  ScopedClipboardHistoryPauseImpl scoped_pause_default_1(
      clipboard_history(), clipboard_history_util::PauseBehavior::kDefault);
  WriteAndEnsureTextHistory(input_string3, expected_strings_initial);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  {
    // When allowing paste-based reorders, clipboard history should be modified.
    // Ensure that nesting pauses causes earlier pause behavior to be
    // overridden.
    ScopedClipboardHistoryPauseImpl scoped_pause_allow_reorders(
        clipboard_history(),
        clipboard_history_util::PauseBehavior::kAllowReorderOnPaste);
    WriteAndEnsureTextHistory(input_string1, expected_strings_reordered1);
    // Clipboard history modifications made during a reorder-on-paste operation
    // should not count as copies (or pastes). A reorder is not a user action.
    histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

    {
      // Test that the newest behavior always applies, regardless of what order
      // behaviors were overridden.
      ScopedClipboardHistoryPauseImpl scoped_pause_default_2(
          clipboard_history(), clipboard_history_util::PauseBehavior::kDefault);
      WriteAndEnsureTextHistory(input_string3, expected_strings_reordered1);
      histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
    }

    // Test that the previous behavior is restored when the newest pause goes
    // out of scope.
    WriteAndEnsureTextHistory(input_string2, expected_strings_reordered2);
    histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
  }

  // Test that the previous behavior is restored when the newest pause goes out
  // of scope, regardless of what order behaviors were overridden.
  WriteAndEnsureTextHistory(input_string3, expected_strings_reordered2);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
}

// Tests that clipboard history pauses do not have to be destroyed in LIFO
// order.
TEST_F(ClipboardHistoryTest, PauseHistoryResumeOutOfOrder) {
  std::vector<std::u16string> input_strings{u"test1", u"test2"};
  std::vector<std::u16string> input_string1{u"test1"};
  std::vector<std::u16string> input_string2{u"test2"};
  std::vector<std::u16string> input_string3{u"test3"};
  std::vector<std::u16string> expected_strings_initial{u"test2", u"test1"};
  std::vector<std::u16string> expected_strings_reordered1 = input_strings;
  std::vector<std::u16string> expected_strings_reordered2 =
      expected_strings_initial;
  std::vector<std::u16string> expected_strings_new_item = {u"test3", u"test2",
                                                           u"test1"};

  // Populate clipboard history to simulate paste-based reorders.
  WriteAndEnsureTextHistory(input_strings, expected_strings_initial);

  base::HistogramTester histogram_tester;
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  auto scoped_pause_default = std::make_unique<ScopedClipboardHistoryPauseImpl>(
      clipboard_history(), clipboard_history_util::PauseBehavior::kDefault);
  WriteAndEnsureTextHistory(input_string3, expected_strings_initial);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  auto scoped_pause_allow_reorders =
      std::make_unique<ScopedClipboardHistoryPauseImpl>(
          clipboard_history(),
          clipboard_history_util::PauseBehavior::kAllowReorderOnPaste);
  WriteAndEnsureTextHistory(input_string1, expected_strings_reordered1);
  // Clipboard history modifications made during a reorder-on-paste operation
  // should not count as copies (or pastes). A reorder is not a user action.
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  // Verify that pauses can be destroyed in non-LIFO order without changing the
  // current pause behavior.
  scoped_pause_default.reset();
  WriteAndEnsureTextHistory(input_string2, expected_strings_reordered2);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);

  // Verify that when all pauses are destroyed, clipboard history is modified as
  // usual.
  base::test::TestFuture<bool> operation_confirmed_future;
  Shell::Get()
      ->clipboard_history_controller()
      ->set_confirmed_operation_callback_for_test(
          operation_confirmed_future.GetRepeatingCallback());
  scoped_pause_allow_reorders.reset();
  WriteAndEnsureTextHistory(input_string3, expected_strings_new_item);
  // Since clipboard history is not paused in any way, data being written to the
  // clipboard is interpreted as a copy operation.
  EXPECT_EQ(operation_confirmed_future.Take(), true);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 1u);
}

// Tests that bitmaps are recorded in clipboard history.
TEST_F(ClipboardHistoryTest, BasicBitmap) {
  SkBitmap test_bitmap = gfx::test::CreateBitmap(3, 2);
  std::vector<SkBitmap> input_bitmaps{test_bitmap};
  std::vector<SkBitmap> expected_bitmaps{test_bitmap};

  WriteAndEnsureBitmapHistory(input_bitmaps, expected_bitmaps);
}

// Tests that duplicate bitmaps show up in history as one item placed in
// most-recent order.
TEST_F(ClipboardHistoryTest, DuplicateBitmap) {
  SkBitmap test_bitmap_1 = gfx::test::CreateBitmap(3, 2);
  SkBitmap test_bitmap_2 = gfx::test::CreateBitmap(4, 3);

  std::vector<SkBitmap> input_bitmaps{test_bitmap_1, test_bitmap_2,
                                      test_bitmap_1};
  std::vector<SkBitmap> expected_bitmaps{test_bitmap_1, test_bitmap_2};
  WriteAndEnsureBitmapHistory(input_bitmaps, expected_bitmaps);
}

// Tests that when a duplicate bitmap is written to clipboard history and the
// first bitmap has been encoded to a PNG, the encoded PNG is still set on the
// clipboard history item's data after deduplication.
TEST_F(ClipboardHistoryTest, DuplicateBitmapEncodingPreserved) {
  SkBitmap test_bitmap_1 = gfx::test::CreateBitmap(3, 2);
  SkBitmap test_bitmap_2 = gfx::test::CreateBitmap(4, 3);

  // Write image data to clipboard.
  std::vector<SkBitmap> input_bitmaps{test_bitmap_1, test_bitmap_2};
  std::vector<SkBitmap> expected_bitmaps{test_bitmap_2, test_bitmap_1};
  WriteAndEnsureBitmapHistory(input_bitmaps, expected_bitmaps);

  // Encode the image belonging to the data that will be written again.
  const std::list<ClipboardHistoryItem>& items = GetClipboardHistoryItems();
  ASSERT_EQ(items.size(), 2u);
  const auto& data_to_duplicate = items.back().data();
  const auto original_sequence_number_token =
      data_to_duplicate.sequence_number_token();
  const auto original_timestamp = items.back().time_copied();
  EXPECT_FALSE(data_to_duplicate.maybe_png());
  auto png = ui::clipboard_util::EncodeBitmapToPng(test_bitmap_1);
  data_to_duplicate.SetPngDataAfterEncoding(png);
  EXPECT_TRUE(data_to_duplicate.maybe_png());

  // Write first image to clipboard again after some time passes.
  task_environment()->FastForwardBy(base::Seconds(1));
  {
    ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
    scw.WriteImage(test_bitmap_1);
  }
  base::RunLoop().RunUntilIdle();

  // Verify that the encoded image data was preserved while deduplicating data
  // and reordering items in clipboard history.
  ASSERT_EQ(items.size(), 2u);
  EXPECT_GT(items.front().time_copied(), original_timestamp);
  const auto& duplicated_data = items.front().data();
  EXPECT_EQ(duplicated_data, data_to_duplicate);
  EXPECT_NE(duplicated_data.sequence_number_token(),
            original_sequence_number_token);
  ASSERT_TRUE(duplicated_data.maybe_png());
  EXPECT_EQ(*duplicated_data.maybe_png(), png);
}

// Tests that unrecognized custom data is omitted from clipboard history.
TEST_F(ClipboardHistoryTest, BasicCustomData) {
  const std::unordered_map<std::u16string, std::u16string> input_data = {
      {u"custom-format-1", u"custom-data-1"},
      {u"custom-format-2", u"custom-data-2"}};

  // Custom data which is not recognized is omitted from history.
  WriteAndEnsureCustomDataHistory(input_data, /*expected_data=*/{});
}

// Tests that file system data is recorded in clipboard history.
TEST_F(ClipboardHistoryTest, BasicFileSystemData) {
  const std::unordered_map<std::u16string, std::u16string> input_data = {
      {u"fs/sources", u"/path/to/My%20File.txt"}};

  const std::unordered_map<std::u16string, std::u16string> expected_data =
      input_data;

  WriteAndEnsureCustomDataHistory(input_data, expected_data);
}

// Tests that the display format for HTML with no <img> or <table> tags is text.
TEST_F(ClipboardHistoryTest, DisplayFormatForPlainHTML) {
  ui::ClipboardData data;
  data.set_markup_data("plain html with no img or table tags");
  EXPECT_EQ(ClipboardHistoryItem(data).display_format(),
            crosapi::mojom::ClipboardHistoryDisplayFormat::kText);

  data.set_markup_data("<img> </img>");
  EXPECT_EQ(ClipboardHistoryItem(data).display_format(),
            crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml);
}

// Tests that exactly one Ash.ClipboardHistory.ControlToVDelayV2 histogram entry
// is recorded every time Ctrl is pressed as part of a Ctrl+V paste sequence and
// that exactly one Ash.ClipboardHistory.ControlVHeldTime histogram entry is
// recorded every time V is pressed as part of a Ctrl+V paste sequence.
TEST_F(ClipboardHistoryTest, RecordControlVMetrics) {
  base::HistogramTester histogram_tester;
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    0u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    0u);

  auto* const event_generator = GetEventGenerator();

  // Press Ctrl+V and end the paste by releasing V. One entry should be recorded
  // for each histogram.
  event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
  PressAndReleaseKey(ui::VKEY_V, ui::EF_CONTROL_DOWN);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    1u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    1u);

  // Press V again, injecting an extra press to simulate a keyboard auto-repeat
  // from holding V down. Neither of these V presses is the first in the paste
  // sequence, so no entry should be recorded for the ControlToVDelayV2
  // histogram.
  event_generator->PressKey(ui::VKEY_V, ui::EF_CONTROL_DOWN);
  event_generator->PressKey(ui::VKEY_V, ui::EF_CONTROL_DOWN | ui::EF_IS_REPEAT);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    1u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    1u);

  // Release Ctrl to end the paste sequence. An entry should be recorded for the
  // ControlVHeldTime histogram.
  event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    1u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    2u);

  // Release V so that no more keys are pressed. No entry should be recorded for
  // the ControlVHeldTime histogram, because the paste sequence already ended.
  event_generator->ReleaseKey(ui::VKEY_V, ui::EF_NONE);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    1u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    2u);

  // Press Ctrl+V and end the paste by pressing a key other than Ctrl or V. One
  // entry should be recorded for each histogram.
  event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
  event_generator->PressKey(ui::VKEY_V, ui::EF_CONTROL_DOWN);
  PressAndReleaseKey(ui::VKEY_X, ui::EF_CONTROL_DOWN);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    2u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    3u);

  // Release V and Ctrl so that no more keys are pressed. No histogram entries
  // should be recorded.
  event_generator->ReleaseKey(ui::VKEY_V, ui::EF_CONTROL_DOWN);
  event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    2u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    3u);

  // Hold Shift while pressing and releasing Ctrl+V. No histogram entries should
  // be recorded.
  event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
  event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_SHIFT_DOWN);
  PressAndReleaseKey(ui::VKEY_V, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_SHIFT_DOWN);
  event_generator->ReleaseKey(ui::VKEY_SHIFT, ui::EF_NONE);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    2u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    3u);

  // Press Ctrl, then press and release a key other than V, then press and
  // release V. One entry should be recorded for each histogram.
  event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
  PressAndReleaseKey(ui::VKEY_X, ui::EF_CONTROL_DOWN);

  // Allow some time between the arbitrary key press and the V key press, during
  // which time Ctrl is pressed again.
  task_environment()->FastForwardBy(base::Milliseconds(100));
  event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_IS_REPEAT);
  task_environment()->FastForwardBy(base::Milliseconds(100));

  PressAndReleaseKey(ui::VKEY_V, ui::EF_CONTROL_DOWN);
  event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);

  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
                                    3u);
  histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
                                    4u);

  // The ControlToVDelayV2 histogram should have one recorded sample of 200ms
  // from the last Ctrl+V; the other recorded samples should all be 0ms. 200ms
  // comes from the delay between the last paste sequence's first Ctrl press and
  // its first V press (the sequence's second Ctrl press between the 100ms
  // pauses should not affect metrics).
  histogram_tester.ExpectTimeBucketCount(
      "Ash.ClipboardHistory.ControlToVDelayV2", base::Milliseconds(0), 2);
  histogram_tester.ExpectTimeBucketCount(
      "Ash.ClipboardHistory.ControlToVDelayV2", base::Milliseconds(100), 0);
  histogram_tester.ExpectTimeBucketCount(
      "Ash.ClipboardHistory.ControlToVDelayV2", base::Milliseconds(200), 1);
}

}  // namespace ash