#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;
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();
}
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()));
}
}
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) {
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()));
}
}
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_byte_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_;
raw_ptr<ClipboardHistory, DanglingUntriaged> clipboard_history_ = nullptr;
};
TEST_F(ClipboardHistoryTest, NothingCopiedNothingSaved) {
WriteAndEnsureTextHistory({},
{});
}
TEST_F(ClipboardHistoryTest, OneThingCopiedOneThingSaved) {
std::vector<std::u16string> input_strings{u"test"};
std::vector<std::u16string> expected_strings = input_strings;
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
TEST_F(ClipboardHistoryTest, DuplicateBasic) {
std::vector<std::u16string> input_strings{u"test", u"test"};
std::vector<std::u16string> expected_strings{u"test"};
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
TEST_F(ClipboardHistoryTest, InSameSequenceBasic) {
std::vector<std::u16string> input_strings{u"test1", u"test2", u"test3"};
std::vector<std::u16string> expected_strings{u"test3"};
WriteAndEnsureTextHistory(input_strings, expected_strings,
true);
}
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;
std::ranges::reverse(expected_strings);
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
TEST_F(ClipboardHistoryTest, DuplicatePrecedesPreviousRecord) {
std::vector<std::u16string> input_strings{u"test1", u"test2", u"test1",
u"test3"};
std::vector<std::u16string> expected_strings{u"test3", u"test1", u"test2"};
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
TEST_F(ClipboardHistoryTest, ClearingClipboardHistoryClearsClipboard) {
std::vector<std::u16string> input_strings{u"test1", u"test2", u"test1"};
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);
auto* const clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
ASSERT_TRUE(clipboard);
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
ASSERT_FALSE(clipboard->GetClipboardData(&data_dst));
}
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);
auto* const clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
ASSERT_TRUE(clipboard);
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
ASSERT_FALSE(clipboard->GetClipboardData(&data_dst));
EnsureTextHistory(expected_strings_after_clear);
}
TEST_F(ClipboardHistoryTest, ClearEmptyClipboard) {
ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste);
}
TEST_F(ClipboardHistoryTest, ClearEmptyClipboardHistory) {
clipboard_history()->Clear();
}
TEST_F(ClipboardHistoryTest, HistoryLimit) {
std::vector<std::u16string> input_strings{u"test1", u"test2", u"test3",
u"test4", u"test5", u"test6"};
std::vector<std::u16string> expected_strings{input_strings.begin() + 1,
input_strings.end()};
std::ranges::reverse(expected_strings);
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
TEST_F(ClipboardHistoryTest, PauseHistoryBasic) {
std::vector<std::u16string> input_strings{u"test1", u"test2", u"test1"};
std::vector<std::u16string> expected_strings{};
ScopedClipboardHistoryPauseImpl scoped_pause(clipboard_history());
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
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;
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);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
}
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;
WriteAndEnsureTextHistory(input_strings, expected_strings_initial);
base::HistogramTester histogram_tester;
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
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);
{
ScopedClipboardHistoryPauseImpl scoped_pause_allow_reorders(
clipboard_history(),
clipboard_history_util::PauseBehavior::kAllowReorderOnPaste);
WriteAndEnsureTextHistory(input_string1, expected_strings_reordered1);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
{
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);
}
WriteAndEnsureTextHistory(input_string2, expected_strings_reordered2);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
}
WriteAndEnsureTextHistory(input_string3, expected_strings_reordered2);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
}
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"};
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);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
scoped_pause_default.reset();
WriteAndEnsureTextHistory(input_string2, expected_strings_reordered2);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 0u);
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);
EXPECT_EQ(operation_confirmed_future.Take(), true);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.Operation", 1u);
}
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);
}
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);
}
TEST_F(ClipboardHistoryTest, DuplicateBitmapEncodingPreserved) {
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};
std::vector<SkBitmap> expected_bitmaps{test_bitmap_2, test_bitmap_1};
WriteAndEnsureBitmapHistory(input_bitmaps, expected_bitmaps);
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());
task_environment()->FastForwardBy(base::Seconds(1));
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteImage(test_bitmap_1);
}
base::RunLoop().RunUntilIdle();
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);
}
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"}};
WriteAndEnsureCustomDataHistory(input_data, {});
}
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);
}
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);
}
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();
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);
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);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
1u);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
2u);
event_generator->ReleaseKey(ui::VKEY_V, ui::EF_NONE);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlToVDelayV2",
1u);
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ControlVHeldTime",
2u);
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);
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);
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);
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
PressAndReleaseKey(ui::VKEY_X, ui::EF_CONTROL_DOWN);
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);
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);
}
}