#include <memory>
#include <string>
#include <vector>
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_controller_impl.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/public/cpp/session/session_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/color_util.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/test/view_drawn_waiter.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "chromeos/ui/clipboard_history/clipboard_history_util.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/vector_icons/vector_icons.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/clipboard_data.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/menu_source_type.mojom.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"
#include "ui/gfx/vector_icon_types.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/controls/textfield/textfield_test_api.h"
namespace ash {
using crosapi::mojom::ClipboardHistoryControllerShowSource;
namespace {
MATCHER_P2(MenuItemsMatch, labels, icons, "") {
if (arg.size() != labels.size() || arg.size() != icons.size()) {
return false;
}
for (size_t index = 0; index < labels.size(); ++index) {
if (arg[index].label != labels[index] ||
!gfx::test::AreImagesEqual(arg[index].icon, icons[index])) {
return false;
}
}
return true;
}
class MockObserver : public ClipboardHistoryController::Observer {
public:
MockObserver() {
scoped_observation_.Observe(ClipboardHistoryController::Get());
}
MOCK_METHOD(void, OnClipboardHistoryItemsUpdated, (), (override));
private:
base::ScopedObservation<ClipboardHistoryController,
ClipboardHistoryController::Observer>
scoped_observation_{this};
};
class MockClipboardImageModelFactory : public ClipboardImageModelFactory {
public:
MockClipboardImageModelFactory() = default;
MockClipboardImageModelFactory(const MockClipboardImageModelFactory&) =
delete;
MockClipboardImageModelFactory& operator=(
const MockClipboardImageModelFactory&) = delete;
~MockClipboardImageModelFactory() override = default;
void Render(const base::UnguessableToken& clipboard_history_item_id,
const std::string& markup,
const gfx::Size& bounding_box_size,
ImageModelCallback callback) override {
std::move(callback).Run(
ui::ImageModel::FromImageSkia(gfx::ImageSkia::CreateFrom1xBitmap(
gfx::test::CreateBitmap(2, 2))));
}
void CancelRequest(const base::UnguessableToken& request_id) override {}
void Activate() override {}
void Deactivate() override {}
void RenderCurrentPendingRequests() override {}
void OnShutdown() override {}
};
struct MenuItemDescriptor {
MenuItemDescriptor(const std::u16string& input_label,
const gfx::Image& input_icon)
: label(input_label), icon(input_icon) {}
const std::u16string label;
const gfx::Image icon;
};
void FlushMessageLoop() {
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
void ExpectHistoryItemImageMatchesBitmap(const ClipboardHistoryItem& item,
const SkBitmap& expected_bitmap) {
EXPECT_EQ(item.display_format(),
crosapi::mojom::ClipboardHistoryDisplayFormat::kPng);
const auto& image = item.display_image();
ASSERT_TRUE(image.has_value());
ASSERT_TRUE(image.value().IsImage());
ASSERT_FALSE(image.value().IsEmpty());
EXPECT_TRUE(gfx::BitmapsAreEqual(*image.value().GetImage().ToSkBitmap(),
expected_bitmap));
}
std::vector<ClipboardHistoryControllerShowSource>
GetClipboardHistoryShowSources() {
std::vector<ClipboardHistoryControllerShowSource> sources;
for (int i =
static_cast<int>(ClipboardHistoryControllerShowSource::kMinValue);
i <= static_cast<int>(ClipboardHistoryControllerShowSource::kMaxValue);
++i) {
if (static_cast<ClipboardHistoryControllerShowSource>(i) !=
ClipboardHistoryControllerShowSource::kControlVLongpress) {
sources.push_back(static_cast<ClipboardHistoryControllerShowSource>(i));
}
}
return sources;
}
}
class ClipboardHistoryControllerTest : public AshTestBase {
public:
ClipboardHistoryControllerTest() = default;
ClipboardHistoryControllerTest(const ClipboardHistoryControllerTest&) =
delete;
ClipboardHistoryControllerTest& operator=(
const ClipboardHistoryControllerTest&) = delete;
~ClipboardHistoryControllerTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
mock_image_factory_ = std::make_unique<MockClipboardImageModelFactory>();
GetClipboardHistoryController()->set_confirmed_operation_callback_for_test(
operation_confirmed_future_.GetRepeatingCallback());
}
ClipboardHistoryControllerImpl* GetClipboardHistoryController() {
return Shell::Get()->clipboard_history_controller();
}
void ShowMenu() { PressAndReleaseKey(ui::VKEY_V, ui::EF_COMMAND_DOWN); }
void WaitForOperationConfirmed() {
EXPECT_TRUE(operation_confirmed_future_.Take());
}
void WriteImageToClipboardAndConfirm(const SkBitmap& bitmap) {
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteImage(bitmap);
}
WaitForOperationConfirmed();
}
void WriteTextToClipboardAndConfirm(const std::u16string& str) {
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(str);
}
WaitForOperationConfirmed();
}
std::vector<ClipboardHistoryItem> GetHistoryValues() {
base::test::TestFuture<std::vector<ClipboardHistoryItem>> future;
GetClipboardHistoryController()->GetHistoryValues(future.GetCallback());
return future.Take();
}
void TestEnteringLockScreen() {
GetClipboardHistoryController()->BlockGetHistoryValuesForTest();
base::test::TestFuture<std::vector<ClipboardHistoryItem>> future;
GetClipboardHistoryController()->GetHistoryValues(future.GetCallback());
EXPECT_FALSE(future.IsReady());
auto* session_controller = Shell::Get()->session_controller();
session_controller->LockScreen();
GetSessionControllerClient()->FlushForTest();
EXPECT_TRUE(session_controller->IsScreenLocked());
GetClipboardHistoryController()->ResumeGetHistoryValuesForTest();
auto locked_during_query_result = future.Take();
EXPECT_TRUE(locked_during_query_result.empty());
auto locked_before_query_result = GetHistoryValues();
EXPECT_TRUE(locked_before_query_result.empty());
}
protected:
base::test::TestFuture<bool> operation_confirmed_future_;
private:
std::unique_ptr<MockClipboardImageModelFactory> mock_image_factory_;
};
TEST_F(ClipboardHistoryControllerTest, NoHistoryNoMenu) {
base::HistogramTester histogram_tester;
ShowMenu();
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", 0);
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 0);
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 0);
}
TEST_F(ClipboardHistoryControllerTest, ShowMenu) {
base::HistogramTester histogram_tester;
WriteTextToClipboardAndConfirm(u"test");
ShowMenu();
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 0);
histogram_tester.ExpectBucketCount(
"Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", 1, 1);
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 1);
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 1);
histogram_tester.ExpectBucketCount(
"Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", 1, 1);
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 1);
ShowMenu();
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 1);
histogram_tester.ExpectBucketCount(
"Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", 1, 2);
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 2);
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.UserJourneyTime", 2);
histogram_tester.ExpectBucketCount(
"Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", 1, 2);
histogram_tester.ExpectTotalCount(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", 2);
}
TEST_F(ClipboardHistoryControllerTest, VerifyAvailabilityInUserModes) {
WriteTextToClipboardAndConfirm(u"text");
constexpr struct {
user_manager::UserType user_type;
bool is_enabled;
} kTestCases[] = {{user_manager::UserType::kRegular, true},
{user_manager::UserType::kGuest, true},
{user_manager::UserType::kPublicAccount, false},
{user_manager::UserType::kKioskChromeApp, false},
{user_manager::UserType::kChild, true},
{user_manager::UserType::kKioskWebApp, false}};
for (const auto& test_case : kTestCases) {
ClearLogin();
SimulateUserLogin({"user1@test.com", test_case.user_type});
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(u"test");
}
if (test_case.is_enabled) {
WaitForOperationConfirmed();
} else {
FlushMessageLoop();
EXPECT_FALSE(operation_confirmed_future_.IsReady());
}
const std::list<ClipboardHistoryItem>& items =
Shell::Get()->clipboard_history_controller()->history()->GetItems();
if (test_case.is_enabled) {
EXPECT_EQ(items.size(), 2u);
ShowMenu();
EXPECT_TRUE(
Shell::Get()->clipboard_history_controller()->IsMenuShowing());
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_FALSE(
Shell::Get()->clipboard_history_controller()->IsMenuShowing());
ClipboardHistory* clipboard_history = const_cast<ClipboardHistory*>(
Shell::Get()->clipboard_history_controller()->history());
clipboard_history->RemoveItemForId(items.begin()->id());
} else {
EXPECT_EQ(items.size(), 1u);
ShowMenu();
EXPECT_FALSE(
Shell::Get()->clipboard_history_controller()->IsMenuShowing());
}
}
}
TEST_F(ClipboardHistoryControllerTest, DisableInUserAddingScreen) {
WriteTextToClipboardAndConfirm(u"text");
Shell::Get()->session_controller()->ShowMultiProfileLogin();
ShowMenu();
EXPECT_FALSE(Shell::Get()->clipboard_history_controller()->IsMenuShowing());
}
TEST_F(ClipboardHistoryControllerTest, VThenSearchDoesNotShowLauncher) {
GetEventGenerator()->PressKey(ui::VKEY_V, ui::EF_NONE);
GetEventGenerator()->PressKey(ui::VKEY_COMMAND, ui::EF_NONE);
GetEventGenerator()->ReleaseKey(ui::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_FALSE(Shell::Get()->app_list_controller()->IsVisible(
std::nullopt));
GetEventGenerator()->ReleaseKey(ui::VKEY_COMMAND, ui::EF_NONE);
EXPECT_FALSE(Shell::Get()->app_list_controller()->IsVisible(
std::nullopt));
}
TEST_F(ClipboardHistoryControllerTest, ClearClipboardClearsHistory) {
WriteTextToClipboardAndConfirm(u"test");
ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste);
FlushMessageLoop();
const std::list<ClipboardHistoryItem>& items =
Shell::Get()->clipboard_history_controller()->history()->GetItems();
EXPECT_TRUE(items.empty());
ShowMenu();
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
}
TEST_F(ClipboardHistoryControllerTest,
ClearingClipboardClosesClipboardHistory) {
WriteTextToClipboardAndConfirm(u"test");
ASSERT_TRUE(Shell::Get()->cursor_manager()->IsCursorVisible());
ShowMenu();
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
EXPECT_TRUE(Shell::Get()->cursor_manager()->IsCursorVisible());
ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste);
FlushMessageLoop();
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
}
TEST_F(ClipboardHistoryControllerTest, EncodeImage) {
SkBitmap test_bitmap = gfx::test::CreateBitmap(3, 2);
WriteImageToClipboardAndConfirm(test_bitmap);
auto result = GetHistoryValues();
ASSERT_EQ(result.size(), 1u);
ExpectHistoryItemImageMatchesBitmap(result[0], test_bitmap);
}
TEST_F(ClipboardHistoryControllerTest, EncodeMultipleImages) {
const std::vector<SkBitmap> test_bitmaps{
gfx::test::CreateBitmap(2, 1),
gfx::test::CreateBitmap(3, 2),
gfx::test::CreateBitmap(4, 3),
};
for (const auto& test_bitmap : test_bitmaps) {
WriteImageToClipboardAndConfirm(test_bitmap);
}
auto result = GetHistoryValues();
auto num_results = result.size();
ASSERT_EQ(num_results, test_bitmaps.size());
for (uint i = 0; i < num_results; ++i) {
ExpectHistoryItemImageMatchesBitmap(result[i],
test_bitmaps[num_results - 1 - i]);
}
}
TEST_F(ClipboardHistoryControllerTest, WriteBitmapWhileEncodingImage) {
const std::vector<SkBitmap> test_bitmaps{
gfx::test::CreateBitmap(3, 2),
gfx::test::CreateBitmap(4, 3),
};
WriteImageToClipboardAndConfirm(test_bitmaps[0]);
GetClipboardHistoryController()
->set_new_bitmap_to_write_while_encoding_for_test(test_bitmaps[1]);
GetClipboardHistoryController()->BlockGetHistoryValuesForTest();
base::test::TestFuture<std::vector<ClipboardHistoryItem>> future;
GetClipboardHistoryController()->GetHistoryValues(future.GetCallback());
EXPECT_FALSE(future.IsReady());
WaitForOperationConfirmed();
GetClipboardHistoryController()->ResumeGetHistoryValuesForTest();
auto result = future.Take();
auto num_results = result.size();
ASSERT_EQ(num_results, test_bitmaps.size());
for (uint i = 0; i < num_results; ++i) {
ExpectHistoryItemImageMatchesBitmap(result[i],
test_bitmaps[num_results - 1 - i]);
}
}
TEST_F(ClipboardHistoryControllerTest, LockedScreenText) {
WriteTextToClipboardAndConfirm(u"test");
auto history_list = GetHistoryValues();
ASSERT_EQ(history_list.size(), 1u);
ASSERT_EQ(history_list[0].display_text(), u"test");
TestEnteringLockScreen();
}
TEST_F(ClipboardHistoryControllerTest, LockedScreenImage) {
SkBitmap test_bitmap = gfx::test::CreateBitmap(3, 2);
WriteImageToClipboardAndConfirm(test_bitmap);
auto result = GetHistoryValues();
ASSERT_EQ(result.size(), 1u);
ExpectHistoryItemImageMatchesBitmap(result[0], test_bitmap);
TestEnteringLockScreen();
}
using ClipboardHistoryControllerObserverTest = ClipboardHistoryControllerTest;
TEST_F(ClipboardHistoryControllerObserverTest, AddAndRemoveItem) {
MockObserver mock_observer;
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(3);
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"B");
ClipboardHistoryController::Get()->DeleteClipboardItemById(
GetHistoryValues()[0].id().ToString());
GetClipboardHistoryController()->FireItemUpdateNotificationTimerForTest();
}
TEST_F(ClipboardHistoryControllerObserverTest, ClearHistory) {
MockObserver mock_observer;
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(3);
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"B");
ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste);
GetClipboardHistoryController()->FireItemUpdateNotificationTimerForTest();
}
TEST_F(ClipboardHistoryControllerObserverTest, Overflow) {
MockObserver mock_observer;
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(5);
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"B");
WriteTextToClipboardAndConfirm(u"C");
WriteTextToClipboardAndConfirm(u"D");
WriteTextToClipboardAndConfirm(u"E");
EXPECT_EQ(GetHistoryValues().size(),
static_cast<size_t>(clipboard_history_util::kMaxClipboardItems));
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated);
WriteTextToClipboardAndConfirm(u"F");
EXPECT_EQ(GetHistoryValues().size(),
static_cast<size_t>(clipboard_history_util::kMaxClipboardItems));
}
TEST_F(ClipboardHistoryControllerObserverTest,
ChangeSessionStateWithEmptyHistory) {
MockObserver mock_observer;
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(0);
TestSessionControllerClient* test_session_client =
GetSessionControllerClient();
test_session_client->SetSessionState(session_manager::SessionState::LOCKED);
test_session_client->FlushForTest();
test_session_client->SetSessionState(session_manager::SessionState::ACTIVE);
test_session_client->FlushForTest();
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated);
WriteTextToClipboardAndConfirm(u"A");
}
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_ChangeSessionStateWithNonEmptyHistory \
DISABLED_ChangeSessionStateWithNonEmptyHistory
#else
#define MAYBE_ChangeSessionStateWithNonEmptyHistory \
ChangeSessionStateWithNonEmptyHistory
#endif
TEST_F(ClipboardHistoryControllerObserverTest,
MAYBE_ChangeSessionStateWithNonEmptyHistory) {
MockObserver mock_observer;
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated);
WriteTextToClipboardAndConfirm(u"A");
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated);
TestSessionControllerClient* test_session_client =
GetSessionControllerClient();
test_session_client->SetSessionState(session_manager::SessionState::LOCKED);
test_session_client->FlushForTest();
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated).Times(0);
test_session_client->SetSessionState(
session_manager::SessionState::LOGGED_IN_NOT_ACTIVE);
test_session_client->FlushForTest();
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnClipboardHistoryItemsUpdated);
test_session_client->SetSessionState(session_manager::SessionState::ACTIVE);
test_session_client->FlushForTest();
}
class ClipboardHistoryControllerWithTextfieldTest
: public ClipboardHistoryControllerTest {
public:
void SetUp() override {
ClipboardHistoryControllerTest::SetUp();
textfield_widget_ = CreateFramelessTestWidget();
textfield_widget_->SetBounds(gfx::Rect(0, 0, 100, 100));
textfield_ = textfield_widget_->SetContentsView(
std::make_unique<views::Textfield>());
textfield_->GetViewAccessibility().SetName(u"Textfield");
textfield_->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
textfield_->RequestFocus();
ASSERT_TRUE(textfield_->HasFocus());
ASSERT_TRUE(textfield_->GetText().empty());
}
void ShowTextfieldContextMenu(const views::View& textfield) {
GetEventGenerator()->MoveMouseTo(
textfield.GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickRightButton();
}
std::unique_ptr<views::Widget> textfield_widget_;
raw_ptr<views::Textfield> textfield_;
};
TEST_F(ClipboardHistoryControllerWithTextfieldTest, PasteClipboardItemById) {
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"B");
WriteTextToClipboardAndConfirm(u"C");
WriteTextToClipboardAndConfirm(u"D");
WriteTextToClipboardAndConfirm(u"E");
const std::vector<ClipboardHistoryItem> items = GetHistoryValues();
ASSERT_EQ(items.size(), 5u);
GetClipboardHistoryController()->set_buffer_restoration_delay_for_test(
base::TimeDelta());
struct {
size_t paste_data_index;
crosapi::mojom::ClipboardHistoryControllerShowSource paste_source;
int event_flags;
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType paste_type;
} test_cases[] = {
{0,
crosapi::mojom::ClipboardHistoryControllerShowSource::kVirtualKeyboard,
ui::EF_NONE,
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType::
kRichTextVirtualKeyboard},
{1,
crosapi::mojom::ClipboardHistoryControllerShowSource::
kTextfieldContextMenu,
ui::EF_MOUSE_BUTTON,
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType::
kRichTextMouse},
{2,
crosapi::mojom::ClipboardHistoryControllerShowSource::
kRenderViewContextMenu,
ui::EF_SHIFT_DOWN | ui::EF_FROM_TOUCH,
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType::
kPlainTextTouch},
{3,
crosapi::mojom::ClipboardHistoryControllerShowSource::
kRenderViewContextSubmenu,
ui::EF_MOUSE_BUTTON,
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType::
kRichTextMouse},
{4,
crosapi::mojom::ClipboardHistoryControllerShowSource::
kTextfieldContextSubmenu,
ui::EF_MOUSE_BUTTON,
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType::
kRichTextMouse}};
for (auto& [paste_data_index, paste_source, event_flags, paste_type] :
test_cases) {
base::HistogramTester histogram_tester;
textfield_->SetText(std::u16string());
ClipboardHistoryController::Get()->PasteClipboardItemById(
items[paste_data_index].id().ToString(), event_flags, paste_source);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(textfield_->GetText(), items[paste_data_index].display_text());
histogram_tester.ExpectBucketCount("Ash.ClipboardHistory.PasteType",
paste_type,
1);
histogram_tester.ExpectBucketCount("Ash.ClipboardHistory.PasteSource",
paste_source,
1);
histogram_tester.ExpectBucketCount(
"Ash.ClipboardHistory.ContextMenu.MenuOptionSelected",
paste_data_index + 1, 1);
}
}
class ClipboardHistoryControllerShowSourceTest
: public ClipboardHistoryControllerTest,
public testing::WithParamInterface<ClipboardHistoryControllerShowSource> {
public:
ClipboardHistoryControllerShowSourceTest() = default;
ClipboardHistoryControllerShowSource GetSource() const { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
ClipboardHistoryControllerShowSourceTest,
testing::ValuesIn(GetClipboardHistoryShowSources()));
TEST_P(ClipboardHistoryControllerShowSourceTest, ShowMenuReturnsSuccess) {
base::HistogramTester histogram_tester;
EXPECT_FALSE(GetClipboardHistoryController()->ShowMenu(
gfx::Rect(), ui::mojom::MenuSourceType::kNone, GetSource()));
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ContextMenu.ShowMenu",
0);
WriteTextToClipboardAndConfirm(u"test");
auto* session_controller = Shell::Get()->session_controller();
session_controller->LockScreen();
GetSessionControllerClient()->FlushForTest();
EXPECT_TRUE(session_controller->IsScreenLocked());
EXPECT_FALSE(GetClipboardHistoryController()->ShowMenu(
gfx::Rect(), ui::mojom::MenuSourceType::kNone, GetSource()));
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.ContextMenu.ShowMenu",
0);
session_controller->HideLockScreen();
GetSessionControllerClient()->FlushForTest();
EXPECT_FALSE(session_controller->IsScreenLocked());
EXPECT_TRUE(GetClipboardHistoryController()->ShowMenu(
gfx::Rect(), ui::mojom::MenuSourceType::kNone, GetSource()));
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectUniqueSample(
"Ash.ClipboardHistory.ContextMenu.ShowMenu", GetSource(),
1);
EXPECT_FALSE(GetClipboardHistoryController()->ShowMenu(
gfx::Rect(), ui::mojom::MenuSourceType::kNone, GetSource()));
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
histogram_tester.ExpectUniqueSample(
"Ash.ClipboardHistory.ContextMenu.ShowMenu", GetSource(),
1);
}
TEST_P(ClipboardHistoryControllerShowSourceTest, OnMenuClosingCallback) {
base::test::TestFuture<bool> on_menu_closing_future;
base::HistogramTester histogram_tester;
WriteTextToClipboardAndConfirm(u"test");
gfx::Rect test_window_rect(100, 100, 100, 100);
std::unique_ptr<aura::Window> window(CreateTestWindow(test_window_rect));
GetClipboardHistoryController()->ShowMenu(
test_window_rect, ui::mojom::MenuSourceType::kNone, GetSource(),
on_menu_closing_future.GetRepeatingCallback());
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
EXPECT_FALSE(on_menu_closing_future.IsReady());
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
EXPECT_FALSE(on_menu_closing_future.Take());
FlushMessageLoop();
histogram_tester.ExpectTotalCount("Ash.ClipboardHistory.PasteSource",
0);
GetClipboardHistoryController()->ShowMenu(
test_window_rect, ui::mojom::MenuSourceType::kNone, GetSource(),
on_menu_closing_future.GetCallback());
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
EXPECT_FALSE(on_menu_closing_future.IsReady());
PressAndReleaseKey(ui::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
EXPECT_TRUE(on_menu_closing_future.Take());
FlushMessageLoop();
histogram_tester.ExpectUniqueSample("Ash.ClipboardHistory.PasteSource",
GetSource(),
1);
}
class ClipboardHistoryRefreshDisplayFormatTest
: public ClipboardHistoryControllerWithTextfieldTest,
public testing::WithParamInterface<
crosapi::mojom::
ClipboardHistoryDisplayFormat> {
public:
ClipboardHistoryRefreshDisplayFormatTest() = default;
std::vector<MenuItemDescriptor> WriteClipboardDataBasedOnParam() {
const ui::ColorProvider* color_provider = GetPrimaryWindowColorProvider();
CHECK(color_provider);
auto get_icon = [color_provider](const gfx::VectorIcon& icon) {
return gfx::Image(ui::ImageModel::FromVectorIcon(icon,
ui::kColorSysSecondary,
20)
.Rasterize(color_provider));
};
const std::u16string show_clipboard_menu_label =
l10n_util::GetStringUTF16(IDS_APP_SHOW_CLIPBOARD_HISTORY);
switch (GetDisplayFormat()) {
case crosapi::mojom::ClipboardHistoryDisplayFormat::kText:
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"B");
WriteTextToClipboardAndConfirm(u"https://google.com/");
return {{u"https://google.com/", get_icon(vector_icons::kLinkIcon)},
{u"B", get_icon(chromeos::kTextIcon)},
{u"A", get_icon(chromeos::kTextIcon)},
{show_clipboard_menu_label, gfx::Image()}};
case crosapi::mojom::ClipboardHistoryDisplayFormat::kPng:
WriteImageToClipboardAndConfirm(
gfx::test::CreateBitmap(3, 3));
WriteImageToClipboardAndConfirm(
gfx::test::CreateBitmap(2, 2));
return {{u"Image", get_icon(chromeos::kFiletypeImageIcon)},
{u"Image", get_icon(chromeos::kFiletypeImageIcon)},
{show_clipboard_menu_label, gfx::Image()}};
case crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml:
WriteHtmlAndConfirm("<table>A</table>");
WriteHtmlAndConfirm("<table>B></table>");
return {{u"HTML Content", get_icon(vector_icons::kCodeIcon)},
{u"HTML Content", get_icon(vector_icons::kCodeIcon)},
{show_clipboard_menu_label, gfx::Image()}};
case crosapi::mojom::ClipboardHistoryDisplayFormat::kFile:
WriteFilePathsAndConfirm({u"dummy_file.webm"});
WriteFilePathsAndConfirm({u"dummy_child1.jpg", u"dummy_child2.png"});
return {{u"2 files", get_icon(vector_icons::kContentCopyIcon)},
{u"dummy_file.webm", get_icon(chromeos::kFiletypeVideoIcon)},
{show_clipboard_menu_label, gfx::Image()}};
case crosapi::mojom::ClipboardHistoryDisplayFormat::kUnknown:
NOTREACHED();
}
return {};
}
void WriteFilePathsAndConfirm(const std::vector<std::u16string>& file_paths) {
{
base::Pickle pickle;
ui::WriteCustomDataToPickle(
std::unordered_map<std::u16string, std::u16string>(
{{u"fs/sources", base::JoinString(file_paths, u"\n")}}),
&pickle);
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WritePickledData(pickle,
ui::ClipboardFormatType::DataTransferCustomType());
}
WaitForOperationConfirmed();
}
void WriteHtmlAndConfirm(const std::string& html) {
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16(html), "");
}
WaitForOperationConfirmed();
}
crosapi::mojom::ClipboardHistoryDisplayFormat GetDisplayFormat() const {
return GetParam();
}
const ui::ColorProvider* GetPrimaryWindowColorProvider() {
auto* color_provider_source = ColorUtil::GetColorProviderSourceForWindow(
Shell::GetPrimaryRootWindow());
auto* color_provider = color_provider_source->GetColorProvider();
return color_provider;
}
};
INSTANTIATE_TEST_SUITE_P(
All,
ClipboardHistoryRefreshDisplayFormatTest,
testing::Values(crosapi::mojom::ClipboardHistoryDisplayFormat::kText,
crosapi::mojom::ClipboardHistoryDisplayFormat::kPng,
crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml,
crosapi::mojom::ClipboardHistoryDisplayFormat::kFile));
TEST_P(ClipboardHistoryRefreshDisplayFormatTest, TextServicesSubMenu) {
ShowTextfieldContextMenu(*textfield_);
views::TextfieldTestApi api(textfield_);
ui::MenuModel* const root_model = api.context_menu_contents();
ASSERT_TRUE(root_model);
ui::MenuModel* target_command_parent_model = root_model;
size_t target_command_index = 0u;
ui::MenuModel::GetModelAndIndexForCommandId(IDS_APP_PASTE_FROM_CLIPBOARD,
&target_command_parent_model,
&target_command_index);
EXPECT_EQ(target_command_parent_model, root_model);
EXPECT_GT(target_command_index, 0u);
EXPECT_FALSE(target_command_parent_model->IsEnabledAt(target_command_index));
const std::vector<MenuItemDescriptor> expected_submenu_items =
WriteClipboardDataBasedOnParam();
ASSERT_FALSE(expected_submenu_items.empty());
GetEventGenerator()->PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
ShowTextfieldContextMenu(*textfield_);
target_command_parent_model = api.context_menu_contents();
ui::MenuModel* const submenu_model =
target_command_parent_model->GetSubmenuModelAt(target_command_index);
EXPECT_TRUE(target_command_parent_model->IsEnabledAt(target_command_index));
EXPECT_EQ(target_command_parent_model->GetTypeAt(target_command_index),
ui::MenuModel::ItemType::TYPE_SUBMENU);
ASSERT_TRUE(submenu_model);
const ui::ColorProvider* color_provider = GetPrimaryWindowColorProvider();
std::vector<std::u16string> actual_labels;
std::vector<gfx::Image> actual_icons;
for (size_t index = 0; index < submenu_model->GetItemCount(); ++index) {
actual_labels.emplace_back(submenu_model->GetLabelAt(index));
const ui::ImageModel image_model = submenu_model->GetIconAt(index);
actual_icons.push_back(
image_model.IsEmpty()
? gfx::Image()
: gfx::Image(image_model.Rasterize(color_provider)));
}
EXPECT_THAT(expected_submenu_items,
MenuItemsMatch(actual_labels, actual_icons));
}
TEST_P(ClipboardHistoryRefreshDisplayFormatTest,
ShowStandaloneMenuFromSubmenu) {
WriteClipboardDataBasedOnParam();
ShowTextfieldContextMenu(*textfield_);
const views::MenuItemView* const submenu_item = WaitForMenuItemWithLabel(
l10n_util::GetStringUTF16(IDS_APP_PASTE_FROM_CLIPBOARD));
ASSERT_TRUE(submenu_item);
base::HistogramTester submenu_histogram_tester;
GetEventGenerator()->MoveMouseTo(
submenu_item->GetBoundsInScreen().CenterPoint());
views::View* const submenu_view = submenu_item->GetSubmenu();
ViewDrawnWaiter().Wait(submenu_view);
submenu_histogram_tester.ExpectUniqueSample(
"Ash.ClipboardHistory.ContextMenu.ShowMenu",
crosapi::mojom::ClipboardHistoryControllerShowSource::
kTextfieldContextSubmenu,
1);
const views::View* const menu_item = WaitForMenuItemWithLabel(
l10n_util::GetStringUTF16(IDS_APP_SHOW_CLIPBOARD_HISTORY));
ASSERT_TRUE(menu_item);
base::HistogramTester histogram_tester;
GetEventGenerator()->MoveMouseTo(
menu_item->GetBoundsInScreen().CenterPoint());
GetEventGenerator()->ClickLeftButton();
EXPECT_TRUE(Shell::Get()->clipboard_history_controller()->IsMenuShowing());
histogram_tester.ExpectUniqueSample(
"Ash.ClipboardHistory.ContextMenu.ShowMenu",
crosapi::mojom::ClipboardHistoryControllerShowSource::
kTextfieldContextMenu,
1);
}
}