blob: 2676b41533147044c65aef538200a890f82adb9c [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 "chrome/browser/ui/views/toolbar/toolbar_controller.h"
#include <gtest/gtest.h>
#include <memory>
#include <variant>
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/toolbar/overflow_button.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_button_status_indicator.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "components/vector_icons/vector_icons.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/actions/actions.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/size.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_utils.h"
namespace {
// Toolbar button size is ~34dp.
constexpr gfx::Size kButtonSize(34, 34);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyButton1);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyButton2);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyButton3);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyButton4);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyObservedView);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyActivateView);
class TestDelegate : public ToolbarController::PinnedActionsDelegate {
public:
MOCK_METHOD(void,
DummyAction,
(actions::ActionItem*, actions::ActionInvocationContext));
TestDelegate() {
for (const auto& id : action_ids_) {
action_items_.push_back(
actions::ActionItem::ActionItemBuilder(
base::BindRepeating(&TestDelegate::DummyAction,
base::Unretained(this)))
.SetActionId(id)
.SetImage(
ui::ImageModel::FromVectorIcon(vector_icons::kDogfoodIcon))
.SetProperty(kActionItemUnderlineIndicatorKey, true)
.SetText(
base::StrCat({u"DummyAction", base::NumberToString16(id)}))
.Build());
if (id == 0) {
kIdToOverflowedMap_[id] = false;
} else {
kIdToOverflowedMap_[id] = true;
++overflowed_count_;
}
kIdToItemMap_[id] = action_items_[id].get();
}
}
~TestDelegate() override = default;
actions::ActionItem* GetActionItemFor(actions::ActionId id) override {
return kIdToItemMap_.at(id);
}
bool IsOverflowed(actions::ActionId id) override {
return kIdToOverflowedMap_.at(id);
}
views::View* GetContainerView() override { return container_view_; }
void SetContainerView(views::View* view) { container_view_ = view; }
bool ShouldAnyButtonsOverflow(gfx::Size available_size) const override {
return true;
}
int get_overflowed_count() { return overflowed_count_; }
std::vector<actions::ActionId> get_action_ids() { return action_ids_; }
const std::vector<actions::ActionId>& PinnedActionIds() const override {
return action_ids_;
}
private:
int overflowed_count_ = 0;
std::vector<actions::ActionId> action_ids_ = {0, 1, 2};
std::vector<std::unique_ptr<actions::ActionItem>> action_items_;
base::flat_map<actions::ActionId,
raw_ptr<actions::ActionItem, CtnExperimental>>
kIdToItemMap_;
base::flat_map<actions::ActionId, bool> kIdToOverflowedMap_;
raw_ptr<views::View> container_view_;
};
class TestDelegateFromModel : public ToolbarController::PinnedActionsDelegate {
public:
MOCK_METHOD(void,
DummyAction,
(actions::ActionItem*, actions::ActionInvocationContext));
explicit TestDelegateFromModel(PinnedToolbarActionsModel* model) {
model_ = model;
}
~TestDelegateFromModel() override = default;
actions::ActionItem* GetActionItemFor(actions::ActionId id) override {
for (const auto& action_item : action_items_) {
if (action_item->GetActionId() == id) {
return action_item.get();
}
}
action_items_.push_back(
actions::ActionItem::ActionItemBuilder(
base::BindRepeating(&TestDelegateFromModel::DummyAction,
base::Unretained(this)))
.SetActionId(id)
.SetImage(
ui::ImageModel::FromVectorIcon(vector_icons::kDogfoodIcon))
.SetProperty(kActionItemUnderlineIndicatorKey, true)
.SetText(base::StrCat({u"DummyAction", base::NumberToString16(id)}))
.Build());
return action_items_.back().get();
}
bool IsOverflowed(actions::ActionId id) override { return false; }
views::View* GetContainerView() override { return &container_view_; }
bool ShouldAnyButtonsOverflow(gfx::Size available_size) const override {
return false;
}
const std::vector<actions::ActionId>& PinnedActionIds() const override {
return model_->PinnedActionIds();
}
private:
raw_ptr<PinnedToolbarActionsModel> model_;
views::View container_view_;
std::vector<std::unique_ptr<actions::ActionItem>> action_items_;
};
class MockToolbarController : public ToolbarController {
public:
MockToolbarController(
const std::vector<ToolbarController::ResponsiveElementInfo>&
responsive_elements,
const std::vector<ui::ElementIdentifier>& elements_in_overflow_order,
int element_flex_order_start,
views::View* toolbar_container_view,
OverflowButton* overflow_button,
TestDelegate* delegate,
TestingProfile* profile)
: ToolbarController(responsive_elements,
elements_in_overflow_order,
element_flex_order_start,
toolbar_container_view,
overflow_button,
delegate,
PinnedToolbarActionsModel::Get(profile)) {}
MOCK_METHOD(bool, PopOut, (ui::ElementIdentifier identifier), (override));
MOCK_METHOD(bool, EndPopOut, (ui::ElementIdentifier identifier), (override));
};
class PopOutHandlerTest : public ChromeViewsTestBase {
public:
PopOutHandlerTest() = default;
PopOutHandlerTest(const PopOutHandlerTest&) = delete;
PopOutHandlerTest& operator=(const PopOutHandlerTest&) = delete;
~PopOutHandlerTest() override = default;
protected:
views::Widget* widget() { return widget_.get(); }
views::View* container_view() { return container_view_; }
OverflowButton* overflow_button() { return overflow_button_; }
private:
std::unique_ptr<views::Widget> widget_;
raw_ptr<views::View, DanglingUntriaged> container_view_;
raw_ptr<OverflowButton, DanglingUntriaged> overflow_button_;
void SetUp() override {
ChromeViewsTestBase::SetUp();
widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.bounds = gfx::Rect(0, 0, 650, 650);
widget_->Init(std::move(params));
widget_->Show();
std::unique_ptr<views::View> toolbar_container_view =
std::make_unique<views::View>();
toolbar_container_view
->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
.SetDefault(views::kFlexBehaviorKey,
views::FlexSpecification(
views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kPreferredSnapToZero,
views::MaximumFlexSizeRule::kPreferred));
container_view_ =
widget_->SetContentsView(std::move(toolbar_container_view));
auto overflow_button = std::make_unique<OverflowButton>();
overflow_button_ =
container_view_->AddChildView(std::move(overflow_button));
}
void TearDown() override {
widget_.reset();
ChromeViewsTestBase::TearDown();
}
};
TEST_F(PopOutHandlerTest, PopOutAndEndPopOut) {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kDummyButton);
auto test_delegate = std::make_unique<TestDelegate>();
auto test_profile = std::make_unique<TestingProfile>();
MockToolbarController toolbar_controller(
std::vector<ToolbarController::ResponsiveElementInfo>{
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(
kDummyButton, 0, &vector_icons::kErrorIcon,
kDummyActivateView, kDummyObservedView),
false)},
std::vector<ui::ElementIdentifier>({kDummyButton}), 1, container_view(),
overflow_button(), test_delegate.get(), test_profile.get());
overflow_button()->set_toolbar_controller(&toolbar_controller);
ui::ElementContext context =
views::ElementTrackerViews::GetContextForWidget(widget());
ToolbarController::PopOutHandler pop_out_controller(
&toolbar_controller, context, kDummyButton, kDummyObservedView);
EXPECT_CALL(toolbar_controller, PopOut(kDummyButton));
auto observed_view = std::make_unique<views::View>();
observed_view->SetProperty(views::kElementIdentifierKey, kDummyObservedView);
views::View* view = container_view()->AddChildView(std::move(observed_view));
EXPECT_CALL(toolbar_controller, EndPopOut(kDummyButton));
container_view()->RemoveChildViewT<views::View>(view);
}
constexpr int kElementFlexOrderStart = 1;
} // namespace
class TestToolbarController : public ToolbarController {
public:
TestToolbarController(
const std::vector<ToolbarController::ResponsiveElementInfo>&
responsive_elements,
const std::vector<ui::ElementIdentifier>& elements_in_overflow_order,
int element_flex_order_start,
views::View* toolbar_container_view,
OverflowButton* overflow_button,
TestDelegate* delegate,
PinnedToolbarActionsModel* model)
: ToolbarController(responsive_elements,
elements_in_overflow_order,
element_flex_order_start,
toolbar_container_view,
overflow_button,
delegate,
model) {}
std::u16string GetMenuText(const ToolbarController::ResponsiveElementInfo&
element_info) const override {
static const base::flat_map<ui::ElementIdentifier, std::u16string>
kToolbarToMenuTextMap = {{kDummyButton1, u"DummyButton1"},
{kDummyButton2, u"DummyButton2"},
{kDummyButton3, u"DummyButton3"},
{kDummyButton4, u"DummyButton4"}};
return kToolbarToMenuTextMap.at(
std::get<ToolbarController::ElementIdInfo>(element_info.overflow_id)
.overflow_identifier);
}
};
class ToolbarControllerUnitTest : public ChromeViewsTestBase {
public:
ToolbarControllerUnitTest() = default;
ToolbarControllerUnitTest(const ToolbarControllerUnitTest&) = delete;
ToolbarControllerUnitTest& operator=(const ToolbarControllerUnitTest&) =
delete;
~ToolbarControllerUnitTest() override = default;
void SetUp() override {
ChromeViewsTestBase::SetUp();
widget_ =
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
widget_->Show();
std::unique_ptr<views::View> toolbar_container_view =
std::make_unique<views::View>();
toolbar_container_view
->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
.SetDefault(views::kFlexBehaviorKey,
views::FlexSpecification(
views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kPreferredSnapToZero,
views::MaximumFlexSizeRule::kPreferred));
toolbar_container_view_ =
widget_->SetContentsView(std::move(toolbar_container_view));
std::vector<ui::ElementIdentifier> element_ids = {
kDummyButton1, kDummyButton2, kDummyButton3};
InitToolbarContainerViewWithTestButtons(element_ids);
auto overflow_button = std::make_unique<OverflowButton>();
overflow_button_ =
toolbar_container_view_->AddChildView(std::move(overflow_button));
overflow_button_->SetVisible(false);
test_delegate_ = std::make_unique<TestDelegate>();
testing_profile_ = std::make_unique<TestingProfile>();
toolbar_controller_ = std::make_unique<TestToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>{
{ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(
kDummyButton1, 0, &vector_icons::kErrorIcon,
kDummyActivateView, kDummyObservedView),
false),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(
kDummyButton2, 0, &vector_icons::kErrorIcon,
kDummyActivateView, kDummyObservedView),
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(
kDummyButton3, 0, &vector_icons::kErrorIcon,
kDummyActivateView, kDummyObservedView),
true)}},
std::vector<ui::ElementIdentifier>(
{kDummyButton3, kDummyButton2, kDummyButton1}),
kElementFlexOrderStart, toolbar_container_view_, overflow_button_,
test_delegate_.get(),
PinnedToolbarActionsModel::Get(testing_profile_.get()));
overflow_button_->set_toolbar_controller(toolbar_controller_.get());
event_generator_ = std::make_unique<ui::test::EventGenerator>(
views::GetRootWindow(widget_.get()));
}
// Add test buttons with `ids` to `toolbar_container_view_`.
void InitToolbarContainerViewWithTestButtons(
std::vector<ui::ElementIdentifier> ids) {
for (size_t i = 0; i < ids.size(); ++i) {
auto button = std::make_unique<views::View>();
button->SetProperty(views::kElementIdentifierKey, ids[i]);
button->SetPreferredSize(kButtonSize);
button->GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
button->GetViewAccessibility().SetName(
base::StrCat({u"DummyButton", base::NumberToString16(i)}));
button->SetVisible(true);
test_buttons_.emplace_back(
toolbar_container_view_->AddChildView(std::move(button)));
}
}
void TearDown() override {
overflow_button_->set_toolbar_controller(nullptr);
overflow_button_ = nullptr;
toolbar_container_view_ = nullptr;
event_generator_.reset();
toolbar_controller_.reset();
widget_.reset();
test_delegate_.reset();
testing_profile_.reset();
ChromeViewsTestBase::TearDown();
}
void UpdateOverflowButtonVisibility() {
toolbar_controller()->overflow_button()->SetVisible(
toolbar_controller()->ShouldShowOverflowButton(widget()->GetSize()));
}
views::Widget* widget() { return widget_.get(); }
ToolbarController* toolbar_controller() { return toolbar_controller_.get(); }
ui::test::EventGenerator* event_generator() { return event_generator_.get(); }
views::View* toolbar_container_view() { return toolbar_container_view_; }
OverflowButton* overflow_button() { return overflow_button_; }
const std::vector<raw_ptr<views::View, VectorExperimental>>& test_buttons() {
return test_buttons_;
}
const ui::SimpleMenuModel* overflow_menu() {
return toolbar_controller_->menu_model_for_testing();
}
std::vector<const ToolbarController::ResponsiveElementInfo*>
GetOverflowedElements() {
return toolbar_controller()->GetOverflowedElements();
}
const std::vector<ToolbarController::ResponsiveElementInfo>&
GetResponsiveElements(const ToolbarController* toolbar_controller) {
return toolbar_controller->responsive_elements_;
}
bool IsOverflowed(const ToolbarController::ResponsiveElementInfo& element) {
return toolbar_controller_->IsOverflowed(element);
}
std::vector<ToolbarController::ResponsiveElementInfo>
GetResponsiveElementsWithOrderedActions(
const ToolbarController* toolbar_controller) const {
return toolbar_controller->GetResponsiveElementsWithOrderedActions();
}
PinnedToolbarActionsModel* GetPinnedToolbarActionsModel() {
return PinnedToolbarActionsModel::Get(testing_profile_.get());
}
private:
std::unique_ptr<views::Widget> widget_;
std::unique_ptr<ToolbarController> toolbar_controller_;
std::unique_ptr<ui::test::EventGenerator> event_generator_;
raw_ptr<views::View> toolbar_container_view_;
raw_ptr<OverflowButton> overflow_button_;
std::unique_ptr<TestDelegate> test_delegate_;
std::unique_ptr<TestingProfile> testing_profile_;
// Buttons being tested.
std::vector<raw_ptr<views::View, VectorExperimental>> test_buttons_;
};
TEST_F(ToolbarControllerUnitTest, OverflowButtonVisibility) {
// Initialize widget width with total width of all test buttons.
// Should not see overflowed buttons.
widget()->SetSize(gfx::Size(kButtonSize.width() * test_buttons().size(),
kButtonSize.height()));
EXPECT_EQ(GetOverflowedElements().size(), size_t(0));
UpdateOverflowButtonVisibility();
EXPECT_FALSE(overflow_button()->GetVisible());
// Shrink widget width with one button width smaller.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
EXPECT_EQ(GetOverflowedElements().size(), size_t(1));
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
}
TEST_F(ToolbarControllerUnitTest, OverflowedButtonsMatchMenu) {
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
widget()->LayoutRootViewIfNecessary();
event_generator()->MoveMouseTo(
overflow_button()->GetBoundsInScreen().CenterPoint());
event_generator()->PressLeftButton();
const ui::SimpleMenuModel* menu = overflow_menu();
const auto overflowed_buttons = GetOverflowedElements();
// Overflowed buttons should match overflow menu.
EXPECT_TRUE(menu);
const auto& responsive_elements = GetResponsiveElements(toolbar_controller());
for (size_t i = 0; i < responsive_elements.size(); ++i) {
if (IsOverflowed(responsive_elements[i])) {
EXPECT_EQ(toolbar_controller()->GetMenuText(responsive_elements[i]),
menu->GetLabelAt(menu->GetIndexOfCommandId(i).value()));
}
}
}
TEST_F(ToolbarControllerUnitTest, RunningMenuAddsStatusIndicator) {
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
widget()->LayoutRootViewIfNecessary();
event_generator()->MoveMouseTo(
overflow_button()->GetBoundsInScreen().CenterPoint());
event_generator()->PressLeftButton();
const ui::SimpleMenuModel* menu = overflow_menu();
// Overflowed buttons should match overflow menu.
EXPECT_TRUE(menu);
views::SubmenuView* sub_menu =
toolbar_controller()->root_menu_item()->GetSubmenu();
for (auto* menu_item : sub_menu->GetMenuItems()) {
PinnedToolbarButtonStatusIndicator* status_indicator = nullptr;
for (auto& child : menu_item->icon_view()->children()) {
if (views::AsViewClass<PinnedToolbarButtonStatusIndicator>(child)) {
status_indicator =
views::AsViewClass<PinnedToolbarButtonStatusIndicator>(child);
}
EXPECT_TRUE(status_indicator);
}
}
}
TEST_F(ToolbarControllerUnitTest, MenuSeparator) {
// Set widget to be small enough to ensure all the buttons overflow.
widget()->SetSize(gfx::Size(1, 1));
UpdateOverflowButtonVisibility();
// All 3 buttons overflowed.
EXPECT_EQ(GetOverflowedElements().size(), static_cast<size_t>(3));
const auto menu = toolbar_controller()->CreateOverflowMenuModel();
EXPECT_TRUE(menu);
// There is no separator between button1 and 2 because button1 is not a menu
// section end.
// There is a separator between button2 and button3 because
// 1) button2 is a section end;
// 2) the section button2 is in is valid;
// 3) the section button3 is in is valid.
// There is no separator after button3 because there is no valid next section.
EXPECT_EQ(menu->GetItemCount(), static_cast<size_t>(4));
EXPECT_EQ(menu->GetTypeAt(0), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(0), u"DummyButton1");
EXPECT_EQ(menu->GetTypeAt(1), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(1), u"DummyButton2");
EXPECT_EQ(menu->GetTypeAt(2), ui::MenuModel::ItemType::TYPE_SEPARATOR);
EXPECT_EQ(menu->GetTypeAt(3), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(3), u"DummyButton3");
}
TEST_F(ToolbarControllerUnitTest, InValidFirstSectionAddsNoLeadingSeparator) {
auto test_delegate = std::make_unique<TestDelegate>();
std::unique_ptr<ToolbarController> test_controller =
std::make_unique<TestToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>(
{ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton1, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton2, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton3, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true)}),
std::vector<ui::ElementIdentifier>(
{kDummyButton3, kDummyButton2, kDummyButton1}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), test_delegate.get(),
GetPinnedToolbarActionsModel());
widget()->SetSize(kButtonSize);
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
views::View* button1 = test_buttons()[0];
views::View* button2 = test_buttons()[1];
views::View* button3 = test_buttons()[2];
// Button2 and 3 overflowed.
EXPECT_TRUE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
EXPECT_EQ(GetOverflowedElements().size(), static_cast<size_t>(2));
const auto menu = test_controller->CreateOverflowMenuModel();
EXPECT_TRUE(menu);
// The first section (contains Button1) is invalid. It should not add a
// separator before Button2.
EXPECT_EQ(menu->GetItemCount(), static_cast<size_t>(3));
EXPECT_EQ(menu->GetTypeAt(0), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(0), u"DummyButton2");
EXPECT_EQ(menu->GetTypeAt(1), ui::MenuModel::ItemType::TYPE_SEPARATOR);
EXPECT_EQ(menu->GetTypeAt(2), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(2), u"DummyButton3");
}
TEST_F(ToolbarControllerUnitTest, InValidSectionInMiddleAddsNoExtraSeparator) {
auto test_delegate = std::make_unique<TestDelegate>();
std::unique_ptr<ToolbarController> test_controller =
std::make_unique<TestToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>(
{ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton1, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton2, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton3, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true)}),
std::vector<ui::ElementIdentifier>(
{kDummyButton1, kDummyButton3, kDummyButton2}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), test_delegate.get(),
GetPinnedToolbarActionsModel());
widget()->SetSize(kButtonSize);
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
views::View* button1 = test_buttons()[0];
views::View* button2 = test_buttons()[1];
views::View* button3 = test_buttons()[2];
// Button1 and 3 overflowed.
EXPECT_FALSE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
EXPECT_EQ(GetOverflowedElements().size(), static_cast<size_t>(2));
const auto menu = test_controller->CreateOverflowMenuModel();
EXPECT_TRUE(menu);
// The second section (contains Button2) is invalid. It should not add a
// redundant separator.
EXPECT_EQ(menu->GetItemCount(), static_cast<size_t>(3));
EXPECT_EQ(menu->GetTypeAt(0), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(0), u"DummyButton1");
EXPECT_EQ(menu->GetTypeAt(1), ui::MenuModel::ItemType::TYPE_SEPARATOR);
EXPECT_EQ(menu->GetTypeAt(2), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(2), u"DummyButton3");
}
TEST_F(ToolbarControllerUnitTest, InValidLastSectionAddsNoTrailingSeparator) {
auto test_delegate = std::make_unique<TestDelegate>();
std::unique_ptr<ToolbarController> test_controller =
std::make_unique<TestToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>(
{ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton1, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton2, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton3, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
true)}),
std::vector<ui::ElementIdentifier>(
{kDummyButton1, kDummyButton2, kDummyButton3}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), test_delegate.get(),
GetPinnedToolbarActionsModel());
widget()->SetSize(kButtonSize);
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
views::View* button1 = test_buttons()[0];
views::View* button2 = test_buttons()[1];
views::View* button3 = test_buttons()[2];
// Button1 and 2 overflowed.
EXPECT_FALSE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
EXPECT_EQ(GetOverflowedElements().size(), static_cast<size_t>(2));
const auto menu = test_controller->CreateOverflowMenuModel();
EXPECT_TRUE(menu);
// The third section (contains Button3) is invalid. It should not add a
// redundant trailing separator.
EXPECT_EQ(menu->GetItemCount(), static_cast<size_t>(3));
EXPECT_EQ(menu->GetTypeAt(0), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(0), u"DummyButton1");
EXPECT_EQ(menu->GetTypeAt(1), ui::MenuModel::ItemType::TYPE_SEPARATOR);
EXPECT_EQ(menu->GetTypeAt(2), ui::MenuModel::ItemType::TYPE_COMMAND);
EXPECT_EQ(menu->GetLabelAt(2), u"DummyButton2");
}
TEST_F(ToolbarControllerUnitTest, PopOutState) {
auto& pop_out_state = toolbar_controller()->pop_out_state_for_testing();
EXPECT_FALSE(pop_out_state.at(kDummyButton1)->original_spec.has_value());
EXPECT_FALSE(pop_out_state.at(kDummyButton2)->original_spec.has_value());
EXPECT_FALSE(pop_out_state.at(kDummyButton3)->original_spec.has_value());
EXPECT_EQ(pop_out_state.at(kDummyButton1)->responsive_spec.order(),
kElementFlexOrderStart);
EXPECT_EQ(pop_out_state.at(kDummyButton2)->responsive_spec.order(),
kElementFlexOrderStart + 1);
EXPECT_EQ(pop_out_state.at(kDummyButton3)->responsive_spec.order(),
kElementFlexOrderStart + 2);
}
TEST_F(ToolbarControllerUnitTest, PopOutButton) {
views::View* button1 = test_buttons()[0];
views::View* button2 = test_buttons()[1];
views::View* button3 = test_buttons()[2];
// Enough space to accommodate 3 buttons.
widget()->SetSize(gfx::Size(kButtonSize.width() * test_buttons().size(),
kButtonSize.height()));
EXPECT_TRUE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
// Not enough space. Button3 is hidden.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
EXPECT_TRUE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
// Pop out button3. Button2 is hidden.
EXPECT_TRUE(toolbar_controller()->PopOut(kDummyButton3));
views::test::RunScheduledLayout(toolbar_container_view());
EXPECT_TRUE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
// Button3 is already popped out.
EXPECT_FALSE(toolbar_controller()->PopOut(kDummyButton3));
// End button3 pop out. Button3 is hidden again.
EXPECT_TRUE(toolbar_controller()->EndPopOut(kDummyButton3));
views::test::RunScheduledLayout(toolbar_container_view());
EXPECT_TRUE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
// Button3 already ended popped out.
EXPECT_FALSE(toolbar_controller()->EndPopOut(kDummyButton3));
// kDummyButton4 does not exist.
EXPECT_FALSE(toolbar_controller()->PopOut(kDummyButton4));
}
// Buttons overflow in order: 3, 2, 1.
TEST_F(ToolbarControllerUnitTest, ButtonsOverflowRightToLeftInContainer) {
views::View* button1 = test_buttons()[0];
views::View* button2 = test_buttons()[1];
views::View* button3 = test_buttons()[2];
// Enough space to accommodate 3 buttons.
widget()->SetSize(gfx::Size(kButtonSize.width() * test_buttons().size(),
kButtonSize.height()));
EXPECT_TRUE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
// Not enough space. Button3 is hidden.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
EXPECT_TRUE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
// Keep resizing smaller. Button2 is hidden.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 2),
kButtonSize.height()));
EXPECT_TRUE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
// Keep resizing smaller. Button1 is hidden.
widget()->SetSize(gfx::Size(1, kButtonSize.height()));
EXPECT_FALSE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
}
// Buttons overflow in order: 1, 2, 3.
TEST_F(ToolbarControllerUnitTest, ButtonsOverflowLeftToRightInContainer) {
auto test_delegate = std::make_unique<TestDelegate>();
std::unique_ptr<ToolbarController> dummy_controller =
std::make_unique<TestToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>(
{ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton1, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
false),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton2, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
false),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{kDummyButton3, 0,
&vector_icons::kErrorIcon,
kDummyActivateView},
false)}),
std::vector<ui::ElementIdentifier>(
{kDummyButton1, kDummyButton2, kDummyButton3}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), test_delegate.get(),
GetPinnedToolbarActionsModel());
views::View* button1 = test_buttons()[0];
views::View* button2 = test_buttons()[1];
views::View* button3 = test_buttons()[2];
// Enough space to accommodate 3 buttons.
widget()->SetSize(gfx::Size(kButtonSize.width() * test_buttons().size(),
kButtonSize.height()));
EXPECT_TRUE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
// Not enough space. Button1 is hidden.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
EXPECT_FALSE(button1->GetVisible());
EXPECT_TRUE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
// Keep resizing smaller. Button2 is hidden.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 2),
kButtonSize.height()));
EXPECT_FALSE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_TRUE(button3->GetVisible());
// Keep resizing smaller. Button3 is hidden.
widget()->SetSize(gfx::Size(1, kButtonSize.height()));
EXPECT_FALSE(button1->GetVisible());
EXPECT_FALSE(button2->GetVisible());
EXPECT_FALSE(button3->GetVisible());
}
TEST_F(ToolbarControllerUnitTest, MenuItemUsability) {
views::View* button3 = test_buttons()[2];
button3->SetEnabled(false);
// Not enough space. Button3 is hidden.
widget()->SetSize(gfx::Size(kButtonSize.width() * (test_buttons().size() - 1),
kButtonSize.height()));
UpdateOverflowButtonVisibility();
EXPECT_TRUE(overflow_button()->GetVisible());
EXPECT_FALSE(button3->GetVisible());
widget()->LayoutRootViewIfNecessary();
event_generator()->MoveMouseTo(
overflow_button()->GetBoundsInScreen().CenterPoint());
event_generator()->PressLeftButton();
const ui::SimpleMenuModel* menu = overflow_menu();
const auto overflowed_buttons = GetOverflowedElements();
EXPECT_TRUE(menu);
const auto& responsive_elements = GetResponsiveElements(toolbar_controller());
for (size_t i = 0; i < responsive_elements.size(); ++i) {
if (IsOverflowed(responsive_elements[i])) {
EXPECT_EQ(ToolbarController::FindToolbarElementWithId(
toolbar_container_view(),
std::get<ToolbarController::ElementIdInfo>(
responsive_elements[i].overflow_id)
.overflow_identifier)
->GetEnabled(),
menu->IsEnabledAt(menu->GetIndexOfCommandId(i).value()));
}
}
}
TEST_F(ToolbarControllerUnitTest, ResponsiveActionsAreOrdered) {
auto test_delegate = std::make_unique<TestDelegate>();
using ElementIdInfo = ToolbarController::ElementIdInfo;
using ResponsiveElementInfo = ToolbarController::ResponsiveElementInfo;
using ActionId = actions::ActionId;
ResponsiveElementInfo element0(
ElementIdInfo{kDummyButton1, 0, &vector_icons::kErrorIcon,
kDummyActivateView},
false);
ResponsiveElementInfo action0(test_delegate->get_action_ids()[0]);
ResponsiveElementInfo action1(test_delegate->get_action_ids()[1]);
ResponsiveElementInfo action2(test_delegate->get_action_ids()[2]);
auto test_controller = ToolbarController(
std::vector<ResponsiveElementInfo>(
{action2, action1, action0, element0, action2, action0}),
std::vector<ui::ElementIdentifier>({kDummyButton1}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), test_delegate.get(),
GetPinnedToolbarActionsModel());
std::vector<ToolbarController::ResponsiveElementInfo> elements =
GetResponsiveElementsWithOrderedActions(&test_controller);
EXPECT_EQ(int(elements.size()), 6);
// Both sections of actions are reordered
EXPECT_EQ(std::get<ActionId>(elements[0].overflow_id),
std::get<ActionId>(action0.overflow_id));
EXPECT_EQ(std::get<ActionId>(elements[1].overflow_id),
std::get<ActionId>(action1.overflow_id));
EXPECT_EQ(std::get<ActionId>(elements[2].overflow_id),
std::get<ActionId>(action2.overflow_id));
EXPECT_EQ(
std::get<ElementIdInfo>(elements[3].overflow_id).overflow_identifier,
std::get<ElementIdInfo>(element0.overflow_id).overflow_identifier);
EXPECT_EQ(std::get<ActionId>(elements[4].overflow_id),
std::get<ActionId>(action0.overflow_id));
EXPECT_EQ(std::get<ActionId>(elements[5].overflow_id),
std::get<ActionId>(action2.overflow_id));
}
TEST_F(ToolbarControllerUnitTest, ResponsiveActionsRemainOrdered) {
using ResponsiveElementInfo = ToolbarController::ResponsiveElementInfo;
using ActionId = actions::ActionId;
ResponsiveElementInfo action0(0);
ResponsiveElementInfo action1(1);
PinnedToolbarActionsModel* model = GetPinnedToolbarActionsModel();
model->UpdatePinnedState(std::get<ActionId>(action0.overflow_id), true);
model->UpdatePinnedState(std::get<ActionId>(action1.overflow_id), true);
auto delegate = std::make_unique<TestDelegateFromModel>(model);
// Create the controller with the ActionIds in the reversed order
// (action1, action0) with respect to delegate.PinnedActionIds().
// They should be sorted in responsive_elements right after the controller
// is instantiated.
auto controller = ToolbarController(
std::vector<ResponsiveElementInfo>({action1, action0}),
std::vector<ui::ElementIdentifier>({kDummyButton1}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), delegate.get(), model);
std::vector<ResponsiveElementInfo> elements =
GetResponsiveElements(&controller);
EXPECT_EQ(int(elements.size()), 2);
EXPECT_EQ(std::get<ActionId>(elements[0].overflow_id),
std::get<ActionId>(action0.overflow_id));
EXPECT_EQ(std::get<ActionId>(elements[1].overflow_id),
std::get<ActionId>(action1.overflow_id));
// Move action1 to the first index. responsive_elements should be reordered.
model->MovePinnedAction(std::get<ActionId>(action1.overflow_id), 0);
elements = GetResponsiveElements(&controller);
EXPECT_EQ(int(elements.size()), 2);
EXPECT_EQ(std::get<ActionId>(elements[0].overflow_id),
std::get<ActionId>(action1.overflow_id));
EXPECT_EQ(std::get<ActionId>(elements[1].overflow_id),
std::get<ActionId>(action0.overflow_id));
}
TEST_F(ToolbarControllerUnitTest, ResponsiveActionsAreNotOrdered) {
auto test_delegate = std::make_unique<TestDelegate>();
using ElementIdInfo = ToolbarController::ElementIdInfo;
using ResponsiveElementInfo = ToolbarController::ResponsiveElementInfo;
using ActionId = actions::ActionId;
ResponsiveElementInfo element0(
ElementIdInfo{kDummyButton1, 0, &vector_icons::kErrorIcon,
kDummyActivateView},
false);
ResponsiveElementInfo element1(
ElementIdInfo{kDummyButton2, 0, &vector_icons::kErrorIcon,
kDummyActivateView},
false);
ResponsiveElementInfo action0(test_delegate->get_action_ids()[0]);
ResponsiveElementInfo action1(test_delegate->get_action_ids()[1]);
ResponsiveElementInfo action2(test_delegate->get_action_ids()[2]);
auto test_controller = ToolbarController(
std::vector<ResponsiveElementInfo>(
{element1, element0, action2, element0, action0, element0, action1}),
std::vector<ui::ElementIdentifier>({kDummyButton1, kDummyButton2}),
kElementFlexOrderStart, toolbar_container_view(),
const_cast<OverflowButton*>(overflow_button()), test_delegate.get(),
GetPinnedToolbarActionsModel());
std::vector<ToolbarController::ResponsiveElementInfo> elements =
GetResponsiveElementsWithOrderedActions(&test_controller);
EXPECT_EQ(int(elements.size()), 7);
// Only sections of actions are reordered, so we
// expect the order not to change
EXPECT_EQ(
std::get<ElementIdInfo>(elements[0].overflow_id).overflow_identifier,
std::get<ElementIdInfo>(element1.overflow_id).overflow_identifier);
EXPECT_EQ(
std::get<ElementIdInfo>(elements[1].overflow_id).overflow_identifier,
std::get<ElementIdInfo>(element0.overflow_id).overflow_identifier);
EXPECT_EQ(std::get<ActionId>(elements[2].overflow_id),
std::get<ActionId>(action2.overflow_id));
EXPECT_EQ(
std::get<ElementIdInfo>(elements[3].overflow_id).overflow_identifier,
std::get<ElementIdInfo>(element0.overflow_id).overflow_identifier);
EXPECT_EQ(std::get<ActionId>(elements[4].overflow_id),
std::get<ActionId>(action0.overflow_id));
EXPECT_EQ(
std::get<ElementIdInfo>(elements[5].overflow_id).overflow_identifier,
std::get<ElementIdInfo>(element0.overflow_id).overflow_identifier);
EXPECT_EQ(std::get<ActionId>(elements[6].overflow_id),
std::get<ActionId>(action1.overflow_id));
}
TEST_F(ToolbarControllerUnitTest, SupportActionIds) {
auto test_delegate = std::make_unique<TestDelegate>();
auto test_controller = std::make_unique<ToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>(
{ToolbarController::ResponsiveElementInfo(
test_delegate->get_action_ids()[0]),
ToolbarController::ResponsiveElementInfo(
test_delegate->get_action_ids()[1]),
ToolbarController::ResponsiveElementInfo(
test_delegate->get_action_ids()[2])}),
std::vector<ui::ElementIdentifier>(), kElementFlexOrderStart,
toolbar_container_view(), const_cast<OverflowButton*>(overflow_button()),
test_delegate.get(), GetPinnedToolbarActionsModel());
test_delegate->SetContainerView(
toolbar_container_view()->AddChildView(std::make_unique<views::View>()));
toolbar_controller()->overflow_button()->SetVisible(
test_controller->ShouldShowOverflowButton(widget()->GetSize()));
EXPECT_TRUE(overflow_button()->GetVisible());
const auto menu = test_controller->CreateOverflowMenuModel();
EXPECT_TRUE(menu);
EXPECT_EQ(menu->GetItemCount(),
static_cast<size_t>(test_delegate->get_overflowed_count()));
// Overflowed actions should match overflow menu.
const auto& responsive_elements =
GetResponsiveElements(test_controller.get());
for (size_t i = 0; i < responsive_elements.size(); ++i) {
if (IsOverflowed(responsive_elements[i])) {
size_t index = menu->GetIndexOfCommandId(i).value();
EXPECT_EQ(test_controller->GetMenuText(responsive_elements[i]),
menu->GetLabelAt(index));
EXPECT_CALL(*test_delegate, DummyAction);
menu->ActivatedAt(index);
}
}
}
TEST_F(ToolbarControllerUnitTest, StatusIndicatorVisibilityUpdates) {
auto test_delegate = std::make_unique<TestDelegate>();
auto test_controller = std::make_unique<ToolbarController>(
std::vector<ToolbarController::ResponsiveElementInfo>(
{ToolbarController::ResponsiveElementInfo(
test_delegate->get_action_ids()[0]),
ToolbarController::ResponsiveElementInfo(
test_delegate->get_action_ids()[1]),
ToolbarController::ResponsiveElementInfo(
test_delegate->get_action_ids()[2])}),
std::vector<ui::ElementIdentifier>(), kElementFlexOrderStart,
toolbar_container_view(), const_cast<OverflowButton*>(overflow_button()),
test_delegate.get(), GetPinnedToolbarActionsModel());
test_delegate->SetContainerView(
toolbar_container_view()->AddChildView(std::make_unique<views::View>()));
toolbar_controller()->overflow_button()->SetVisible(
test_controller->ShouldShowOverflowButton(widget()->GetSize()));
EXPECT_TRUE(overflow_button()->GetVisible());
overflow_button()->set_toolbar_controller(test_controller.get());
widget()->LayoutRootViewIfNecessary();
overflow_button()->RunMenu();
const ui::SimpleMenuModel* menu = test_controller->menu_model_for_testing();
// Overflowed buttons should match overflow menu.
EXPECT_TRUE(menu);
views::SubmenuView* sub_menu =
test_controller->root_menu_item()->GetSubmenu();
for (auto* menu_item : sub_menu->GetMenuItems()) {
PinnedToolbarButtonStatusIndicator* status_indicator =
PinnedToolbarButtonStatusIndicator::GetStatusIndicator(
menu_item->icon_view());
EXPECT_TRUE(status_indicator);
EXPECT_EQ(status_indicator->GetVisible(), true);
}
const auto& responsive_elements =
GetResponsiveElements(test_controller.get());
for (size_t i = 0; i < responsive_elements.size(); ++i) {
if (IsOverflowed(responsive_elements[i])) {
actions::ActionId element_action_id =
std::get<actions::ActionId>(responsive_elements[i].overflow_id);
test_delegate->GetActionItemFor(element_action_id)
->SetProperty(kActionItemUnderlineIndicatorKey, false);
size_t index = menu->GetIndexOfCommandId(i).value();
views::MenuItemView* menu_item = sub_menu->GetMenuItemAt(index);
PinnedToolbarButtonStatusIndicator* status_indicator =
PinnedToolbarButtonStatusIndicator::GetStatusIndicator(
menu_item->icon_view());
EXPECT_EQ(status_indicator->GetVisible(), false);
}
}
for (size_t i = 0; i < responsive_elements.size(); ++i) {
if (IsOverflowed(responsive_elements[i])) {
actions::ActionId element_action_id =
std::get<actions::ActionId>(responsive_elements[i].overflow_id);
test_delegate->GetActionItemFor(element_action_id)
->SetProperty(kActionItemUnderlineIndicatorKey, true);
size_t index = menu->GetIndexOfCommandId(i).value();
views::MenuItemView* menu_item = sub_menu->GetMenuItemAt(index);
PinnedToolbarButtonStatusIndicator* status_indicator =
PinnedToolbarButtonStatusIndicator::GetStatusIndicator(
menu_item->icon_view());
EXPECT_EQ(status_indicator->GetVisible(), true);
}
}
overflow_button()->set_toolbar_controller(nullptr);
}