blob: 1a86c3b8e99f53b46ac31067b8b8e8431d2b2e80 [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 "ui/actions/actions.h"
#include <optional>
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/class_property.h"
namespace actions {
enum class ContextValues {
kContextNone,
kContextKeyboard,
kContextMouse,
kContextTouch,
};
namespace {
const std::u16string kActionText = u"Test Action";
const std::u16string kChild1Text = u"Child Action 1";
const std::u16string kChild2Text = u"Child Action 2";
const std::u16string kActionAccessibleText = u"Accessible Action Text";
const std::u16string kActionTooltipText = u"Tooltip text";
#define TEST_ACTION_IDS \
E(kActionTest1, , kActionTestStart, TestActionIds) \
E(kActionTest2) \
E(kActionTest3) \
E(kActionTest4)
#include "ui/actions/action_id_macros.inc"
// clang-format off
enum TestActionIds : ActionId {
kActionTestStart = kActionsEnd,
TEST_ACTION_IDS
kActionTestEnd,
};
// clang-format on
#include "ui/actions/action_id_macros.inc"
DEFINE_UI_CLASS_PROPERTY_KEY(ContextValues,
kContextValueKey,
ContextValues::kContextNone)
class ActionManagerTest : public testing::Test {
public:
ActionManagerTest() { ActionManager::ResetForTesting(); }
ActionManagerTest(const ActionManagerTest&) = delete;
ActionManagerTest& operator=(const ActionManagerTest&) = delete;
~ActionManagerTest() override { ActionManager::ResetForTesting(); }
protected:
void InitializeActions(ActionManager* manager) {
// clang-format off
manager->AddAction(ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(false)
.AddChildren(
ActionItem::Builder()
.SetActionId(kActionTest2)
.SetText(kChild1Text),
ActionItem::Builder()
.SetActionId(kActionTest3)
.SetText(kChild2Text)).Build());
// clang-format on
}
void SetupInitializer() {
auto& manager = ActionManager::GetForTesting();
initialization_subscription_ =
manager.AppendActionItemInitializer(base::BindRepeating(
&ActionManagerTest::InitializeActions, base::Unretained(this)));
}
void TearDown() override { ActionIdMap::ResetMapsForTesting(); }
private:
base::CallbackListSubscription initialization_subscription_;
};
using ActionItemTest = ActionManagerTest;
using ActionIdMapTest = ActionManagerTest;
} // namespace
} // namespace actions
DECLARE_EXPORTED_UI_CLASS_PROPERTY_TYPE(, actions::ContextValues)
DEFINE_UI_CLASS_PROPERTY_TYPE(actions::ContextValues)
namespace actions {
// Verifies that the test harness functions correctly.
TEST_F(ActionManagerTest, Harness) {
auto* manager = &ActionManager::GetForTesting();
ActionManager::ResetForTesting();
auto* new_manager = &ActionManager::GetForTesting();
EXPECT_EQ(manager, new_manager);
}
// Verifies that the Initializers are properly called.
TEST_F(ActionManagerTest, InitializerTest) {
bool initializer_called = false;
auto& manager = ActionManager::GetForTesting();
auto subscription = manager.AppendActionItemInitializer(base::BindRepeating(
[](bool* called, ActionManager* manager) { *called = true; },
&initializer_called));
EXPECT_FALSE(initializer_called);
manager.IndexActions();
EXPECT_TRUE(initializer_called);
}
TEST_F(ActionManagerTest, ActionRegisterAndInvoke) {
const std::u16string text = u"Test Action";
int action_invoked_count = 0;
auto& manager = ActionManager::GetForTesting();
auto subscription = manager.AppendActionItemInitializer(base::BindRepeating(
[](int* invoked_count, const std::u16string& text,
ActionManager* manager) {
auto action = std::make_unique<ActionItem>(base::BindRepeating(
[](int* invoked_count, actions::ActionItem* action,
ActionInvocationContext context) {
++*invoked_count;
EXPECT_EQ(*invoked_count, action->GetInvokeCount());
EXPECT_GE(base::TimeTicks::Now(), *action->GetLastInvokeTime());
},
invoked_count));
action->SetActionId(kActionCut);
action->SetText(text);
action->SetEnabled(true);
action->SetVisible(true);
manager->AddAction(std::move(action));
},
&action_invoked_count, text));
EXPECT_EQ(action_invoked_count, 0);
auto* action = manager.FindAction(kActionCut);
ASSERT_TRUE(action);
EXPECT_EQ(action->GetText(), text);
EXPECT_TRUE(action->GetEnabled());
EXPECT_TRUE(action->GetVisible());
EXPECT_EQ(action->GetInvokeCount(), 0);
EXPECT_EQ(action->GetLastInvokeTime(), std::nullopt);
auto action_id = action->GetActionId();
ASSERT_TRUE(action_id);
EXPECT_EQ(*action_id, kActionCut);
action->InvokeAction();
EXPECT_GT(action_invoked_count, 0);
}
TEST_F(ActionManagerTest, ActionNotFound) {
auto& manager = ActionManager::GetForTesting();
auto* action = manager.FindAction(kActionPaste);
EXPECT_FALSE(action);
}
TEST_F(ActionItemTest, ScopedFindActionTest) {
// clang-format off
auto builder = ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(false)
.AddChildren(
ActionItem::Builder()
.SetActionId(kActionTest2)
.SetText(kChild1Text),
ActionItem::Builder()
.SetActionId(kActionTest3)
.SetChecked(true)
.SetText(kChild2Text));
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
auto* action_test1 = manager.FindAction(kActionTest1);
ASSERT_TRUE(action_test1);
auto* action_test2 = manager.FindAction(kActionTest2, action_test1);
ASSERT_TRUE(action_test2);
auto* action_test3 = manager.FindAction(kActionTest3, action_test2);
EXPECT_FALSE(action_test3);
}
TEST_F(ActionIdMapTest, TestCreateActionId) {
const std::string new_action_id_1 = "kNewActionId1";
const std::string new_action_id_2 = "kNewActionId2";
const std::string existing_action_id = "kActionPaste";
auto result_1 = ActionIdMap::CreateActionId(new_action_id_1);
EXPECT_TRUE(result_1.second);
auto result_2 = ActionIdMap::CreateActionId(new_action_id_2);
EXPECT_TRUE(result_2.second);
EXPECT_NE(result_1.first, result_2.first);
auto result_2_dupe = ActionIdMap::CreateActionId(new_action_id_2);
EXPECT_FALSE(result_2_dupe.second);
EXPECT_EQ(result_2.first, result_2_dupe.first);
auto result_existing = ActionIdMap::CreateActionId(existing_action_id);
EXPECT_FALSE(result_existing.second);
}
TEST_F(ActionIdMapTest, MapBetweenEnumAndString) {
auto result_1 = ActionIdMap::CreateActionId("kNewActionId1");
EXPECT_TRUE(result_1.second);
const std::string expected_action_string = "kActionPaste";
auto actual_action_string = ActionIdMap::ActionIdToString(kActionPaste);
ASSERT_THAT(actual_action_string, testing::Optional(expected_action_string));
// Map back from enum to string
auto actual_action_id =
ActionIdMap::StringToActionId(actual_action_string.value());
EXPECT_THAT(actual_action_id, testing::Optional(kActionPaste));
const std::vector<std::string> strings{"kActionPaste", "kActionCut"};
const std::vector<ActionId> action_ids{kActionPaste, kActionCut};
auto actual_strings = ActionIdMap::ActionIdsToStrings(action_ids);
ASSERT_EQ(strings.size(), actual_strings.size());
EXPECT_THAT(actual_strings[0], testing::Optional(strings[0]));
EXPECT_THAT(actual_strings[1], testing::Optional(strings[1]));
auto actual_action_ids = ActionIdMap::StringsToActionIds(strings);
ASSERT_EQ(action_ids.size(), actual_action_ids.size());
EXPECT_THAT(actual_action_ids[0], testing::Optional(action_ids[0]));
EXPECT_THAT(actual_action_ids[1], testing::Optional(action_ids[1]));
}
#define MAP_ACTION_IDS_TO_STRINGS
#include "ui/actions/action_id_macros.inc"
TEST_F(ActionIdMapTest, MergeMaps) {
auto test_action_map = base::flat_map<ActionId, std::string>(
std::vector<std::pair<ActionId, std::string>>{TEST_ACTION_IDS});
ActionIdMap::AddActionIdToStringMappings(test_action_map);
const std::string expected_action_string = "kActionPaste";
auto actual_action_string = ActionIdMap::ActionIdToString(kActionPaste);
EXPECT_THAT(actual_action_string, testing::Optional(expected_action_string));
const std::string expected_string = "kActionTest2";
auto actual_string = ActionIdMap::ActionIdToString(kActionTest2);
EXPECT_THAT(actual_string, testing::Optional(expected_string));
}
#include "ui/actions/action_id_macros.inc"
#undef MAP_ACTION_IDS_TO_STRINGS
TEST_F(ActionIdMapTest, TestEnumNotFound) {
const std::string unknown_action = "kActionUnknown";
auto unknown_id = ActionIdMap::StringToActionId(unknown_action);
EXPECT_FALSE(unknown_id.has_value());
const ActionId invalid_action_id = static_cast<ActionId>(-1);
auto unknown_id_string = ActionIdMap::ActionIdToString(invalid_action_id);
EXPECT_FALSE(unknown_id_string.has_value());
}
TEST_F(ActionItemTest, ActionBuilderTest) {
const std::u16string text = u"Test Action";
// clang-format off
auto builder =
ActionItem::Builder()
.SetText(text)
.SetVisible(false)
.SetActionId(actions::kActionCopy)
.SetInvokeActionCallback(
base::DoNothing());
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
auto* action = manager.FindAction(actions::kActionCopy);
ASSERT_TRUE(action);
EXPECT_EQ(action->GetText(), text);
EXPECT_FALSE(action->GetVisible());
EXPECT_EQ(action->GetInvokeCount(), 0);
action->InvokeAction();
EXPECT_EQ(action->GetInvokeCount(), 1);
}
TEST_F(ActionItemTest, ActionBuilderChildrenTest) {
const size_t expected_child_count = 2;
ActionItem* root_action = nullptr;
ActionItem* child_action1 = nullptr;
ActionItem* child_action2 = nullptr;
int action_invoked_count = 0;
// clang-format off
auto builder = ActionItem::Builder(
base::BindRepeating([](int* invoked_count, actions::ActionItem* action,
ActionInvocationContext context){
++*invoked_count;
}, &action_invoked_count))
.CopyAddressTo(&root_action)
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(false)
.AddChildren(
ActionItem::Builder(base::DoNothing())
.CopyAddressTo(&child_action1)
.SetActionId(kActionTest2)
.SetText(kChild1Text),
ActionItem::Builder(base::DoNothing())
.CopyAddressTo(&child_action2)
.SetActionId(kActionTest3)
.SetChecked(true)
.SetIsShowingBubble(true)
.SetText(kChild2Text));
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
ASSERT_TRUE(root_action);
ASSERT_TRUE(child_action1);
ASSERT_TRUE(child_action2);
EXPECT_EQ(root_action->GetChildren().children().size(), expected_child_count);
EXPECT_EQ(child_action1->GetText(), kChild1Text);
auto child_action_id1 = child_action1->GetActionId();
ASSERT_TRUE(child_action_id1);
EXPECT_EQ(child_action_id1.value(), kActionTest2);
EXPECT_FALSE(child_action1->GetChecked());
EXPECT_FALSE(child_action1->GetIsShowingBubble());
EXPECT_EQ(child_action2->GetText(), kChild2Text);
auto child_action_id2 = child_action2->GetActionId();
ASSERT_TRUE(child_action_id2);
EXPECT_EQ(child_action_id2.value(), kActionTest3);
EXPECT_TRUE(child_action2->GetChecked());
EXPECT_TRUE(child_action2->GetIsShowingBubble());
EXPECT_FALSE(root_action->GetEnabled());
EXPECT_EQ(action_invoked_count, 0);
root_action->InvokeAction();
// `root_action` is not enabled, so InvokeAction() shouldn't trigger callback.
EXPECT_EQ(action_invoked_count, 0);
// The child actions should trigger their callbacks since they're enabled.
child_action1->InvokeAction();
EXPECT_EQ(child_action1->GetInvokeCount(), 1);
child_action2->InvokeAction();
EXPECT_EQ(child_action2->GetInvokeCount(), 1);
}
TEST_F(ActionItemTest, TestGetChildren) {
ActionItemVector actions;
auto& manager = ActionManager::GetForTesting();
SetupInitializer();
manager.GetActions(actions);
EXPECT_FALSE(actions.empty());
EXPECT_EQ(actions.size(), size_t{3});
}
TEST_F(ActionItemTest, TestItemBatchUpdate) {
bool action_item_changed = false;
ActionItem* root_action = nullptr;
// clang-format off
auto builder = ActionItem::Builder()
.CopyAddressTo(&root_action)
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(false)
.AddChildren(
ActionItem::Builder()
.SetActionId(kActionTest2)
.SetText(kChild1Text),
ActionItem::Builder()
.SetActionId(kActionTest3)
.SetChecked(true)
.SetText(kChild2Text));
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
auto changed_subscription =
root_action->AddActionChangedCallback(base::BindRepeating(
[](bool* action_item_changed) { *action_item_changed = true; },
&action_item_changed));
{
auto scoped_updater = root_action->BeginUpdate();
root_action->SetEnabled(true);
EXPECT_FALSE(action_item_changed);
root_action->SetVisible(false);
EXPECT_FALSE(action_item_changed);
}
EXPECT_TRUE(action_item_changed);
}
TEST_F(ActionItemTest, TestGroupIdExclusion) {
ActionItem* action_test2 = nullptr;
ActionItem* action_test3 = nullptr;
// clang-format off
auto builder = ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(false)
.AddChildren(
ActionItem::Builder()
.CopyAddressTo(&action_test2)
.SetActionId(kActionTest2)
.SetGroupId(10)
.SetText(kChild1Text),
ActionItem::Builder()
.CopyAddressTo(&action_test3)
.SetActionId(kActionTest3)
.SetGroupId(10)
.SetChecked(true)
.SetText(kChild2Text));
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddActions(std::move(builder).Build(),
ActionItem::Builder().SetActionId(kActionTest4).Build());
ASSERT_TRUE(action_test2);
ASSERT_TRUE(action_test2);
EXPECT_FALSE(action_test2->GetChecked());
EXPECT_TRUE(action_test3->GetChecked());
action_test2->SetChecked(true);
EXPECT_TRUE(action_test2->GetChecked());
EXPECT_FALSE(action_test3->GetChecked());
}
TEST_F(ActionItemTest, TestActionItemPinnableKey) {
// clang-format off
auto builder = ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(true);
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
auto* action_test1 = manager.FindAction(kActionTest1);
ASSERT_TRUE(action_test1);
ASSERT_EQ(action_test1->GetProperty(kActionItemPinnableKey),
std::underlying_type_t<actions::ActionPinnableState>(
actions::ActionPinnableState::kNotPinnable));
action_test1->SetProperty(kActionItemPinnableKey, true);
ASSERT_EQ(action_test1->GetProperty(kActionItemPinnableKey),
std::underlying_type_t<actions::ActionPinnableState>(
actions::ActionPinnableState::kPinnable));
// test using builder
builder =
ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest2)
.SetProperty(kActionItemPinnableKey,
std::underlying_type_t<actions::ActionPinnableState>(
actions::ActionPinnableState::kPinnable))
.SetVisible(true)
.SetEnabled(true);
manager.AddAction(std::move(builder).Build());
auto* action_test2 = manager.FindAction(kActionTest2);
ASSERT_TRUE(action_test2);
ASSERT_EQ(action_test2->GetProperty(kActionItemPinnableKey),
std::underlying_type_t<actions::ActionPinnableState>(
actions::ActionPinnableState::kPinnable));
}
TEST_F(ActionItemTest, TestActionProperties) {
constexpr int kGroupId = 5;
// clang-format off
auto builder = ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(true)
.SetAccessibleName(kActionAccessibleText)
.SetTooltipText(kActionTooltipText)
.SetGroupId(kGroupId);
// clang-format on
auto action_item = std::move(builder).Build();
EXPECT_EQ(action_item->GetText(), kActionText);
EXPECT_EQ(action_item->GetActionId().value(), kActionTest1);
EXPECT_TRUE(action_item->GetVisible());
EXPECT_TRUE(action_item->GetEnabled());
EXPECT_EQ(action_item->GetAccessibleName(), kActionAccessibleText);
EXPECT_EQ(action_item->GetTooltipText(), kActionTooltipText);
EXPECT_EQ(action_item->GetGroupId(), kGroupId);
}
TEST_F(ActionItemTest, TestActionWeakPtr) {
base::WeakPtr<ActionItem> action_test2;
base::WeakPtr<ActionItem> action_test3;
// clang-format off
auto builder = ActionItem::Builder()
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(false)
.AddChildren(
ActionItem::Builder()
.CopyWeakPtrTo(&action_test2)
.SetActionId(kActionTest2)
.SetGroupId(10)
.SetText(kChild1Text),
ActionItem::Builder()
.CopyWeakPtrTo(&action_test3)
.SetActionId(kActionTest3)
.SetGroupId(10)
.SetChecked(true)
.SetText(kChild2Text));
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
ASSERT_NE(action_test2.get(), nullptr);
ASSERT_NE(action_test3.get(), nullptr);
manager.ResetActions();
EXPECT_EQ(action_test2.get(), nullptr);
EXPECT_EQ(action_test3.get(), nullptr);
}
TEST_F(ActionItemTest, TextActionInvocationContext) {
int action_invoked_count = 0;
base::WeakPtr<ActionItem> action_test1;
// clang-format off
auto builder = ActionItem::Builder(
base::BindRepeating([](int* invoked_count, actions::ActionItem* action,
ActionInvocationContext context){
++*invoked_count;
ContextValues context_value = context.GetProperty(kContextValueKey);
EXPECT_EQ(context_value, ContextValues::kContextKeyboard);
}, &action_invoked_count))
.CopyWeakPtrTo(&action_test1)
.SetText(kActionText)
.SetActionId(kActionTest1)
.SetVisible(true)
.SetEnabled(true);
// clang-format on
auto& manager = ActionManager::GetForTesting();
manager.AddAction(std::move(builder).Build());
ASSERT_TRUE(action_test1);
action_test1->InvokeAction(
ActionInvocationContext::Builder()
.SetProperty(kContextValueKey, ContextValues::kContextKeyboard)
.Build());
EXPECT_EQ(action_invoked_count, 1);
}
} // namespace actions