blob: 991ffa988872274dbe5dd40913e6534180ce265d [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_menu_model_adapter.h"
#include <string>
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_controller_impl.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/views/clipboard_history_view_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "chromeos/ui/clipboard_history/clipboard_history_util.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/view_utils.h"
namespace ash {
using ::testing::AllOf;
using ::testing::Bool;
using ::testing::Combine;
using ::testing::Conditional;
using ::testing::Eq;
using ::testing::IsNull;
using ::testing::NotNull;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::Values;
using ::testing::ValuesIn;
using ::testing::WithParamInterface;
using crosapi::mojom::ClipboardHistoryControllerShowSource;
namespace {
// Helpers ---------------------------------------------------------------------
ClipboardHistoryControllerImpl* GetClipboardHistoryController() {
return Shell::Get()->clipboard_history_controller();
}
std::vector<ClipboardHistoryControllerShowSource>
GetClipboardHistoryShowSources() {
std::vector<ClipboardHistoryControllerShowSource> sources;
for (int i =
static_cast<int>(ClipboardHistoryControllerShowSource::kMinValue);
i <= static_cast<int>(ClipboardHistoryControllerShowSource::kMaxValue);
++i) {
sources.push_back(static_cast<ClipboardHistoryControllerShowSource>(i));
}
return sources;
}
void FlushMessageLoop() {
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
// Matchers --------------------------------------------------------------------
template <typename ViewType, typename MatcherType>
auto GetViewById(int id, MatcherType m) {
return ResultOf(
[id](const auto* arg) {
return views::AsViewClass<ViewType>(arg->GetViewByID(id));
},
m);
}
} // namespace
// ClipboardHistoryMenuModelAdapterRefreshTest ---------------------------------
// Base class for `ClipboardHistoryMenuModelAdapter` tests whose only required
// parameterization is whether the clipboard history refresh is enabled.
class ClipboardHistoryMenuModelAdapterRefreshTest
: public AshTestBase,
public WithParamInterface</*enable_refresh=*/bool> {
public:
ClipboardHistoryMenuModelAdapterRefreshTest() {
scoped_feature_list_.InitWithFeatureStates(
{{chromeos::features::kClipboardHistoryRefresh,
IsClipboardHistoryRefreshEnabled()},
{chromeos::features::kJelly, IsClipboardHistoryRefreshEnabled()}});
}
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
GetClipboardHistoryController()->set_confirmed_operation_callback_for_test(
operation_confirmed_future_.GetRepeatingCallback());
}
void WriteTextToClipboardAndConfirm(const std::u16string& str) {
EXPECT_FALSE(operation_confirmed_future_.IsReady());
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(str);
}
EXPECT_TRUE(operation_confirmed_future_.Take());
}
bool IsClipboardHistoryRefreshEnabled() const { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
base::test::TestFuture<bool> operation_confirmed_future_;
};
INSTANTIATE_TEST_SUITE_P(All,
ClipboardHistoryMenuModelAdapterRefreshTest,
/*enable_refresh=*/Bool());
TEST_P(ClipboardHistoryMenuModelAdapterRefreshTest, FirstItemShowsCtrlVLabel) {
// Write items to clipboard history so that the menu can show.
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"B");
WriteTextToClipboardAndConfirm(u"C");
// Show the clipboard history menu and wait for the first item to be selected
// so that item selection does not interfere with removing items later.
auto* const controller = GetClipboardHistoryController();
ASSERT_TRUE(controller);
base::RunLoop run_loop;
controller->set_initial_item_selected_callback_for_test(
run_loop.QuitClosure());
EXPECT_TRUE(controller->ShowMenu(
gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE,
ClipboardHistoryControllerShowSource::kDefaultValue));
run_loop.Run();
EXPECT_TRUE(controller->IsMenuShowing());
// Verify the number of items in the menu.
auto* const adapter = controller->context_menu_for_test();
ASSERT_EQ(adapter->GetMenuItemsCount(), 3u);
// Get handles to three clipboard history items' Ctrl+V labels, noting that
// the items' indices will be offset by 1 if the menu has a header.
const size_t offset = IsClipboardHistoryRefreshEnabled() ? 1u : 0u;
const auto* const ctrl_v_label1 =
adapter->GetMenuItemViewAtForTest(0u + offset)
->GetViewByID(clipboard_history_util::kCtrlVLabelID);
ASSERT_EQ(!!ctrl_v_label1, IsClipboardHistoryRefreshEnabled());
const auto* const ctrl_v_label2 =
adapter->GetMenuItemViewAtForTest(1u + offset)
->GetViewByID(clipboard_history_util::kCtrlVLabelID);
ASSERT_EQ(!!ctrl_v_label2, IsClipboardHistoryRefreshEnabled());
const auto* const ctrl_v_label3 =
adapter->GetMenuItemViewAtForTest(2u + offset)
->GetViewByID(clipboard_history_util::kCtrlVLabelID);
ASSERT_EQ(!!ctrl_v_label3, IsClipboardHistoryRefreshEnabled());
// If the labels do exist, test that only the first item's label is visible.
if (IsClipboardHistoryRefreshEnabled()) {
// Initially, the first item's label should be visible.
EXPECT_TRUE(ctrl_v_label1->GetVisible());
EXPECT_FALSE(ctrl_v_label2->GetVisible());
EXPECT_FALSE(ctrl_v_label3->GetVisible());
// Remove the first item. Now the second item's label should be visible.
adapter->RemoveMenuItemWithCommandId(
clipboard_history_util::kFirstItemCommandId);
FlushMessageLoop();
EXPECT_TRUE(ctrl_v_label2->GetVisible());
EXPECT_FALSE(ctrl_v_label3->GetVisible());
// Remove the second item. Now the third item's label should be visible.
adapter->RemoveMenuItemWithCommandId(
clipboard_history_util::kFirstItemCommandId + 1);
FlushMessageLoop();
EXPECT_TRUE(ctrl_v_label3->GetVisible());
}
}
TEST_P(ClipboardHistoryMenuModelAdapterRefreshTest,
TextItemHasExpectedDisplayTextLabel) {
// Write items to clipboard history so that the menu can show.
WriteTextToClipboardAndConfirm(u"A");
WriteTextToClipboardAndConfirm(u"https://google.com/");
// Show the clipboard history menu.
auto* const controller = GetClipboardHistoryController();
ASSERT_TRUE(controller);
EXPECT_TRUE(controller->ShowMenu(
gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE,
ClipboardHistoryControllerShowSource::kDefaultValue));
EXPECT_TRUE(controller->IsMenuShowing());
// Verify the number of items in the menu.
const auto* const adapter = controller->context_menu_for_test();
ASSERT_THAT(
adapter,
Property(&ClipboardHistoryMenuModelAdapter::GetMenuItemsCount, Eq(2u)));
// Verify expected display text labels.
const size_t offset = IsClipboardHistoryRefreshEnabled() ? 1u : 0u;
for (size_t i = 0u; i < 2u; ++i) {
const views::Label* display_text_label = views::AsViewClass<views::Label>(
adapter->GetMenuItemViewAtForTest(i + offset)
->GetViewByID(clipboard_history_util::kDisplayTextLabelID));
gfx::ElideBehavior elide_behavior = gfx::ELIDE_TAIL;
size_t max_lines = 1u;
if (IsClipboardHistoryRefreshEnabled()) {
if (chromeos::clipboard_history::IsUrl(display_text_label->GetText())) {
elide_behavior = gfx::ELIDE_MIDDLE;
} else {
max_lines = ClipboardHistoryViews::kTextItemMaxLines;
}
}
EXPECT_THAT(
display_text_label,
AllOf(Property(&views::Label::GetElideBehavior, Eq(elide_behavior)),
Property(&views::Label::GetMaxLines, Eq(max_lines)),
Property(&views::Label::GetMultiLine, Eq(max_lines > 1u))));
}
}
// Base class for `ClipboardHistoryMenuModelAdapter` tests that verify the
// presence of a menu header, a menu footer, both, or neither.
class ClipboardHistoryMenuModelAdapterMenuItemTest
: public AshTestBase,
public WithParamInterface<
std::tuple<ClipboardHistoryControllerShowSource,
/*time_since_menu_shown=*/std::optional<base::TimeDelta>,
/*time_since_nudge_shown=*/std::optional<base::TimeDelta>,
/*enable_refresh=*/bool>> {
public:
ClipboardHistoryMenuModelAdapterMenuItemTest()
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
scoped_feature_list_.InitWithFeatureStates(
{{features::kClipboardHistoryLongpress,
IsClipboardHistoryLongpressEnabled()},
{chromeos::features::kClipboardHistoryRefresh,
IsClipboardHistoryRefreshEnabled()},
{chromeos::features::kJelly, IsClipboardHistoryRefreshEnabled()}});
}
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
auto* session_controller = Shell::Get()->session_controller();
ASSERT_TRUE(session_controller);
auto* prefs = session_controller->GetLastActiveUserPrefService();
ASSERT_TRUE(prefs);
// Set nudge last time shown.
if (const auto& time_since_nudge_shown = GetTimeSinceNudgeShown()) {
ClipboardHistoryController::Get()->OnScreenshotNotificationCreated();
task_environment()->FastForwardBy(*time_since_nudge_shown);
}
// Set menu last time shown.
if (const auto& time_since_menu_shown = GetTimeSinceMenuShown()) {
prefs->SetTime(prefs::kMultipasteMenuLastTimeShown,
base::Time::Now() - time_since_menu_shown.value());
}
}
void WriteTextToClipboardAndFlushMessageLoop(const std::u16string& str) {
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(str);
}
FlushMessageLoop();
}
ClipboardHistoryControllerShowSource GetSource() const {
return std::get<0>(GetParam());
}
const std::optional<base::TimeDelta>& GetTimeSinceMenuShown() const {
return std::get<1>(GetParam());
}
const std::optional<base::TimeDelta>& GetTimeSinceNudgeShown() const {
return std::get<2>(GetParam());
}
bool IsClipboardHistoryLongpressEnabled() const {
return GetSource() ==
ClipboardHistoryControllerShowSource::kControlVLongpress;
}
bool IsClipboardHistoryRefreshEnabled() const {
return std::get<3>(GetParam());
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
ClipboardHistoryMenuModelAdapterMenuItemTest,
Combine(ValuesIn(GetClipboardHistoryShowSources()),
/*time_since_menu_shown=*/
Values(std::make_optional(base::Days(60)),
std::make_optional(base::Days(59)),
std::nullopt),
/*time_since_nudge_shown=*/
Values(std::make_optional(base::Seconds(61)),
std::make_optional(base::Seconds(60)),
std::nullopt),
/*enable_refresh=*/Bool()));
TEST_P(ClipboardHistoryMenuModelAdapterMenuItemTest,
HeaderAndFooterConditionallyPresent) {
// Write items to clipboard history so that the menu can show.
WriteTextToClipboardAndFlushMessageLoop(u"A");
WriteTextToClipboardAndFlushMessageLoop(u"B");
auto* const controller = GetClipboardHistoryController();
ASSERT_TRUE(controller);
EXPECT_EQ(controller->history()->GetItems().size(), 2u);
// Show the clipboard history menu.
EXPECT_TRUE(controller->ShowMenu(
gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE, GetSource()));
EXPECT_TRUE(controller->IsMenuShowing());
const auto time_since_menu_shown =
GetTimeSinceMenuShown().value_or(base::TimeDelta::Max());
const auto time_since_nudge_shown =
GetTimeSinceNudgeShown().value_or(base::TimeDelta::Max());
const bool has_header = IsClipboardHistoryRefreshEnabled();
const bool has_footer = IsClipboardHistoryLongpressEnabled() ||
(IsClipboardHistoryRefreshEnabled() &&
((time_since_menu_shown >= base::Days(60)) ||
(time_since_nudge_shown <= base::Seconds(60))));
// Verify the number of items in the menu model.
size_t expected_menu_item_count = controller->history()->GetItems().size();
if (has_header) {
// The menu's first item should be a header.
++expected_menu_item_count;
}
if (has_footer) {
// The menu's last item should be a footer.
++expected_menu_item_count;
}
const auto* const adapter = controller->context_menu_for_test();
const auto* const model = adapter->GetModelForTest();
ASSERT_TRUE(model);
ASSERT_EQ(model->GetItemCount(), expected_menu_item_count);
// Verify that the first item is a header iff the UI refresh is enabled.
EXPECT_EQ(model->GetTypeAt(0u), has_header
? ui::MenuModel::ItemType::TYPE_TITLE
: ui::MenuModel::ItemType::TYPE_COMMAND);
// Verify that the last item is a footer iff the menu was shown via Ctrl+V
// long-press.
EXPECT_EQ(model->GetTypeAt(model->GetItemCount() - 1u),
has_footer ? ui::MenuModel::ItemType::TYPE_TITLE
: ui::MenuModel::ItemType::TYPE_COMMAND);
if (!has_footer) {
return;
}
// Verify that footer content is of the expected version.
const int footer_index = model->GetItemCount() - 1u;
const auto* const footer = adapter->GetMenuItemViewAtForTest(footer_index);
EXPECT_THAT(
footer->GetViewByID(clipboard_history_util::kFooterContentViewID),
Conditional(IsClipboardHistoryRefreshEnabled(), IsNull(), NotNull()));
EXPECT_THAT(
footer->GetViewByID(clipboard_history_util::kFooterContentV2ViewID),
Conditional(
IsClipboardHistoryRefreshEnabled(),
GetViewById<views::StyledLabel>(
clipboard_history_util::kFooterContentV2LabelID,
Property(
&views::StyledLabel::GetText,
Conditional(
IsClipboardHistoryLongpressEnabled(),
l10n_util::GetStringUTF16(
IDS_ASH_CLIPBOARD_HISTORY_CONTROL_V_LONGPRESS_FOOTER),
l10n_util::GetStringFUTF16(
IDS_ASH_CLIPBOARD_HISTORY_FOOTER,
clipboard_history_util::GetShortcutKeyName())))),
IsNull()));
}
} // namespace ash