blob: f6db9ce153f12e4b22e71c218908dcc4b2626c8a [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 <unordered_map>
#include "ash/clipboard/clipboard_history_controller.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/events/test/event_generator.h"
namespace ash {
class ClipboardHistoryTest : public AshTestBase {
public:
ClipboardHistoryTest() = default;
ClipboardHistoryTest(const ClipboardHistoryTest&) = delete;
ClipboardHistoryTest& operator=(const ClipboardHistoryTest&) = delete;
~ClipboardHistoryTest() override = default;
// AshTestBase:
void SetUp() override {
scoped_feature_list_.InitWithFeatures(
{chromeos::features::kClipboardHistory}, {});
AshTestBase::SetUp();
clipboard_history_ = const_cast<ClipboardHistory*>(
Shell::Get()->clipboard_history_controller()->history());
}
const std::list<ClipboardHistoryItem>& GetClipboardHistoryItems() {
return clipboard_history_->GetItems();
}
// 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<base::string16>& input_strings,
const std::vector<base::string16>& 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<base::string16>& 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) {
EXPECT_TRUE(gfx::BitmapsAreEqual(
expected_bitmaps[expected_bitmaps_index++], item.data().bitmap()));
}
}
// 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<base::string16, base::string16>& input_data,
const std::unordered_map<base::string16, base::string16>& 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::GetWebCustomDataType());
}
base::RunLoop().RunUntilIdle();
const std::list<ClipboardHistoryItem> items = GetClipboardHistoryItems();
EXPECT_EQ(expected_data.empty() ? 0u : 1u, items.size());
if (expected_data.empty())
return;
std::unordered_map<base::string16, base::string16> actual_data;
ui::ReadCustomDataIntoMap(items.front().data().custom_data_data().c_str(),
items.front().data().custom_data_data().size(),
&actual_data);
EXPECT_EQ(expected_data, actual_data);
}
ClipboardHistory* clipboard_history() { return clipboard_history_; }
private:
base::test::ScopedFeatureList scoped_feature_list_;
// Owned by ClipboardHistoryController.
ClipboardHistory* 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<base::string16> input_strings{base::UTF8ToUTF16("test")};
std::vector<base::string16> 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, both things are saved.
TEST_F(ClipboardHistoryTest, DuplicateBasic) {
std::vector<base::string16> input_strings{base::UTF8ToUTF16("test"),
base::UTF8ToUTF16("test")};
std::vector<base::string16> expected_strings = input_strings;
// 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<base::string16> input_strings{base::UTF8ToUTF16("test1"),
base::UTF8ToUTF16("test2"),
base::UTF8ToUTF16("test3")};
// Because |input_strings| will be copied in the same task sequence, history
// should only retain the most recent thing.
std::vector<base::string16> expected_strings{base::UTF8ToUTF16("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<base::string16> input_strings{
base::UTF8ToUTF16("test1"), base::UTF8ToUTF16("test2"),
base::UTF8ToUTF16("test3"), base::UTF8ToUTF16("test4")};
std::vector<base::string16> 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 duplicate shows up in the proper
// order and that the older version is still returned.
TEST_F(ClipboardHistoryTest, DuplicatePrecedesPreviousRecord) {
// Input holds a unique string sandwiched by a copy.
std::vector<base::string16> input_strings{
base::UTF8ToUTF16("test1"), base::UTF8ToUTF16("test2"),
base::UTF8ToUTF16("test1"), base::UTF8ToUTF16("test3")};
// The result should be a reversal of the copied elements. When a duplicate
// is copied, history will show all versions of the recent duplicate.
std::vector<base::string16> expected_strings{
base::UTF8ToUTF16("test3"), base::UTF8ToUTF16("test1"),
base::UTF8ToUTF16("test2"), base::UTF8ToUTF16("test1")};
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
// Tests that nothing is saved after history is cleared.
TEST_F(ClipboardHistoryTest, ClearHistoryBasic) {
// Input holds a unique string sandwhiched by a copy.
std::vector<base::string16> input_strings{base::UTF8ToUTF16("test1"),
base::UTF8ToUTF16("test2"),
base::UTF8ToUTF16("test1")};
// The result should be a reversal of the last two elements. When a duplicate
// is copied, history will show the most recent version of that duplicate.
std::vector<base::string16> 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);
}
// Tests that the limit of clipboard history is respected.
TEST_F(ClipboardHistoryTest, HistoryLimit) {
std::vector<base::string16> input_strings{
base::UTF8ToUTF16("test1"), base::UTF8ToUTF16("test2"),
base::UTF8ToUTF16("test3"), base::UTF8ToUTF16("test4"),
base::UTF8ToUTF16("test5"), base::UTF8ToUTF16("test6")};
// The result should be a reversal of the last five elements.
std::vector<base::string16> 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, PauseHistory) {
std::vector<base::string16> input_strings{base::UTF8ToUTF16("test1"),
base::UTF8ToUTF16("test2"),
base::UTF8ToUTF16("test1")};
// Because history is paused, there should be nothing stored.
std::vector<base::string16> expected_strings{};
ClipboardHistory::ScopedPause scoped_pause(clipboard_history());
WriteAndEnsureTextHistory(input_strings, expected_strings);
}
// Tests that bitmaps are recorded in clipboard history.
TEST_F(ClipboardHistoryTest, BasicBitmap) {
SkBitmap test_bitmap;
test_bitmap.allocN32Pixels(3, 2);
test_bitmap.eraseARGB(255, 0, 255, 0);
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 in most-recent order.
TEST_F(ClipboardHistoryTest, DuplicateBitmap) {
SkBitmap test_bitmap_1;
test_bitmap_1.allocN32Pixels(3, 2);
test_bitmap_1.eraseARGB(255, 0, 255, 0);
SkBitmap test_bitmap_2;
test_bitmap_2.allocN32Pixels(3, 2);
test_bitmap_2.eraseARGB(0, 255, 0, 0);
std::vector<SkBitmap> input_bitmaps{test_bitmap_1, test_bitmap_2,
test_bitmap_1};
std::vector<SkBitmap> expected_bitmaps = input_bitmaps;
WriteAndEnsureBitmapHistory(input_bitmaps, expected_bitmaps);
}
// Tests that unrecognized custom data is omitted from clipboard history.
TEST_F(ClipboardHistoryTest, BasicCustomData) {
const std::unordered_map<base::string16, base::string16> input_data = {
{base::UTF8ToUTF16("custom-format-1"),
base::UTF8ToUTF16("custom-data-1")},
{base::UTF8ToUTF16("custom-format-2"),
base::UTF8ToUTF16("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<base::string16, base::string16> input_data = {
{base::UTF8ToUTF16("fs/sources"),
base::UTF8ToUTF16("/path/to/My%20File.txt")}};
const std::unordered_map<base::string16, base::string16> expected_data =
input_data;
WriteAndEnsureCustomDataHistory(input_data, expected_data);
}
// Verifies the features related to the menu view.
class ClipboardHistoryControllerTest : public ClipboardHistoryTest {
public:
ClipboardHistoryControllerTest() = default;
ClipboardHistoryControllerTest(const ClipboardHistoryControllerTest&) =
delete;
ClipboardHistoryControllerTest& operator=(
const ClipboardHistoryControllerTest&) = delete;
~ClipboardHistoryControllerTest() override = default;
// ClipboardHistoryTest:
void SetUp() override {
ClipboardHistoryTest::SetUp();
image_model_factory_ = std::make_unique<FakeClipboardImageModelFactory>();
}
void ShowClipboardHistoryMenuByAccelerator() {
GetEventGenerator()->PressKey(ui::KeyboardCode::VKEY_V,
ui::EF_COMMAND_DOWN);
GetEventGenerator()->ReleaseKey(ui::KeyboardCode::VKEY_V,
ui::EF_COMMAND_DOWN);
}
void HideClipboardHistoryMenuByAccelerator() {
GetEventGenerator()->PressKey(ui::KeyboardCode::VKEY_ESCAPE, ui::EF_NONE);
GetEventGenerator()->ReleaseKey(ui::KeyboardCode::VKEY_ESCAPE, ui::EF_NONE);
}
private:
class FakeClipboardImageModelFactory : public ClipboardImageModelFactory {
public:
FakeClipboardImageModelFactory() = default;
FakeClipboardImageModelFactory(const FakeClipboardImageModelFactory&) =
delete;
FakeClipboardImageModelFactory& operator=(
const FakeClipboardImageModelFactory&) = delete;
~FakeClipboardImageModelFactory() override = default;
// ClipboardImageModelFactory:
void Render(const base::UnguessableToken&,
const std::string&,
ImageModelCallback) override {}
void CancelRequest(const base::UnguessableToken&) override {}
void Activate() override {}
void Deactivate() override {}
void OnShutdown() override {}
};
std::unique_ptr<FakeClipboardImageModelFactory> image_model_factory_;
};
// Verifies that the clipboard history is disabled in some user modes, which
// means that the clipboard history should not be recorded and meanwhile the
// menu view should not show (https://crbug.com/1100739).
TEST_F(ClipboardHistoryControllerTest, VerifyAvailabilityInUserModes) {
// Write one item into the clipboard history.
const base::string16 input(base::UTF8ToUTF16("text"));
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(input);
}
base::RunLoop().RunUntilIdle();
EnsureTextHistory({input});
const base::UnguessableToken& original_first_item_id =
GetClipboardHistoryItems().begin()->id();
constexpr struct {
user_manager::UserType user_type;
bool is_enabled;
} kTestCases[] = {{user_manager::USER_TYPE_REGULAR, true},
{user_manager::USER_TYPE_GUEST, true},
{user_manager::USER_TYPE_PUBLIC_ACCOUNT, false},
{user_manager::USER_TYPE_SUPERVISED, true},
{user_manager::USER_TYPE_KIOSK_APP, false},
{user_manager::USER_TYPE_CHILD, true},
{user_manager::USER_TYPE_ARC_KIOSK_APP, false},
{user_manager::USER_TYPE_WEB_KIOSK_APP, false}};
UserSession session;
session.session_id = 1u;
session.user_info.account_id = AccountId::FromUserEmail("user1@test.com");
session.user_info.display_name = "User 1";
session.user_info.display_email = "user1@test.com";
for (const auto& test_case : kTestCases) {
// Switch to the target user mode.
session.user_info.type = test_case.user_type;
Shell::Get()->session_controller()->UpdateUserSession(session);
// Write a new item into the clipboard buffer.
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(input);
}
base::RunLoop().RunUntilIdle();
if (test_case.is_enabled) {
// Verify that the new item should be included in the clipboard history
// and the menu should be able to show.
EXPECT_EQ(2u, GetClipboardHistoryItems().size());
EXPECT_NE(original_first_item_id,
GetClipboardHistoryItems().begin()->id());
ShowClipboardHistoryMenuByAccelerator();
EXPECT_TRUE(
Shell::Get()->clipboard_history_controller()->IsMenuShowing());
HideClipboardHistoryMenuByAccelerator();
EXPECT_FALSE(
Shell::Get()->clipboard_history_controller()->IsMenuShowing());
// Restore the clipboard history by removing the new item.
clipboard_history()->RemoveItemForId(
GetClipboardHistoryItems().begin()->id());
} else {
// Verify that the new item should not be written into the clipboard
// history. The menu cannot show although the clipboard history is
// non-empty.
EXPECT_EQ(1u, GetClipboardHistoryItems().size());
EXPECT_EQ(original_first_item_id,
GetClipboardHistoryItems().begin()->id());
ShowClipboardHistoryMenuByAccelerator();
EXPECT_FALSE(
Shell::Get()->clipboard_history_controller()->IsMenuShowing());
}
}
}
} // namespace ash