blob: 5216dee98282c47093dc74be2de56d779a1d27aa [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/test_support/clipboard_history_item_builder.h"
#include "ash/test/ash_test_base.h"
#include "base/files/file_path.h"
#include "base/strings/string_util.h"
#include "base/test/icu_test_util.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/strings/grit/ui_strings.h"
namespace ash {
namespace {
using ::testing::Bool;
using ::testing::Combine;
using ::testing::Optional;
using ::testing::Values;
using ::testing::WithParamInterface;
struct FormatPair {
ui::ClipboardInternalFormat clipboard_format;
crosapi::mojom::ClipboardHistoryDisplayFormat display_format;
};
} // namespace
using ClipboardHistoryItemTest = AshTestBase;
// Verifies that a callback can be added to an item to run iff the item's
// display image is updated.
TEST_F(ClipboardHistoryItemTest, SetDisplayImageNotifiesCallback) {
// Create a clipboard history item.
ClipboardHistoryItemBuilder builder;
builder.SetFormat(ui::ClipboardInternalFormat::kHtml);
ClipboardHistoryItem item = builder.Build();
EXPECT_EQ(item.display_format(),
crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml);
ASSERT_TRUE(item.display_image().has_value());
EXPECT_EQ(item.display_image().value(),
clipboard_history_util::GetHtmlPreviewPlaceholder());
{
SCOPED_TRACE("Set a callback that is run.");
// Set a callback to be notified when the item's display image is updated.
base::MockCallback<base::RepeatingClosure> callback;
auto subscription = item.AddDisplayImageUpdatedCallback(callback.Get());
EXPECT_CALL(callback, Run());
// Update the item's display image. The callback should be run.
item.SetDisplayImage(
ui::ImageModel::FromImage(gfx::test::CreateImage(100, 50)));
// Verify that the display image was, in fact, updated.
EXPECT_NE(item.display_image().value(),
clipboard_history_util::GetHtmlPreviewPlaceholder());
}
{
SCOPED_TRACE("Set a callback that is not run because the item was copied.");
base::MockCallback<base::RepeatingClosure> callback;
auto subscription = item.AddDisplayImageUpdatedCallback(callback.Get());
EXPECT_CALL(callback, Run()).Times(0);
// Copy the item and update the new item's display image. The callback
// should not be run.
ClipboardHistoryItem copied_item(item);
copied_item.SetDisplayImage(
ui::ImageModel::FromImage(gfx::test::CreateImage(100, 50)));
}
{
SCOPED_TRACE("Set a callback that is not run because the item was moved.");
base::MockCallback<base::RepeatingClosure> callback;
auto subscription = item.AddDisplayImageUpdatedCallback(callback.Get());
EXPECT_CALL(callback, Run()).Times(0);
// Move the item and update the new item's display image. The callback
// should not be run.
ClipboardHistoryItem moved_item(std::move(item));
moved_item.SetDisplayImage(
ui::ImageModel::FromImage(gfx::test::CreateImage(100, 50)));
}
}
// Base class for tests parameterized by whether the clipboard history refresh
// is enabled.
class ClipboardHistoryItemRefreshTest
: public ClipboardHistoryItemTest,
public WithParamInterface</*enable_refresh=*/bool> {
public:
ClipboardHistoryItemRefreshTest() {
scoped_feature_list_.InitWithFeatureStates(
{{chromeos::features::kClipboardHistoryRefresh,
IsClipboardHistoryRefreshEnabled()},
{chromeos::features::kJelly, IsClipboardHistoryRefreshEnabled()}});
}
bool IsClipboardHistoryRefreshEnabled() { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
ClipboardHistoryItemRefreshTest,
/*enable_refresh=*/Bool());
TEST_P(ClipboardHistoryItemRefreshTest, DisplayText) {
base::test::ScopedRestoreICUDefaultLocale locale("en_US");
// Populate a builder with all the data formats that we expect to handle.
ClipboardHistoryItemBuilder builder;
builder.SetText("Text")
.SetMarkup("HTML with no image or table tags")
.SetRtf("Rtf")
.SetFilenames({ui::FileInfo(base::FilePath("/path/to/File.txt"),
base::FilePath("File.txt")),
ui::FileInfo(base::FilePath("/path/to/Other%20File.txt"),
base::FilePath("Other%20File.txt"))})
.SetBookmarkTitle("Bookmark Title")
.SetPng(gfx::test::CreatePNGBytes(10))
.SetFileSystemData({u"/path/to/Third%20File.txt"})
.SetWebSmartPaste(true);
// PNG data always takes precedence. When we must show text rather than the
// image itself, we display the PNG placeholder text.
EXPECT_EQ(builder.Build().display_text(),
l10n_util::GetStringUTF16(IDS_CLIPBOARD_MENU_IMAGE));
builder.ClearPng();
// In the absence of PNG data, HTML data takes precedence, but we use the
// plain-text format for the label.
EXPECT_EQ(builder.Build().display_text(), u"Text");
builder.ClearText();
// If plain text does not exist, we show a placeholder label.
EXPECT_EQ(builder.Build().display_text(),
l10n_util::GetStringUTF16(IDS_CLIPBOARD_MENU_HTML));
builder.SetText("Text");
builder.ClearMarkup();
// In the absence of HTML data, text data takes precedence.
EXPECT_EQ(builder.Build().display_text(), u"Text");
builder.ClearText();
// In the absence of text data, RTF data takes precedence.
EXPECT_EQ(builder.Build().display_text(),
l10n_util::GetStringUTF16(IDS_CLIPBOARD_MENU_RTF_CONTENT));
builder.ClearRtf();
// In the absence of RTF data, filename data takes precedence.
EXPECT_EQ(builder.Build().display_text(), IsClipboardHistoryRefreshEnabled()
? u"2 files"
: u"File.txt, Other File.txt");
builder.ClearFilenames();
// In the absence of filename data, bookmark data takes precedence.
EXPECT_EQ(builder.Build().display_text(), u"Bookmark Title");
builder.ClearBookmarkTitle();
// In the absence of bookmark data, web smart paste data takes precedence.
EXPECT_EQ(builder.Build().display_text(),
l10n_util::GetStringUTF16(IDS_CLIPBOARD_MENU_WEB_SMART_PASTE));
builder.ClearWebSmartPaste();
// In the absence of web smart paste data, file system data takes precedence.
// NOTE: File system data is the only kind of custom data currently supported.
EXPECT_EQ(builder.Build().display_text(), u"Third File.txt");
}
// Base class for tests parameterized by the type of item being tested, based on
// its display format. The clipboard history refresh enablement status is also
// parameterized as the refresh changes whether some items will have icons.
class ClipboardHistoryItemDisplayFormatTest
: public ClipboardHistoryItemTest,
public WithParamInterface<
std::tuple<FormatPair, /*enable_refresh=*/bool>> {
public:
ClipboardHistoryItemDisplayFormatTest() {
scoped_feature_list_.InitWithFeatureStates(
{{chromeos::features::kClipboardHistoryRefresh,
IsClipboardHistoryRefreshEnabled()},
{chromeos::features::kJelly, IsClipboardHistoryRefreshEnabled()}});
}
ClipboardHistoryItem BuildClipboardHistoryItem() const {
ClipboardHistoryItemBuilder builder;
builder.SetFormat(GetClipboardFormat());
ClipboardHistoryItem item = builder.Build();
EXPECT_EQ(item.display_format(), GetDisplayFormat());
return item;
}
ui::ClipboardInternalFormat GetClipboardFormat() const {
return std::get<0>(GetParam()).clipboard_format;
}
crosapi::mojom::ClipboardHistoryDisplayFormat GetDisplayFormat() const {
return std::get<0>(GetParam()).display_format;
}
bool IsClipboardHistoryRefreshEnabled() { return std::get<1>(GetParam()); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
ClipboardHistoryItemDisplayFormatTest,
Combine(
Values(FormatPair{ui::ClipboardInternalFormat::kText,
crosapi::mojom::ClipboardHistoryDisplayFormat::kText},
FormatPair{ui::ClipboardInternalFormat::kPng,
crosapi::mojom::ClipboardHistoryDisplayFormat::kPng},
FormatPair{ui::ClipboardInternalFormat::kHtml,
crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml},
FormatPair{
ui::ClipboardInternalFormat::kFilenames,
crosapi::mojom::ClipboardHistoryDisplayFormat::kFile}),
/*enable_refresh=*/Bool()));
TEST_P(ClipboardHistoryItemDisplayFormatTest, Icon) {
const auto item = BuildClipboardHistoryItem();
const auto& maybe_icon = item.icon();
if (IsClipboardHistoryRefreshEnabled() ||
GetDisplayFormat() ==
crosapi::mojom::ClipboardHistoryDisplayFormat::kFile) {
ASSERT_TRUE(maybe_icon.has_value());
EXPECT_TRUE(maybe_icon.value().IsVectorIcon());
} else {
EXPECT_FALSE(maybe_icon.has_value());
}
}
TEST_P(ClipboardHistoryItemDisplayFormatTest, DisplayImage) {
const auto item = BuildClipboardHistoryItem();
const auto& maybe_image = item.display_image();
switch (GetDisplayFormat()) {
case crosapi::mojom::ClipboardHistoryDisplayFormat::kUnknown:
NOTREACHED_NORETURN();
case crosapi::mojom::ClipboardHistoryDisplayFormat::kText:
case crosapi::mojom::ClipboardHistoryDisplayFormat::kFile:
EXPECT_FALSE(maybe_image);
break;
case crosapi::mojom::ClipboardHistoryDisplayFormat::kPng:
ASSERT_TRUE(maybe_image);
EXPECT_TRUE(maybe_image.value().IsImage());
break;
case crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml:
// Because the HTML placeholder image is a static `ImageModel`,
// `maybe_image` might be a vector icon or an image depending on which
// test cases are being run. What we know reliably is that the value of
// `maybe_image` should always be the current placeholder instance.
EXPECT_THAT(
maybe_image,
Optional(clipboard_history_util::GetHtmlPreviewPlaceholder()));
break;
}
}
} // namespace ash