#include "ui/views/controls/menu/menu_controller.h"
#include <algorithm>
#include <functional>
#include <type_traits>
#include <utility>
#include <vector>
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/current_thread.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_mode.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/platform/ax_platform_for_test.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/menu_source_type.mojom-shared.h"
#include "ui/base/owned_window_anchor.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/base/ui_base_types.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_handler.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/menu/menu_controller_delegate.h"
#include "ui/views/controls/menu/menu_delegate.h"
#include "ui/views/controls/menu/menu_host.h"
#include "ui/views/controls/menu/menu_host_root_view.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_scroll_view_container.h"
#include "ui/views/controls/menu/menu_types.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/test/ax_event_counter.h"
#include "ui/views/test/menu_test_utils.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget_utils.h"
#if defined(USE_AURA)
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/client/drag_drop_client_observer.h"
#include "ui/aura/null_window_targeter.h"
#include "ui/aura/scoped_window_targeter.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/window.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/views/controls/menu/menu_pre_target_handler.h"
#endif
#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif
#if BUILDFLAG(IS_OZONE_X11)
#include "ui/events/test/events_test_utils_x11.h"
#endif
namespace views {
namespace {
using ::ui::mojom::DragOperation;
constexpr MenuAnchorPosition kBubblePositions[] = {
MenuAnchorPosition::kBubbleTopLeft,
MenuAnchorPosition::kBubbleTopRight,
MenuAnchorPosition::kBubbleLeft,
MenuAnchorPosition::kBubbleRight,
MenuAnchorPosition::kBubbleBottomLeft,
MenuAnchorPosition::kBubbleBottomRight};
bool ShouldIgnoreScreenBoundsForMenus() {
#if BUILDFLAG(IS_OZONE)
return !ui::OzonePlatform::GetInstance()
->GetPlatformProperties()
.supports_global_screen_coordinates;
#else
return false;
#endif
}
gfx::Size GetPreferredSizeForSubmenu(SubmenuView& submenu) {
auto size = submenu.GetPreferredSize({});
const auto insets = submenu.GetScrollViewContainer()->GetInsets();
size.Enlarge(insets.width(), insets.height());
return size;
}
#define GET_CHILD_BUTTON(name, parent, index) \
ASSERT_GT((parent)->children().size(), size_t{(index)}); \
auto* const name = Button::AsButton((parent)->children()[index]); \
ASSERT_NE(nullptr, name)
class TestMenuControllerDelegate : public internal::MenuControllerDelegate {
public:
TestMenuControllerDelegate() = default;
int on_menu_closed_called() const { return on_menu_closed_called_; }
NotifyType on_menu_closed_notify_type() const {
return on_menu_closed_notify_type_;
}
const MenuItemView* on_menu_closed_menu() const {
return on_menu_closed_menu_;
}
int on_menu_closed_mouse_event_flags() const {
return on_menu_closed_mouse_event_flags_;
}
void set_on_menu_closed_callback(base::RepeatingClosure callback) {
on_menu_closed_callback_ = std::move(callback);
}
void OnMenuClosed(NotifyType type,
MenuItemView* menu,
int mouse_event_flags) override;
void SiblingMenuCreated(MenuItemView* menu) override;
private:
int on_menu_closed_called_ = 0;
NotifyType on_menu_closed_notify_type_ = NOTIFY_DELEGATE;
raw_ptr<MenuItemView> on_menu_closed_menu_ = nullptr;
int on_menu_closed_mouse_event_flags_ = 0;
base::RepeatingClosure on_menu_closed_callback_;
};
void TestMenuControllerDelegate::OnMenuClosed(NotifyType type,
MenuItemView* menu,
int mouse_event_flags) {
++on_menu_closed_called_;
on_menu_closed_notify_type_ = type;
on_menu_closed_menu_ = menu;
on_menu_closed_mouse_event_flags_ = mouse_event_flags;
if (on_menu_closed_callback_) {
on_menu_closed_callback_.Run();
}
}
void TestMenuControllerDelegate::SiblingMenuCreated(MenuItemView* menu) {}
class TestEventHandler : public ui::EventHandler {
public:
TestEventHandler() = default;
void OnTouchEvent(ui::TouchEvent* event) override {
if (event->type() == ui::EventType::kTouchPressed) {
++outstanding_touches_;
} else if (event->type() == ui::EventType::kTouchReleased ||
event->type() == ui::EventType::kTouchCancelled) {
--outstanding_touches_;
}
}
int outstanding_touches() const { return outstanding_touches_; }
private:
int outstanding_touches_ = 0;
};
class GestureTestWidget : public Widget {
public:
GestureTestWidget() = default;
void OnGestureEvent(ui::GestureEvent* event) override;
int gesture_count() const { return gesture_count_; }
private:
int gesture_count_ = 0;
};
void GestureTestWidget::OnGestureEvent(ui::GestureEvent* event) {
++gesture_count_;
}
#if defined(USE_AURA)
class TestDragDropClient : public aura::client::DragDropClient {
public:
explicit TestDragDropClient(base::RepeatingClosure callback)
: start_drag_and_drop_callback_(std::move(callback)) {}
DragOperation StartDragAndDrop(std::unique_ptr<ui::OSExchangeData> data,
aura::Window* root_window,
aura::Window* source_window,
const gfx::Point& screen_location,
int allowed_operations,
ui::mojom::DragEventSource source) override;
#if BUILDFLAG(IS_LINUX)
void UpdateDragImage(const gfx::ImageSkia& image,
const gfx::Vector2d& offset) override {}
#endif
void DragCancel() override;
bool IsDragDropInProgress() override;
void AddObserver(aura::client::DragDropClientObserver* observer) override {}
void RemoveObserver(aura::client::DragDropClientObserver* observer) override {
}
private:
base::RepeatingClosure start_drag_and_drop_callback_;
bool drag_in_progress_ = false;
};
DragOperation TestDragDropClient::StartDragAndDrop(
std::unique_ptr<ui::OSExchangeData> data,
aura::Window* root_window,
aura::Window* source_window,
const gfx::Point& screen_location,
int allowed_operations,
ui::mojom::DragEventSource source) {
drag_in_progress_ = true;
start_drag_and_drop_callback_.Run();
return DragOperation::kNone;
}
void TestDragDropClient::DragCancel() {
drag_in_progress_ = false;
}
bool TestDragDropClient::IsDragDropInProgress() {
return drag_in_progress_;
}
#endif
class CancelMenuOnMousePressView : public View {
METADATA_HEADER(CancelMenuOnMousePressView, View)
public:
explicit CancelMenuOnMousePressView(base::WeakPtr<MenuController> controller)
: controller_(controller) {}
bool OnMousePressed(const ui::MouseEvent& event) override;
gfx::Size CalculatePreferredSize(
const SizeBounds& ) const override;
private:
const base::WeakPtr<MenuController> controller_;
};
bool CancelMenuOnMousePressView::OnMousePressed(const ui::MouseEvent& event) {
controller_->Cancel(MenuController::ExitType::kAll);
return true;
}
gfx::Size CancelMenuOnMousePressView::CalculatePreferredSize(
const SizeBounds& ) const {
return size();
}
BEGIN_METADATA(CancelMenuOnMousePressView)
END_METADATA
}
struct MenuBoundsOptions {
gfx::Rect anchor_bounds = gfx::Rect(500, 500, 10, 10);
gfx::Rect monitor_bounds = gfx::Rect(0, 0, 1000, 1000);
gfx::Size menu_size = gfx::Size(100, 150);
MenuAnchorPosition menu_anchor = MenuAnchorPosition::kTopLeft;
MenuItemView::MenuPosition menu_position =
MenuItemView::MenuPosition::kBestFit;
};
class MenuControllerTest : public ViewsTestBase,
public testing::WithParamInterface<bool> {
public:
MenuControllerTest() = default;
void SetUp() override;
void TearDown() override;
void ReleaseTouchId(int id);
void PressKey(ui::KeyboardCode key_code);
void DispatchKey(ui::KeyboardCode key_code);
gfx::Rect CalculateMenuBounds(const MenuBoundsOptions& options);
gfx::Rect CalculateBubbleMenuBoundsWithoutInsets(
const MenuBoundsOptions& options,
MenuItemView* menu_item = nullptr);
MenuItemView::MenuPosition menu_item_actual_position() const {
return menu_item_->actual_menu_position();
}
gfx::Rect CalculateExpectedMenuAnchorRect(MenuItemView* menu_item);
MenuController::MenuOpenDirection GetChildMenuOpenDirectionAtDepth(
size_t depth) const;
void SetChildMenuOpenDirectionAtDepth(
size_t depth,
MenuController::MenuOpenDirection direction);
void MenuChildrenChanged(MenuItemView* item);
static MenuAnchorPosition AdjustAnchorPositionForRtl(
MenuAnchorPosition position);
#if defined(USE_AURA)
void TestAsyncEscapeKey();
void TestCancelEvent();
#endif
void VerifyDragCompleteThenDestroy();
void TestDragCompleteThenDestroyOnMenuClosed();
void TestMenuControllerReplacementDuringDrag();
void TestCancelAllDuringDrag();
void TestDestroyedDuringViewsRelease();
void TestMenuFitsOnScreen(MenuAnchorPosition menu_anchor_position,
const gfx::Rect& monitor_bounds);
void TestMenuFitsOnScreenSmallAnchor(MenuAnchorPosition menu_anchor_position,
const gfx::Rect& monitor_bounds);
void TestMenuFitsOnSmallScreen(MenuAnchorPosition menu_anchor_position,
const gfx::Rect& monitor_bounds);
void TestSubmenuFitsOnScreen(MenuItemView* item,
const gfx::Rect& monitor_bounds,
const gfx::Rect& parent_bounds,
MenuAnchorPosition menu_anchor);
protected:
void SetPendingStateItem(MenuItemView* item);
void SetState(MenuItemView* item);
void IncrementSelection();
void DecrementSelection();
void DestroyMenuControllerOnMenuClosed(TestMenuControllerDelegate* delegate);
MenuItemView* FindInitialSelectableMenuItemDown(MenuItemView* parent);
MenuItemView* FindInitialSelectableMenuItemUp(MenuItemView* parent);
internal::MenuControllerDelegate* current_controller_delegate() {
return menu_controller_->delegate_;
}
bool showing() const { return menu_controller_->showing_; }
MenuHost* menu_host_for_submenu(SubmenuView* submenu) {
return submenu->host_;
}
MenuHostRootView* CreateMenuHostRootView(MenuHost* host);
void MenuHostOnDragWillStart(MenuHost* host);
void MenuHostOnDragComplete(MenuHost* host);
void SelectByChar(char16_t character);
void SetDropMenuItem(MenuItemView* target,
MenuDelegate::DropPosition position);
void SetComboboxType(MenuController::ComboboxType combobox_type);
template <typename T>
static T ConvertEvent(View* source, const T& event) {
return T(event, source, source->GetWidget()->GetRootView());
}
void SetSelectionOnPointerDown(SubmenuView* source,
const ui::MouseEvent& event);
bool ProcessMousePressed(SubmenuView* source, const ui::MouseEvent& event);
bool ProcessMouseDragged(SubmenuView* source, const ui::MouseEvent& event);
void ProcessMouseReleased(SubmenuView* source, const ui::MouseEvent& event);
void ProcessMouseMoved(SubmenuView* source, const ui::MouseEvent& event);
void ProcessGestureEvent(SubmenuView* source, const ui::GestureEvent& event);
void ProcessTouchEvent(SubmenuView* source, const ui::TouchEvent& event);
void Accept(MenuItemView* item, int event_flags);
void StartDrag();
void SetUpMenuControllerForCalculateBounds(const MenuBoundsOptions& options,
MenuItemView* menu_item);
GestureTestWidget* owner() { return owner_.get(); }
ui::test::EventGenerator* event_generator() { return event_generator_.get(); }
MenuItemView* menu_item() { return menu_item_.get(); }
test::TestMenuDelegate* menu_delegate() { return menu_delegate_.get(); }
TestMenuControllerDelegate* menu_controller_delegate() {
return menu_controller_delegate_.get();
}
MenuController* menu_controller() { return menu_controller_; }
const MenuItemView* pending_state_item() const {
return menu_controller_->pending_state_.item;
}
MenuController::ExitType menu_exit_type() const {
return menu_controller_->exit_type_;
}
template <typename T = void (*)(MenuHost::InitParams&)>
requires(std::is_invocable_v<T, MenuHost::InitParams&>)
void ShowSubmenu(
SubmenuView* submenu = nullptr,
T&& adjust_params = [](auto&) {}) {
if (!submenu) {
submenu = menu_item()->GetSubmenu();
}
MenuHost::InitParams params;
params.parent = owner();
params.bounds = gfx::Rect(GetPreferredSizeForSubmenu(*submenu));
std::invoke(std::forward<T>(adjust_params), params);
submenu->ShowAt(params);
}
MenuItemView* AddButtonMenuItems(bool single_child);
void DestroyMenuItem();
Button* hot_button() { return menu_controller_->hot_button_; }
void SetHotTrackedButton(Button* hot_button);
void ExitMenuRun();
void DestroyMenuController();
void DestroyMenuControllerDelegate();
int owner_gesture_count() const { return owner_->gesture_count(); }
static bool SelectionWraps();
void OpenMenu(MenuItemView* parent,
const MenuBoundsOptions& options = MenuBoundsOptions());
gfx::Insets GetBorderAndShadowInsets(bool is_submenu);
private:
virtual bool for_drop() const { return false; }
std::unique_ptr<GestureTestWidget> owner_;
std::unique_ptr<ui::test::EventGenerator> event_generator_;
std::unique_ptr<MenuItemView> menu_item_;
std::unique_ptr<TestMenuControllerDelegate> menu_controller_delegate_;
std::unique_ptr<test::TestMenuDelegate> menu_delegate_;
raw_ptr<MenuController> menu_controller_ = nullptr;
};
void MenuControllerTest::SetUp() {
if (testing::UnitTest::GetInstance()->current_test_info()->value_param()) {
base::i18n::SetRTLForTesting(GetParam());
}
set_views_delegate(std::make_unique<test::ReleaseRefTestViewsDelegate>());
ViewsTestBase::SetUp();
ASSERT_TRUE(base::CurrentUIThread::IsSet());
owner_ = std::make_unique<GestureTestWidget>();
Widget::InitParams params = CreateParams(
Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
owner_->Init(std::move(params));
event_generator_ =
std::make_unique<ui::test::EventGenerator>(GetRootWindow(owner()));
owner_->Show();
menu_delegate_ = std::make_unique<test::TestMenuDelegate>();
menu_item_ = std::make_unique<MenuItemView>(menu_delegate_.get());
menu_item_->AppendMenuItem(1, u"One");
menu_item_->AppendMenuItem(2, u"Two");
menu_item_->AppendMenuItem(3, u"Three");
menu_item_->AppendMenuItem(4, u"Four");
menu_controller_delegate_ = std::make_unique<TestMenuControllerDelegate>();
menu_controller_ =
new MenuController(for_drop(), menu_controller_delegate_.get());
menu_controller_->owner_ = owner();
menu_controller_->showing_ = true;
menu_controller_->SetSelection(menu_item(),
MenuController::SELECTION_UPDATE_IMMEDIATELY);
menu_item_->set_controller(menu_controller_);
}
void MenuControllerTest::TearDown() {
owner_->CloseNow();
DestroyMenuController();
menu_controller_delegate_.reset();
menu_item_.reset();
ViewsTestBase::TearDown();
base::i18n::SetRTLForTesting(false);
}
void MenuControllerTest::ReleaseTouchId(int id) {
event_generator_->ReleaseTouchId(id);
}
void MenuControllerTest::PressKey(ui::KeyboardCode key_code) {
event_generator_->PressKey(key_code, 0);
}
void MenuControllerTest::DispatchKey(ui::KeyboardCode key_code) {
ui::KeyEvent event(ui::EventType::kKeyPressed, key_code, 0);
menu_controller_->OnWillDispatchKeyEvent(&event);
}
gfx::Rect MenuControllerTest::CalculateMenuBounds(
const MenuBoundsOptions& options) {
SetUpMenuControllerForCalculateBounds(options, menu_item_.get());
MenuController::MenuOpenDirection resulting_direction;
ui::OwnedWindowAnchor anchor;
return menu_controller_->CalculateMenuBounds(
menu_item_.get(), MenuController::MenuOpenDirection::kLeading,
&resulting_direction, &anchor);
}
gfx::Rect MenuControllerTest::CalculateBubbleMenuBoundsWithoutInsets(
const MenuBoundsOptions& options,
MenuItemView* menu_item) {
if (!menu_item) {
menu_item = menu_item_.get();
}
SetUpMenuControllerForCalculateBounds(options, menu_item);
MenuController::MenuOpenDirection resulting_direction;
ui::OwnedWindowAnchor anchor;
gfx::Rect bounds = menu_controller_->CalculateBubbleMenuBounds(
menu_item, MenuController::MenuOpenDirection::kLeading,
&resulting_direction, &anchor);
bounds.Inset(menu_item->GetSubmenu()
->GetScrollViewContainer()
->outside_border_insets());
return bounds;
}
gfx::Rect MenuControllerTest::CalculateExpectedMenuAnchorRect(
MenuItemView* menu_item) {
if (!menu_item->GetParentMenuItem()) {
return menu_controller_->state_.initial_bounds;
}
gfx::Rect bounds = menu_item->GetBoundsInScreen();
bounds.set_height(1);
return bounds;
}
MenuController::MenuOpenDirection
MenuControllerTest::GetChildMenuOpenDirectionAtDepth(size_t depth) const {
return menu_controller_->GetChildMenuOpenDirectionAtDepth(depth);
}
void MenuControllerTest::SetChildMenuOpenDirectionAtDepth(
size_t depth,
MenuController::MenuOpenDirection direction) {
menu_controller_->SetChildMenuOpenDirectionAtDepth(depth, direction);
}
void MenuControllerTest::MenuChildrenChanged(MenuItemView* item) {
menu_controller_->MenuChildrenChanged(item);
}
MenuAnchorPosition MenuControllerTest::AdjustAnchorPositionForRtl(
MenuAnchorPosition position) {
return MenuController::AdjustAnchorPositionForRtl(position);
}
#if defined(USE_AURA)
void MenuControllerTest::TestAsyncEscapeKey() {
ui::KeyEvent event(ui::EventType::kKeyPressed, ui::VKEY_ESCAPE, 0);
menu_controller_->OnWillDispatchKeyEvent(&event);
}
void MenuControllerTest::TestCancelEvent() {
EXPECT_EQ(MenuController::ExitType::kNone, menu_controller_->exit_type());
ui::CancelModeEvent cancel_event;
event_generator_->Dispatch(&cancel_event);
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller_->exit_type());
}
#endif
void MenuControllerTest::VerifyDragCompleteThenDestroy() {
EXPECT_FALSE(menu_controller()->drag_in_progress());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
DestroyMenuController();
}
void MenuControllerTest::TestDragCompleteThenDestroyOnMenuClosed() {
menu_controller_delegate_->set_on_menu_closed_callback(
base::BindRepeating(&MenuControllerTest::VerifyDragCompleteThenDestroy,
base::Unretained(this)));
}
void MenuControllerTest::TestMenuControllerReplacementDuringDrag() {
DestroyMenuController();
menu_item()->GetSubmenu()->Close();
menu_controller_ =
new MenuController(false, menu_controller_delegate_.get());
menu_controller_->owner_ = owner_.get();
menu_controller_->showing_ = true;
}
void MenuControllerTest::TestCancelAllDuringDrag() {
menu_controller_->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(0, menu_controller_delegate_->on_menu_closed_called());
}
void MenuControllerTest::TestDestroyedDuringViewsRelease() {
static_cast<test::ReleaseRefTestViewsDelegate*>(test_views_delegate())
->set_release_ref_callback(base::BindRepeating(
&MenuControllerTest::DestroyMenuController, base::Unretained(this)));
menu_controller_->ExitMenu();
}
void MenuControllerTest::TestMenuFitsOnScreen(
MenuAnchorPosition menu_anchor_position,
const gfx::Rect& monitor_bounds) {
constexpr int kButtonSize = 50;
const auto test_within_bounds = [&](gfx::Point origin) {
const MenuBoundsOptions options = {
.anchor_bounds = gfx::Rect(origin, gfx::Size(kButtonSize, kButtonSize)),
.monitor_bounds = monitor_bounds,
.menu_anchor = menu_anchor_position};
EXPECT_TRUE(options.monitor_bounds.Contains(
CalculateBubbleMenuBoundsWithoutInsets(options)))
<< "Anchor position: " << base::ToString(menu_anchor_position)
<< ", monitor bounds: " << monitor_bounds.ToString()
<< ", origin: " << origin.ToString();
};
const gfx::Point monitor_center = monitor_bounds.CenterPoint();
test_within_bounds(
gfx::Point(monitor_center.x(), monitor_bounds.bottom() - kButtonSize));
test_within_bounds(gfx::Point(monitor_bounds.x(), monitor_center.y()));
test_within_bounds(
gfx::Point(monitor_bounds.right() - kButtonSize, monitor_center.y()));
}
void MenuControllerTest::TestMenuFitsOnScreenSmallAnchor(
MenuAnchorPosition menu_anchor_position,
const gfx::Rect& monitor_bounds) {
const auto test_within_bounds = [&](gfx::Point origin) {
const MenuBoundsOptions options = {.anchor_bounds = gfx::Rect(origin, {}),
.monitor_bounds = monitor_bounds,
.menu_anchor = menu_anchor_position};
EXPECT_TRUE(options.monitor_bounds.Contains(
CalculateBubbleMenuBoundsWithoutInsets(options)))
<< "Anchor position: " << base::ToString(menu_anchor_position)
<< ", monitor bounds: " << monitor_bounds.ToString()
<< ", origin: " << origin.ToString();
};
test_within_bounds(monitor_bounds.origin());
test_within_bounds(monitor_bounds.bottom_left());
test_within_bounds(monitor_bounds.top_right());
test_within_bounds(monitor_bounds.bottom_right());
}
void MenuControllerTest::TestMenuFitsOnSmallScreen(
MenuAnchorPosition menu_anchor_position,
const gfx::Rect& monitor_bounds) {
const auto test_within_bounds = [&](gfx::Point origin) {
const MenuBoundsOptions options = {
.anchor_bounds = gfx::Rect(origin, {}),
.monitor_bounds = monitor_bounds,
.menu_size = monitor_bounds.size() + gfx::Size(100, 100),
.menu_anchor = menu_anchor_position};
EXPECT_TRUE(options.monitor_bounds.Contains(
CalculateBubbleMenuBoundsWithoutInsets(options)))
<< "Anchor position: " << base::ToString(menu_anchor_position)
<< ", monitor bounds: " << monitor_bounds.ToString()
<< ", origin: " << origin.ToString();
};
test_within_bounds(monitor_bounds.origin());
test_within_bounds(monitor_bounds.bottom_left());
test_within_bounds(monitor_bounds.top_right());
test_within_bounds(monitor_bounds.bottom_right());
test_within_bounds(monitor_bounds.CenterPoint());
}
void MenuControllerTest::TestSubmenuFitsOnScreen(
MenuItemView* item,
const gfx::Rect& monitor_bounds,
const gfx::Rect& parent_bounds,
MenuAnchorPosition menu_anchor) {
const MenuBoundsOptions options = {
.monitor_bounds = monitor_bounds,
.menu_size = GetPreferredSizeForSubmenu(*item->GetSubmenu()),
.menu_anchor = menu_anchor};
SubmenuView* const submenu = item->GetParentMenuItem()->GetSubmenu();
ShowSubmenu(submenu, [&](auto& params) {
params.bounds = parent_bounds;
params.bounds.Inset(-submenu->GetScrollViewContainer()->GetInsets());
});
const gfx::Rect final_bounds =
CalculateBubbleMenuBoundsWithoutInsets(options, item);
EXPECT_TRUE(monitor_bounds.Contains(final_bounds))
<< monitor_bounds.ToString() << " does not contain "
<< final_bounds.ToString();
submenu->Close();
}
void MenuControllerTest::SetPendingStateItem(MenuItemView* item) {
menu_controller_->pending_state_.item = item;
menu_controller_->pending_state_.submenu_open = true;
}
void MenuControllerTest::SetState(MenuItemView* item) {
SetPendingStateItem(item);
menu_controller_->state_.item = item;
menu_controller_->state_.submenu_open = true;
}
void MenuControllerTest::IncrementSelection() {
menu_controller_->IncrementSelection(
MenuController::INCREMENT_SELECTION_DOWN);
}
void MenuControllerTest::DecrementSelection() {
menu_controller_->IncrementSelection(MenuController::INCREMENT_SELECTION_UP);
}
void MenuControllerTest::DestroyMenuControllerOnMenuClosed(
TestMenuControllerDelegate* delegate) {
delegate->set_on_menu_closed_callback(base::BindRepeating(
&MenuControllerTest::DestroyMenuController, base::Unretained(this)));
}
void MenuControllerTest::DestroyMenuControllerDelegate() {
menu_controller_delegate_.reset();
}
MenuItemView* MenuControllerTest::FindInitialSelectableMenuItemDown(
MenuItemView* parent) {
return menu_controller_->FindInitialSelectableMenuItem(
parent, MenuController::INCREMENT_SELECTION_DOWN);
}
MenuItemView* MenuControllerTest::FindInitialSelectableMenuItemUp(
MenuItemView* parent) {
return menu_controller_->FindInitialSelectableMenuItem(
parent, MenuController::INCREMENT_SELECTION_UP);
}
MenuHostRootView* MenuControllerTest::CreateMenuHostRootView(MenuHost* host) {
return static_cast<MenuHostRootView*>(host->CreateRootView());
}
void MenuControllerTest::MenuHostOnDragWillStart(MenuHost* host) {
host->OnDragWillStart();
}
void MenuControllerTest::MenuHostOnDragComplete(MenuHost* host) {
host->OnDragComplete();
}
void MenuControllerTest::SelectByChar(char16_t character) {
menu_controller_->SelectByChar(character);
}
void MenuControllerTest::SetDropMenuItem(MenuItemView* target,
MenuDelegate::DropPosition position) {
menu_controller_->SetDropMenuItem(target, position);
}
void MenuControllerTest::SetComboboxType(
MenuController::ComboboxType combobox_type) {
menu_controller_->set_combobox_type(combobox_type);
}
void MenuControllerTest::SetSelectionOnPointerDown(
SubmenuView* source,
const ui::MouseEvent& event) {
const ui::MouseEvent converted_event = ConvertEvent(source, event);
menu_controller_->SetSelectionOnPointerDown(source, &converted_event);
}
bool MenuControllerTest::ProcessMousePressed(SubmenuView* source,
const ui::MouseEvent& event) {
return menu_controller_->OnMousePressed(source, ConvertEvent(source, event));
}
bool MenuControllerTest::ProcessMouseDragged(SubmenuView* source,
const ui::MouseEvent& event) {
return menu_controller_->OnMouseDragged(source, ConvertEvent(source, event));
}
void MenuControllerTest::ProcessMouseReleased(SubmenuView* source,
const ui::MouseEvent& event) {
menu_controller_->OnMouseReleased(source, ConvertEvent(source, event));
}
void MenuControllerTest::ProcessMouseMoved(SubmenuView* source,
const ui::MouseEvent& event) {
menu_controller_->OnMouseMoved(source, ConvertEvent(source, event));
}
void MenuControllerTest::ProcessGestureEvent(SubmenuView* source,
const ui::GestureEvent& event) {
ui::GestureEvent converted_event = ConvertEvent(source, event);
menu_controller_->OnGestureEvent(source, &converted_event);
}
void MenuControllerTest::ProcessTouchEvent(SubmenuView* source,
const ui::TouchEvent& event) {
ui::TouchEvent converted_event = ConvertEvent(source, event);
menu_controller_->OnTouchEvent(source, &converted_event);
}
void MenuControllerTest::Accept(MenuItemView* item, int event_flags) {
menu_controller_->Accept(item, event_flags);
views::test::WaitForMenuClosureAnimation();
}
void MenuControllerTest::StartDrag() {
MenuItemView* const dragged_item =
menu_item()->GetSubmenu()->GetMenuItemAt(0);
menu_controller_->state_.item = dragged_item;
menu_controller_->StartDrag(menu_item()->GetSubmenu(),
dragged_item->bounds().CenterPoint());
}
void MenuControllerTest::SetUpMenuControllerForCalculateBounds(
const MenuBoundsOptions& options,
MenuItemView* menu_item) {
menu_controller_->pending_state_.anchor = menu_controller_->state_.anchor =
options.menu_anchor;
menu_controller_->pending_state_.initial_bounds =
menu_controller_->state_.initial_bounds = options.anchor_bounds;
menu_controller_->pending_state_.monitor_bounds =
menu_controller_->state_.monitor_bounds = options.monitor_bounds;
menu_item->set_actual_menu_position(options.menu_position);
menu_item->GetSubmenu()->GetScrollViewContainer()->SetPreferredSize(
options.menu_size);
}
MenuItemView* MenuControllerTest::AddButtonMenuItems(bool single_child) {
MenuItemView* const item_view = menu_item()->AppendMenuItem(5, u"Five");
const size_t children_count = single_child ? 1 : 3;
for (size_t i = 0; i < children_count; ++i) {
item_view
->AddChildView(
std::make_unique<LabelButton>(Button::PressedCallback(), u"Label"))
->SetFocusBehavior(View::FocusBehavior::ALWAYS);
}
ShowSubmenu();
return item_view;
}
void MenuControllerTest::DestroyMenuItem() {
menu_item_.reset();
}
void MenuControllerTest::SetHotTrackedButton(Button* hot_button) {
menu_controller_->SetHotTrackedButton(hot_button);
}
void MenuControllerTest::ExitMenuRun() {
menu_controller_->SetExitType(MenuController::ExitType::kOutermost);
menu_controller_->ExitTopMostMenu();
}
void MenuControllerTest::DestroyMenuController() {
if (!menu_controller_) {
return;
}
if (!owner_->IsClosed()) {
owner_->RemoveObserver(menu_controller_);
}
menu_controller_->showing_ = false;
menu_controller_->owner_ = nullptr;
delete menu_controller_.ExtractAsDangling();
}
bool MenuControllerTest::SelectionWraps() {
return MenuConfig::instance().arrow_key_selection_wraps;
}
void MenuControllerTest::OpenMenu(MenuItemView* parent,
const MenuBoundsOptions& options) {
SetUpMenuControllerForCalculateBounds(options, parent);
menu_controller_->OpenMenuImpl(parent, true);
}
gfx::Insets MenuControllerTest::GetBorderAndShadowInsets(bool is_submenu) {
const MenuConfig& menu_config = MenuConfig::instance();
int elevation = menu_config.bubble_menu_shadow_elevation;
BubbleBorder::Shadow shadow_type = BubbleBorder::STANDARD_SHADOW;
#if BUILDFLAG(IS_CHROMEOS)
if (menu_controller_->use_ash_system_ui_layout()) {
if (is_submenu) {
elevation = menu_config.bubble_submenu_shadow_elevation;
}
shadow_type = BubbleBorder::CHROMEOS_SYSTEM_UI_SHADOW;
}
#endif
return BubbleBorder::GetBorderAndShadowInsets(elevation, shadow_type);
}
class MenuControllerForDropTest : public MenuControllerTest {
public:
MenuControllerForDropTest() = default;
~MenuControllerForDropTest() override = default;
private:
bool for_drop() const override { return true; }
};
INSTANTIATE_TEST_SUITE_P(All,
MenuControllerTest,
testing::Bool(),
[](const auto& info) {
return info.param ? "RTL" : "LTR";
});
#if defined(USE_AURA)
TEST_F(MenuControllerTest, EventTargeter) {
{
aura::ScopedWindowTargeter scoped_targeter(
GetRootWindow(owner()), std::make_unique<aura::NullWindowTargeter>());
PressKey(ui::VKEY_ESCAPE);
EXPECT_EQ(MenuController::ExitType::kNone, menu_exit_type());
}
TestAsyncEscapeKey();
EXPECT_EQ(MenuController::ExitType::kAll, menu_exit_type());
}
#endif
#if BUILDFLAG(IS_OZONE_X11)
TEST_F(MenuControllerTest, TouchIdsReleasedCorrectly) {
if (ui::OzonePlatform::GetPlatformNameForTest() != "x11") {
GTEST_SKIP();
}
TestEventHandler test_event_handler;
GetRootWindow(owner())->AddPreTargetHandler(&test_event_handler);
ui::SetUpTouchDevicesForTest({1});
event_generator()->PressTouchId(0);
event_generator()->PressTouchId(1);
event_generator()->ReleaseTouchId(0);
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
MenuControllerTest::ReleaseTouchId(1);
TestAsyncEscapeKey();
EXPECT_EQ(MenuController::ExitType::kAll, menu_exit_type());
EXPECT_EQ(0, test_event_handler.outstanding_touches());
GetRootWindow(owner())->RemovePreTargetHandler(&test_event_handler);
}
#endif
TEST_F(MenuControllerTest, InitialSelectedItem) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
submenu->GetMenuItemAt(0)->SetEnabled(false);
const auto check_has_command = [](const MenuItemView* item, int command) {
ASSERT_NE(nullptr, item);
EXPECT_EQ(command, item->GetCommand());
};
check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 2);
check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 4);
submenu->GetMenuItemAt(0)->SetEnabled(true);
submenu->GetMenuItemAt(2)->SetEnabled(false);
submenu->GetMenuItemAt(3)->SetEnabled(false);
check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 1);
check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 2);
submenu->GetMenuItemAt(1)->SetEnabled(false);
check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 1);
check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 1);
submenu->GetMenuItemAt(0)->SetEnabled(false);
submenu->GetMenuItemAt(2)->SetEnabled(true);
check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 3);
check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 3);
submenu->GetMenuItemAt(1)->SetEnabled(true);
submenu->GetMenuItemAt(2)->SetEnabled(false);
check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 2);
check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 2);
}
TEST_F(MenuControllerTest, VerifyScrollArrowShown) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
auto* const scroll_container = submenu->GetScrollViewContainer();
MenuHost::InitParams params;
params.parent = owner();
params.bounds = gfx::Rect(GetPreferredSizeForSubmenu(*submenu));
submenu->ShowAt(params);
EXPECT_FALSE(scroll_container->scroll_down_button()->GetVisible());
params.bounds.set_height(params.bounds.height() - 1);
submenu->ShowAt(params);
EXPECT_TRUE(scroll_container->scroll_down_button()->GetVisible());
}
TEST_F(MenuControllerTest, VerifyMenuBubblePositionAfterSizeChanges) {
constexpr gfx::Rect kMonitorBounds(0, 0, 500, 500);
constexpr gfx::Size kMenuSize(100, 200);
const gfx::Insets border_and_shadow_insets =
GetBorderAndShadowInsets(false);
const gfx::Point anchor_point(kMonitorBounds.width() / 2,
kMonitorBounds.bottom() + 1 -
kMenuSize.height() +
border_and_shadow_insets.height());
MenuBoundsOptions options = {
.anchor_bounds = gfx::Rect(anchor_point, gfx::Size()),
.monitor_bounds = kMonitorBounds,
.menu_anchor = MenuAnchorPosition::kBubbleRight};
options.menu_size = kMenuSize;
EXPECT_GT(options.anchor_bounds.y() - border_and_shadow_insets.height() +
kMenuSize.height(),
kMonitorBounds.bottom());
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds,
menu_item_actual_position());
options.menu_position = MenuItemView::MenuPosition::kBelowBounds;
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds,
menu_item_actual_position());
constexpr gfx::Size kUpdatedSize(kMenuSize.width(), kMenuSize.height() / 2);
EXPECT_LE(options.anchor_bounds.y() - border_and_shadow_insets.height() +
kUpdatedSize.height(),
kMonitorBounds.bottom());
options.menu_size = kUpdatedSize;
options.menu_position = MenuItemView::MenuPosition::kAboveBounds;
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds,
menu_item_actual_position());
}
TEST_F(MenuControllerTest, VerifyContextMenuBubblePositionAfterSizeChanges) {
constexpr gfx::Rect kMonitorBounds(0, 0, 500, 500);
constexpr gfx::Size kMenuSize(100, 200);
const gfx::Insets border_and_shadow_insets =
GetBorderAndShadowInsets(false);
const gfx::Point anchor_point(kMonitorBounds.width() / 2,
kMonitorBounds.bottom() + 1 -
kMenuSize.height() +
border_and_shadow_insets.height());
MenuBoundsOptions options = {
.anchor_bounds = gfx::Rect(anchor_point, gfx::Size()),
.monitor_bounds = kMonitorBounds,
.menu_anchor = MenuAnchorPosition::kBubbleBottomRight};
options.menu_size = kMenuSize;
EXPECT_GT(options.anchor_bounds.y() - border_and_shadow_insets.height() +
kMenuSize.height(),
kMonitorBounds.bottom());
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds,
menu_item_actual_position());
options.menu_position = MenuItemView::MenuPosition::kBelowBounds;
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds,
menu_item_actual_position());
constexpr gfx::Size kUpdatedSize(kMenuSize.width(), kMenuSize.height() / 2);
EXPECT_LE(options.anchor_bounds.y() - border_and_shadow_insets.height() +
kUpdatedSize.height(),
kMonitorBounds.bottom());
options.menu_size = kUpdatedSize;
options.menu_position = MenuItemView::MenuPosition::kAboveBounds;
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds,
menu_item_actual_position());
options.menu_position = MenuItemView::MenuPosition::kBelowBounds;
CalculateBubbleMenuBoundsWithoutInsets(options);
EXPECT_EQ(MenuItemView::MenuPosition::kBelowBounds,
menu_item_actual_position());
}
TEST_F(MenuControllerTest, FirstSelectedItem) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
SetPendingStateItem(submenu->GetMenuItemAt(0));
EXPECT_EQ(1, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_HOME);
EXPECT_EQ(1, pending_state_item()->GetCommand());
SetPendingStateItem(menu_item());
EXPECT_EQ(0, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_HOME);
EXPECT_EQ(1, pending_state_item()->GetCommand());
SetPendingStateItem(submenu->GetMenuItemAt(3));
EXPECT_EQ(4, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_HOME);
EXPECT_EQ(1, pending_state_item()->GetCommand());
}
TEST_F(MenuControllerTest, LastSelectedItem) {
ShowSubmenu();
SetPendingStateItem(menu_item());
EXPECT_EQ(0, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_END);
EXPECT_EQ(4, pending_state_item()->GetCommand());
SubmenuView* const submenu = menu_item()->GetSubmenu();
SetPendingStateItem(submenu->GetMenuItemAt(3));
EXPECT_EQ(4, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_END);
EXPECT_EQ(4, pending_state_item()->GetCommand());
SetPendingStateItem(submenu->GetMenuItemAt(0));
EXPECT_EQ(1, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_END);
EXPECT_EQ(4, pending_state_item()->GetCommand());
}
class MenuControllerSelectionTest : public MenuControllerTest {
public:
MenuControllerSelectionTest() = default;
protected:
struct SelectionMechanism {
base::RepeatingClosure IncrementSelection;
base::RepeatingClosure DecrementSelection;
};
const std::vector<SelectionMechanism>& selection_mechanisms() {
return selection_mechanisms_;
}
private:
const std::vector<SelectionMechanism> selection_mechanisms_ = {
{base::BindRepeating(&MenuControllerSelectionTest::IncrementSelection,
base::Unretained(this)),
base::BindRepeating(&MenuControllerSelectionTest::DecrementSelection,
base::Unretained(this))},
{base::BindRepeating(&MenuControllerTest::DispatchKey,
base::Unretained(this),
ui::VKEY_DOWN),
base::BindRepeating(&MenuControllerTest::DispatchKey,
base::Unretained(this),
ui::VKEY_UP)},
{base::BindRepeating(&MenuControllerTest::DispatchKey,
base::Unretained(this),
ui::VKEY_NEXT),
base::BindRepeating(&MenuControllerTest::DispatchKey,
base::Unretained(this),
ui::VKEY_PRIOR)}};
};
TEST_F(MenuControllerSelectionTest, NextSelectedItem) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
for (const auto& selection_mechanism : selection_mechanisms()) {
submenu->GetMenuItemAt(2)->SetEnabled(false);
SetPendingStateItem(submenu->GetMenuItemAt(0));
EXPECT_EQ(1, pending_state_item()->GetCommand());
selection_mechanism.IncrementSelection.Run();
EXPECT_EQ(2, pending_state_item()->GetCommand());
selection_mechanism.IncrementSelection.Run();
EXPECT_EQ(4, pending_state_item()->GetCommand());
selection_mechanism.IncrementSelection.Run();
if (SelectionWraps()) {
EXPECT_EQ(1, pending_state_item()->GetCommand());
selection_mechanism.DecrementSelection.Run();
}
EXPECT_EQ(4, pending_state_item()->GetCommand());
selection_mechanism.DecrementSelection.Run();
EXPECT_EQ(2, pending_state_item()->GetCommand());
selection_mechanism.DecrementSelection.Run();
EXPECT_EQ(1, pending_state_item()->GetCommand());
}
}
TEST_F(MenuControllerSelectionTest, PreviousSelectedItem) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
for (const auto& selection_mechanism : selection_mechanisms()) {
submenu->GetMenuItemAt(3)->SetEnabled(false);
SetPendingStateItem(menu_item());
EXPECT_EQ(0, pending_state_item()->GetCommand());
selection_mechanism.DecrementSelection.Run();
EXPECT_EQ(3, pending_state_item()->GetCommand());
}
}
TEST_F(MenuControllerTest, CurrentSelectedItem) {
ShowSubmenu();
SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0));
EXPECT_EQ(1, pending_state_item()->GetCommand());
DispatchKey(ui::VKEY_HOME);
EXPECT_EQ(pending_state_item(), menu_controller()->GetSelectedMenuItem());
EXPECT_TRUE(showing());
EXPECT_EQ(1, pending_state_item()->GetCommand());
menu_controller()->SelectItemAndOpenSubmenu(menu_item());
EXPECT_TRUE(showing());
EXPECT_EQ(0, pending_state_item()->GetCommand());
}
TEST_F(MenuControllerTest, SelectByChar) {
SetComboboxType(MenuController::ComboboxType::kReadonly);
ShowSubmenu();
SelectByChar(0);
EXPECT_EQ(0, pending_state_item()->GetCommand());
SelectByChar('f');
EXPECT_EQ(4, pending_state_item()->GetCommand());
}
TEST_F(MenuControllerTest, SelectChildButtonView) {
AddButtonMenuItems(false);
SubmenuView* const submenu = menu_item()->GetSubmenu();
const View* const buttons_view = submenu->children()[4];
ASSERT_NE(nullptr, buttons_view);
GET_CHILD_BUTTON(button1, buttons_view, 0);
GET_CHILD_BUTTON(button2, buttons_view, 1);
GET_CHILD_BUTTON(button3, buttons_view, 2);
SelectByChar('f');
EXPECT_EQ(4, pending_state_item()->GetCommand());
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_FALSE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_TRUE(button1->IsHotTracked());
EXPECT_FALSE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_TRUE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_FALSE(button2->IsHotTracked());
EXPECT_TRUE(button3->IsHotTracked());
const gfx::Point location = View::ConvertPointToTarget(
button1, submenu, button1->GetLocalBounds().CenterPoint());
ProcessMouseMoved(
submenu, ui::MouseEvent(ui::EventType::kMouseMoved, location, location,
ui::EventTimeForNow(), 0, 0));
EXPECT_EQ(button1, hot_button());
EXPECT_TRUE(button1->IsHotTracked());
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_TRUE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
IncrementSelection();
IncrementSelection();
EXPECT_EQ(SelectionWraps() ? 1 : 5, pending_state_item()->GetCommand());
}
TEST_F(MenuControllerTest, DeleteChildButtonView) {
AddButtonMenuItems(false);
SelectByChar('f');
EXPECT_EQ(4, pending_state_item()->GetCommand());
const View* const buttons_view = menu_item()->GetSubmenu()->children()[4];
ASSERT_NE(nullptr, buttons_view);
GET_CHILD_BUTTON(button1, buttons_view, 0);
GET_CHILD_BUTTON(button2, buttons_view, 1);
GET_CHILD_BUTTON(button3, buttons_view, 2);
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_FALSE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
IncrementSelection();
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_TRUE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
delete button2;
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_TRUE(button1->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
}
TEST_F(MenuControllerTest, ChildButtonHotTrackedAfterMenuItemSelection) {
MenuItemView* const hosting_menu_item =
AddButtonMenuItems(true);
ASSERT_FALSE(hosting_menu_item->IsSelected());
GET_CHILD_BUTTON(button, hosting_menu_item, 0);
EXPECT_FALSE(button->IsHotTracked());
menu_controller()->SelectItemAndOpenSubmenu(hosting_menu_item);
EXPECT_TRUE(hosting_menu_item->IsSelected());
EXPECT_TRUE(button->IsHotTracked());
}
TEST_F(MenuControllerTest, ChildButtonHotTrackedAfterMouseMove) {
const MenuItemView* const hosting_menu_item =
AddButtonMenuItems(true);
GET_CHILD_BUTTON(button, hosting_menu_item, 0);
EXPECT_FALSE(button->IsHotTracked());
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location = View::ConvertPointToTarget(
button, submenu, button->GetLocalBounds().CenterPoint());
ProcessMouseMoved(
submenu, ui::MouseEvent(ui::EventType::kMouseMoved, location, location,
ui::EventTimeForNow(), 0, 0));
EXPECT_EQ(button, hot_button());
EXPECT_TRUE(button->IsHotTracked());
}
TEST_F(MenuControllerTest, ChildButtonHotTrackedWhenNested) {
AddButtonMenuItems(false);
SelectByChar('f');
EXPECT_EQ(4, pending_state_item()->GetCommand());
const View* const buttons_view = menu_item()->GetSubmenu()->children()[4];
ASSERT_NE(nullptr, buttons_view);
GET_CHILD_BUTTON(button1, buttons_view, 0);
GET_CHILD_BUTTON(button2, buttons_view, 1);
GET_CHILD_BUTTON(button3, buttons_view, 2);
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_FALSE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
IncrementSelection();
IncrementSelection();
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_TRUE(button2->IsHotTracked());
EXPECT_FALSE(button3->IsHotTracked());
EXPECT_EQ(button2, hot_button());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_TRUE(button2->IsHotTracked());
EXPECT_EQ(nullptr, hot_button());
SetHotTrackedButton(button1);
EXPECT_TRUE(button1->IsHotTracked());
EXPECT_EQ(button1, hot_button());
button1->SetHotTracked(false);
SetHotTrackedButton(button1);
EXPECT_TRUE(button1->IsHotTracked());
EXPECT_EQ(button1, hot_button());
ExitMenuRun();
EXPECT_FALSE(button1->IsHotTracked());
EXPECT_TRUE(button2->IsHotTracked());
EXPECT_EQ(button2, hot_button());
}
TEST_F(MenuControllerTest, AsynchronousAccept) {
views::test::DisableMenuClosureAnimations();
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
MenuItemView* const accepted = menu_item()->GetSubmenu()->GetMenuItemAt(0);
constexpr int kEventFlags = 42;
Accept(accepted, kEventFlags);
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(accepted, menu_controller_delegate()->on_menu_closed_menu());
EXPECT_EQ(kEventFlags,
menu_controller_delegate()->on_menu_closed_mouse_event_flags());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
menu_controller_delegate()->on_menu_closed_notify_type());
}
TEST_F(MenuControllerTest, AsynchronousCancelAll) {
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
menu_controller()->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(nullptr, menu_controller_delegate()->on_menu_closed_menu());
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_mouse_event_flags());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
menu_controller_delegate()->on_menu_closed_notify_type());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
TEST_F(MenuControllerTest, AsynchronousNestedDelegate) {
auto nested_delegate = std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_delegate.get());
EXPECT_EQ(nested_delegate.get(), current_controller_delegate());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
menu_controller()->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(menu_controller_delegate(), current_controller_delegate());
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(1, nested_delegate->on_menu_closed_called());
EXPECT_EQ(nullptr, nested_delegate->on_menu_closed_menu());
EXPECT_EQ(0, nested_delegate->on_menu_closed_mouse_event_flags());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
nested_delegate->on_menu_closed_notify_type());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
TEST_F(MenuControllerTest, AsynchronousPerformDrop) {
SubmenuView* const source = menu_item()->GetSubmenu();
MenuItemView* const target = source->GetMenuItemAt(0);
SetDropMenuItem(target, MenuDelegate::DropPosition::kAfter);
ui::OSExchangeData drop_data;
gfx::PointF location(target->origin());
const ui::DropTargetEvent target_event(drop_data, location, location,
ui::DragDropTypes::DRAG_MOVE);
DragOperation output_drag_op = DragOperation::kNone;
menu_controller()
->GetDropCallback(source, target_event)
.Run(target_event, output_drag_op,
nullptr);
EXPECT_TRUE(static_cast<test::TestMenuDelegate*>(target->GetDelegate())
->is_drop_performed());
EXPECT_TRUE(showing());
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
}
TEST_F(MenuControllerTest, AsynchronousDragComplete) {
TestDragCompleteThenDestroyOnMenuClosed();
menu_controller()->OnDragWillStart();
menu_controller()->OnDragComplete(true);
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(nullptr, menu_controller_delegate()->on_menu_closed_menu());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
menu_controller_delegate()->on_menu_closed_notify_type());
}
TEST_F(MenuControllerTest, AsynchronousDragCompleteWithoutClose) {
TestDragCompleteThenDestroyOnMenuClosed();
menu_controller()->OnDragWillStart();
menu_controller()->OnDragComplete(false);
EXPECT_EQ(BUILDFLAG(IS_OZONE_X11) ? 1 : 0,
menu_controller_delegate()->on_menu_closed_called());
}
TEST_F(MenuControllerTest, AsynchronousCancelDuringDrag) {
TestDragCompleteThenDestroyOnMenuClosed();
menu_controller()->OnDragWillStart();
menu_controller()->Cancel(MenuController::ExitType::kAll);
menu_controller()->OnDragComplete(true);
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(nullptr, menu_controller_delegate()->on_menu_closed_menu());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
menu_controller_delegate()->on_menu_closed_notify_type());
}
TEST_F(MenuControllerTest, AsynchronousDragHostDeleted) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuHost* const host = menu_host_for_submenu(submenu);
MenuHostOnDragWillStart(host);
submenu->Close();
DestroyMenuItem();
MenuHostOnDragComplete(host);
}
TEST_F(MenuControllerTest, AsyncDropCallback) {
SubmenuView* const source = menu_item()->GetSubmenu();
MenuItemView* const target = source->GetMenuItemAt(0);
SetDropMenuItem(target, MenuDelegate::DropPosition::kAfter);
ui::OSExchangeData drop_data;
gfx::PointF location(target->origin());
const ui::DropTargetEvent target_event(drop_data, location, location,
ui::DragDropTypes::DRAG_MOVE);
auto drop_cb = menu_controller()->GetDropCallback(source, target_event);
const auto* const menu_delegate =
static_cast<test::TestMenuDelegate*>(target->GetDelegate());
EXPECT_FALSE(menu_delegate->is_drop_performed());
EXPECT_TRUE(showing());
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
DragOperation output_drag_op;
std::move(drop_cb).Run(target_event, output_drag_op,
nullptr);
EXPECT_TRUE(menu_delegate->is_drop_performed());
}
TEST_F(MenuControllerForDropTest, AsyncDropCallback) {
SubmenuView* const source = menu_item()->GetSubmenu();
MenuItemView* const target = source->GetMenuItemAt(0);
SetDropMenuItem(target, MenuDelegate::DropPosition::kAfter);
ui::OSExchangeData drop_data;
gfx::PointF location(target->origin());
const ui::DropTargetEvent target_event(drop_data, location, location,
ui::DragDropTypes::DRAG_MOVE);
auto drop_cb = menu_controller()->GetDropCallback(source, target_event);
const auto* const menu_delegate =
static_cast<test::TestMenuDelegate*>(target->GetDelegate());
EXPECT_FALSE(menu_delegate->is_drop_performed());
EXPECT_FALSE(showing());
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
DragOperation output_drag_op;
std::move(drop_cb).Run(target_event, output_drag_op,
nullptr);
EXPECT_TRUE(menu_delegate->is_drop_performed());
}
TEST_F(MenuControllerForDropTest, OnMouseReleasedIgnored) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* const target = submenu->GetMenuItemAt(0);
const gfx::Point press_location = target->bounds().CenterPoint();
ProcessMouseReleased(
submenu, ui::MouseEvent(ui::EventType::kMouseReleased, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_EQ(menu_delegate()->execute_command_id(),
test::TestMenuDelegate::kInvalidExecuteCommandId);
EXPECT_EQ(menu_controller_delegate()->on_menu_closed_called(), 0);
EXPECT_TRUE(showing());
}
TEST_F(MenuControllerTest, HostReceivesInputBeforeDestruction) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->bounds().bottom_right() + gfx::Vector2d(1, 1);
std::unique_ptr<MenuHostRootView> root_view(
CreateMenuHostRootView(menu_host_for_submenu(submenu)));
DestroyMenuController();
root_view->OnMouseMoved(ui::MouseEvent(ui::EventType::kMouseMoved, location,
location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
}
TEST_F(MenuControllerTest, DoubleAsynchronousNested) {
auto nested_delegate = std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_delegate.get());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
menu_controller()->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(1, nested_delegate->on_menu_closed_called());
}
TEST_F(MenuControllerTest, PreserveGestureForOwner) {
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kBottomCenter);
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->GetLocalBounds().bottom_left() + gfx::Vector2d(0, 10);
const ui::GestureEvent event(
location.x(), location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(owner_gesture_count(), 0);
EXPECT_FALSE(menu_controller()->send_gesture_events_to_owner());
ProcessGestureEvent(submenu, event);
EXPECT_EQ(owner_gesture_count(), 0);
menu_controller()->set_send_gesture_events_to_owner(true);
ProcessGestureEvent(submenu, event);
EXPECT_EQ(owner_gesture_count(), 1);
const ui::GestureEvent event2(
location.x(), location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
ProcessGestureEvent(submenu, event2);
EXPECT_EQ(owner_gesture_count(), 2);
ProcessGestureEvent(submenu, event2);
EXPECT_EQ(owner_gesture_count(), 2);
}
#if defined(USE_AURA)
TEST_F(MenuControllerTest, ForwardsEventsToNativeViewForGestures) {
aura::test::EventCountDelegate child_delegate;
auto child_window = std::make_unique<aura::Window>(&child_delegate);
child_window->Init(ui::LAYER_TEXTURED);
owner()->GetNativeView()->AddChild(child_window.get());
menu_controller()->Cancel(MenuController::ExitType::kAll);
menu_controller()->Run(
owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kBottomCenter, ui::mojom::MenuSourceType::kNone,
MenuController::MenuType::kNormal, false, child_window.get());
ShowSubmenu(nullptr, [&](auto& params) {
params.native_view_for_gestures = child_window.get();
});
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->GetLocalBounds().bottom_left() + gfx::Vector2d(0, 10);
const ui::GestureEvent event(
location.x(), location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(0, owner_gesture_count());
EXPECT_EQ(0, child_delegate.GetGestureCountAndReset());
EXPECT_FALSE(menu_controller()->send_gesture_events_to_owner());
ProcessGestureEvent(submenu, event);
EXPECT_EQ(0, owner_gesture_count());
EXPECT_EQ(0, child_delegate.GetGestureCountAndReset());
menu_controller()->set_send_gesture_events_to_owner(true);
ProcessGestureEvent(submenu, event);
EXPECT_EQ(0, owner_gesture_count());
EXPECT_EQ(1, child_delegate.GetGestureCountAndReset());
const ui::GestureEvent event2(
location.x(), location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
ProcessGestureEvent(submenu, event2);
EXPECT_EQ(0, owner_gesture_count());
EXPECT_EQ(1, child_delegate.GetGestureCountAndReset());
ProcessGestureEvent(submenu, event2);
EXPECT_EQ(0, owner_gesture_count());
EXPECT_EQ(0, child_delegate.GetGestureCountAndReset());
}
#endif
TEST_F(MenuControllerTest, NoTouchCloseWhenSendingGesturesToOwner) {
views::test::DisableMenuClosureAnimations();
menu_controller()->set_send_gesture_events_to_owner(true);
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->GetLocalBounds().bottom_right() + gfx::Vector2d(1, 1);
const ui::TouchEvent touch_event(
ui::EventType::kTouchPressed, location, ui::EventTimeForNow(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0));
ProcessTouchEvent(submenu, touch_event);
EXPECT_TRUE(showing());
ProcessGestureEvent(
submenu,
ui::GestureEvent(location.x(), location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureEnd)));
ProcessTouchEvent(submenu, touch_event);
views::test::WaitForMenuClosureAnimation();
EXPECT_FALSE(showing());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
TEST_F(MenuControllerTest, AsynchronousRepostEvent) {
views::test::DisableMenuClosureAnimations();
auto nested_delegate = std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_delegate.get());
EXPECT_EQ(nested_delegate.get(), current_controller_delegate());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->GetLocalBounds().bottom_right() + gfx::Vector2d(1, 1);
SetSelectionOnPointerDown(
submenu,
ui::MouseEvent(ui::EventType::kMousePressed, location, location,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0));
views::test::WaitForMenuClosureAnimation();
EXPECT_EQ(menu_controller_delegate(), current_controller_delegate());
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(1, nested_delegate->on_menu_closed_called());
EXPECT_EQ(nullptr, nested_delegate->on_menu_closed_menu());
EXPECT_EQ(0, nested_delegate->on_menu_closed_mouse_event_flags());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
nested_delegate->on_menu_closed_notify_type());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
TEST_F(MenuControllerTest, AsynchronousTouchEventRepostEvent) {
views::test::DisableMenuClosureAnimations();
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->GetLocalBounds().bottom_right() + gfx::Vector2d(1, 1);
ProcessTouchEvent(
submenu,
ui::TouchEvent(ui::EventType::kTouchPressed, location,
ui::EventTimeForNow(),
ui::PointerDetails(ui::EventPointerType::kTouch, 0)));
views::test::WaitForMenuClosureAnimation();
EXPECT_FALSE(showing());
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(nullptr, menu_controller_delegate()->on_menu_closed_menu());
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_mouse_event_flags());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
menu_controller_delegate()->on_menu_closed_notify_type());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
TEST_F(MenuControllerTest, AsynchronousRepostEventDeletesController) {
views::test::DisableMenuClosureAnimations();
auto nested_delegate = std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_delegate.get());
EXPECT_EQ(nested_delegate.get(), current_controller_delegate());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location =
submenu->GetLocalBounds().bottom_right() + gfx::Vector2d(1, 1);
DestroyMenuControllerOnMenuClosed(nested_delegate.get());
SetSelectionOnPointerDown(
submenu,
ui::MouseEvent(ui::EventType::kMousePressed, location, location,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0));
views::test::WaitForMenuClosureAnimation();
submenu->Close();
EXPECT_EQ(1, nested_delegate->on_menu_closed_called());
}
TEST_F(MenuControllerTest, AsynchronousGestureDeletesController) {
views::test::DisableMenuClosureAnimations();
auto nested_delegate = std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_delegate.get());
EXPECT_EQ(nested_delegate.get(), current_controller_delegate());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const gfx::Point location = submenu->GetMenuItemAt(0)->bounds().CenterPoint();
DestroyMenuControllerOnMenuClosed(nested_delegate.get());
ProcessGestureEvent(
submenu,
ui::GestureEvent(location.x(), location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureTap)));
views::test::WaitForMenuClosureAnimation();
submenu->Close();
EXPECT_EQ(1, nested_delegate->on_menu_closed_called());
}
TEST_F(MenuControllerTest, CalculateMenuBoundsBestFitTest) {
const bool ignore_screen_bounds_for_menus =
ShouldIgnoreScreenBoundsForMenus();
MenuBoundsOptions options;
options.anchor_bounds =
gfx::Rect(options.menu_size.width(), options.menu_size.height(), 0, 0);
options.monitor_bounds =
gfx::Rect(0, 0, options.anchor_bounds.right() + options.menu_size.width(),
options.anchor_bounds.bottom() + options.menu_size.height());
gfx::Rect expected(options.anchor_bounds.x(), options.anchor_bounds.bottom(),
options.menu_size.width(), options.menu_size.height());
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.monitor_bounds.set_height(options.anchor_bounds.bottom());
expected.set_y(
options.anchor_bounds.y() -
(ignore_screen_bounds_for_menus ? 0 : options.menu_size.height()));
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.anchor_bounds.set_y(options.menu_size.height() / 2);
options.monitor_bounds.set_height(options.menu_size.height());
if (ignore_screen_bounds_for_menus) {
expected.set_y(options.anchor_bounds.y());
} else {
expected.set_origin(
{options.anchor_bounds.right(), options.monitor_bounds.y()});
}
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.monitor_bounds.set_width(options.anchor_bounds.right());
if (!ignore_screen_bounds_for_menus) {
expected.set_x(options.anchor_bounds.x() - options.menu_size.width());
}
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.menu_anchor = MenuAnchorPosition::kTopRight;
options.monitor_bounds.set_width(options.anchor_bounds.right() +
options.menu_size.width());
if (ignore_screen_bounds_for_menus) {
expected.set_x(options.anchor_bounds.right() - options.menu_size.width());
}
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.anchor_bounds.set_x(0);
expected.set_x(
options.anchor_bounds.right() -
(ignore_screen_bounds_for_menus ? options.menu_size.width() : 0));
EXPECT_EQ(expected, CalculateMenuBounds(options));
}
TEST_F(MenuControllerTest, CalculateMenuBoundsAnchorTest) {
MenuBoundsOptions options = {.menu_anchor = MenuAnchorPosition::kTopLeft};
gfx::Rect expected(options.anchor_bounds.x(), options.anchor_bounds.bottom(),
options.menu_size.width(), options.menu_size.height());
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.menu_anchor = MenuAnchorPosition::kTopRight;
expected.set_x(options.anchor_bounds.right() - options.menu_size.width());
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.menu_anchor = MenuAnchorPosition::kBottomCenter;
constexpr int kTouchYPadding = 15;
expected.set_origin(
{options.anchor_bounds.x() +
(options.anchor_bounds.width() - options.menu_size.width()) / 2,
options.anchor_bounds.y() - options.menu_size.height() -
kTouchYPadding});
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.anchor_bounds = gfx::Rect(options.menu_size.width(),
options.menu_size.height() / 2, 0, 0);
expected.set_origin(
{options.anchor_bounds.x() +
(options.anchor_bounds.width() - options.menu_size.width()) / 2,
(ShouldIgnoreScreenBoundsForMenus()
? (-options.anchor_bounds.bottom() - kTouchYPadding)
: options.anchor_bounds.y() + kTouchYPadding)});
EXPECT_EQ(expected, CalculateMenuBounds(options));
}
TEST_F(MenuControllerTest, MenuAnchorPositionFlippedInRtl) {
ASSERT_FALSE(base::i18n::IsRTL());
constexpr struct {
MenuAnchorPosition original_position;
MenuAnchorPosition mirrored_position;
} kPositions[] = {
{MenuAnchorPosition::kTopLeft, MenuAnchorPosition::kTopRight},
{MenuAnchorPosition::kBubbleTopLeft, MenuAnchorPosition::kBubbleTopRight},
{MenuAnchorPosition::kBubbleLeft, MenuAnchorPosition::kBubbleRight},
{MenuAnchorPosition::kBubbleBottomLeft,
MenuAnchorPosition::kBubbleBottomRight}};
for (const auto& position : kPositions) {
EXPECT_EQ(position.original_position,
AdjustAnchorPositionForRtl(position.original_position));
EXPECT_EQ(position.mirrored_position,
AdjustAnchorPositionForRtl(position.mirrored_position));
}
base::i18n::SetRTLForTesting(true);
for (const auto& position : kPositions) {
EXPECT_EQ(position.mirrored_position,
AdjustAnchorPositionForRtl(position.original_position));
EXPECT_EQ(position.original_position,
AdjustAnchorPositionForRtl(position.mirrored_position));
}
base::i18n::SetRTLForTesting(false);
}
TEST_F(MenuControllerTest, CalculateMenuBoundsMonitorFitTest) {
constexpr gfx::Rect kMonitorBounds(0, 0, 100, 100);
MenuBoundsOptions options = {
.anchor_bounds = gfx::Rect(),
.monitor_bounds = kMonitorBounds,
.menu_size =
gfx::Size(kMonitorBounds.width() / 2, kMonitorBounds.height() * 2)};
gfx::Rect expected(options.anchor_bounds.x(), options.anchor_bounds.bottom(),
options.menu_size.width(), kMonitorBounds.height());
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.menu_size =
gfx::Size(kMonitorBounds.width() * 2, kMonitorBounds.height() / 2);
expected.set_size({kMonitorBounds.width(), options.menu_size.height()});
EXPECT_EQ(expected, CalculateMenuBounds(options));
options.menu_size =
gfx::Size(kMonitorBounds.width() * 2, kMonitorBounds.height() * 2);
expected.set_size(kMonitorBounds.size());
EXPECT_EQ(expected, CalculateMenuBounds(options));
}
TEST_P(MenuControllerTest, TestMenuFitsOnScreen) {
constexpr int kDisplaySize = 500;
constexpr int kCoords[] = {-kDisplaySize, 0, kDisplaySize};
for (int x : kCoords) {
for (int y : kCoords) {
const gfx::Rect monitor_bounds(x, y, kDisplaySize, kDisplaySize);
for (auto position : kBubblePositions) {
TestMenuFitsOnScreen(position, monitor_bounds);
}
}
}
}
TEST_P(MenuControllerTest, TestMenuFitsOnScreenSmallAnchor) {
constexpr int kDisplaySize = 500;
constexpr int kCoords[] = {-kDisplaySize, 0, kDisplaySize};
for (int x : kCoords) {
for (int y : kCoords) {
const gfx::Rect monitor_bounds(x, y, kDisplaySize, kDisplaySize);
for (auto position : kBubblePositions) {
TestMenuFitsOnScreenSmallAnchor(position, monitor_bounds);
}
}
}
}
TEST_P(MenuControllerTest, TestMenuFitsOnSmallScreen) {
constexpr int kDisplaySize = 500;
constexpr int kCoords[] = {-kDisplaySize, 0, kDisplaySize};
for (int x : kCoords) {
for (int y : kCoords) {
const gfx::Rect monitor_bounds(x, y, kDisplaySize, kDisplaySize);
for (auto position : kBubblePositions) {
TestMenuFitsOnSmallScreen(position, monitor_bounds);
}
}
}
}
TEST_P(MenuControllerTest, TestSubmenuFitsOnScreen) {
menu_controller()->set_use_ash_system_ui_layout(true);
SubmenuView* const submenu = menu_item()->GetSubmenu();
const std::vector<MenuItemView*> menu_items = submenu->GetMenuItems();
std::ranges::for_each(base::span(menu_items).subspan<1>(),
[&](auto* item) { menu_item()->RemoveMenuItem(item); });
MenuItemView* const sub_item = submenu->GetMenuItemAt(0);
sub_item->AppendMenuItem(11, u"Subitem.One");
const int menu_width = MenuConfig::instance().touchable_menu_min_width;
const gfx::Size parent_size(menu_width, menu_width);
const gfx::Size parent_size_wide(menu_width * 2, menu_width);
const int display_width = parent_size.width() * 3;
const int display_height = parent_size.height() * 3;
for (auto menu_position : {MenuAnchorPosition::kBubbleTopLeft,
MenuAnchorPosition::kBubbleTopRight}) {
for (int x : {-display_width, 0, display_width}) {
for (int y : {-display_height, 0, display_height}) {
const gfx::Rect monitor_bounds(x, y, display_width, display_height);
const int x_min = monitor_bounds.x();
const int x_max = monitor_bounds.right() - parent_size.width();
const int y_min = monitor_bounds.y();
const int y_max = monitor_bounds.bottom() - parent_size.height();
for (const auto& origin :
{gfx::Point(x_min, y_min), gfx::Point(x_max, y_min),
gfx::Point((x_min + x_max) / 2, y_min),
gfx::Point(x_min, (y_min + y_max) / 2),
gfx::Point(x_min, y_max)}) {
TestSubmenuFitsOnScreen(sub_item, monitor_bounds,
gfx::Rect(origin, parent_size),
menu_position);
}
TestSubmenuFitsOnScreen(
sub_item, monitor_bounds,
gfx::Rect(gfx::Point(x_min + (x_max - x_min) / 4, y_min),
parent_size_wide),
menu_position);
}
}
}
}
TEST_F(MenuControllerTest, GrowingMenuMovesLaterallyNotVertically) {
if (ShouldIgnoreScreenBoundsForMenus()) {
return;
}
MenuBoundsOptions options = {
.anchor_bounds = gfx::Rect(80, 70, 15, 10),
.monitor_bounds = gfx::Rect(0, 0, 100, 100),
.menu_size = gfx::Size(20, 20),
};
EXPECT_EQ(gfx::Rect(80, 80, 20, 20), CalculateMenuBounds(options));
EXPECT_EQ(MenuItemView::MenuPosition::kBelowBounds,
menu_item_actual_position());
options.menu_size = gfx::Size(50, 50);
options.menu_position = MenuItemView::MenuPosition::kBelowBounds;
EXPECT_EQ(gfx::Rect(50, 80, 50, 20), CalculateMenuBounds(options));
}
#if defined(USE_AURA)
TEST_F(MenuControllerTest, MouseAtMenuItemOnShow) {
auto menu_item = std::make_unique<MenuItemView>(menu_delegate());
const MenuItemView* const first_item = menu_item->AppendMenuItem(1, u"One");
menu_item->AppendMenuItem(2, u"Two");
menu_item->set_controller(menu_controller());
const gfx::Size item_size = first_item->CalculatePreferredSize({});
gfx::Point location(item_size.width() / 2, item_size.height() / 2);
GetRootWindow(owner())->MoveCursorTo(location);
menu_controller()->Run(owner(), nullptr, menu_item.get(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(0, pending_state_item()->GetCommand());
SubmenuView* const submenu = menu_item->GetSubmenu();
View::ConvertPointFromScreen(submenu, &location);
ProcessMouseMoved(
submenu, ui::MouseEvent(ui::EventType::kMouseMoved, location, location,
ui::EventTimeForNow(), 0, 0));
EXPECT_EQ(0, pending_state_item()->GetCommand());
location.Offset(0, 1);
ProcessMouseMoved(
submenu, ui::MouseEvent(ui::EventType::kMouseMoved, location, location,
ui::EventTimeForNow(), 0, 0));
EXPECT_EQ(1, pending_state_item()->GetCommand());
}
TEST_F(MenuControllerTest, AsynchronousCancelEvent) {
ExitMenuRun();
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(MenuController::ExitType::kNone, menu_controller()->exit_type());
ui::CancelModeEvent cancel_event;
event_generator()->Dispatch(&cancel_event);
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
TEST_F(MenuControllerTest, WidgetStateChangeCancelsMenu) {
ExitMenuRun();
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_TRUE(showing());
EXPECT_EQ(MenuController::ExitType::kNone, menu_controller()->exit_type());
owner()->SetFullscreen(true);
EXPECT_FALSE(showing());
EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type());
}
#if BUILDFLAG(ENABLE_DESKTOP_AURA) && !BUILDFLAG(IS_OZONE_WAYLAND)
class DesktopMenuControllerTest : public MenuControllerTest {
public:
void SetUp() override {
set_native_widget_type(NativeWidgetType::kDesktop);
MenuControllerTest::SetUp();
}
};
TEST_F(DesktopMenuControllerTest, RunWithoutWidgetDoesntCrash) {
ExitMenuRun();
menu_controller()->Run(nullptr, nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
}
#endif
TEST_F(MenuControllerTest, MenuControllerReplacedDuringDrag) {
AddButtonMenuItems(false);
TestDragDropClient drag_drop_client(base::BindRepeating(
&MenuControllerTest::TestMenuControllerReplacementDuringDrag,
base::Unretained(this)));
aura::client::SetDragDropClient(
GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client);
StartDrag();
}
TEST_F(MenuControllerTest, CancelAllDuringDrag) {
AddButtonMenuItems(false);
TestDragDropClient drag_drop_client(base::BindRepeating(
&MenuControllerTest::TestCancelAllDuringDrag, base::Unretained(this)));
aura::client::SetDragDropClient(
GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client);
StartDrag();
}
TEST_F(MenuControllerTest, RestoreCaptureAfterDrag) {
AddButtonMenuItems(false);
menu_delegate()->set_should_close_on_drag_complete(false);
SubmenuView* const base_submenu = menu_item()->GetSubmenu();
MenuHost* const base_host = menu_host_for_submenu(base_submenu);
base_host->SetCapture(base_submenu);
TestDragDropClient drag_drop_client(base::DoNothing());
aura::client::SetDragDropClient(
GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client);
EXPECT_TRUE(base_host->HasCapture());
StartDrag();
EXPECT_NE(base_host->HasCapture(), BUILDFLAG(IS_OZONE_X11));
}
TEST_F(MenuControllerTest, DontRestoreCaptureOnHiddenHostAfterDrag) {
AddButtonMenuItems(false);
menu_delegate()->set_should_close_on_drag_complete(true);
SubmenuView* const base_submenu = menu_item()->GetSubmenu();
MenuHost* const base_host = menu_host_for_submenu(base_submenu);
base_host->SetCapture(base_submenu);
TestDragDropClient drag_drop_client(base::DoNothing());
aura::client::SetDragDropClient(
GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client);
EXPECT_TRUE(base_host->HasCapture());
StartDrag();
EXPECT_FALSE(base_host->HasCapture());
}
TEST_F(MenuControllerTest, HostWithoutCaptureAfterDrag) {
AddButtonMenuItems(false);
menu_delegate()->set_should_close_on_drag_complete(false);
SubmenuView* const base_submenu = menu_item()->GetSubmenu();
MenuHost* const base_host = menu_host_for_submenu(base_submenu);
base_host->ReleaseCapture();
TestDragDropClient drag_drop_client(base::DoNothing());
aura::client::SetDragDropClient(
GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client);
EXPECT_FALSE(base_host->HasCapture());
StartDrag();
EXPECT_FALSE(base_host->HasCapture());
}
TEST_F(MenuControllerTest, DestroyedDuringViewsRelease) {
ExitMenuRun();
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
TestDestroyedDuringViewsRelease();
}
TEST_F(MenuControllerTest, RepostEventToEmptyMenuItem) {
ShowSubmenu();
SubmenuView* const base_submenu = menu_item()->GetSubmenu();
menu_host_for_submenu(base_submenu)
->SetContentsView(base_submenu->GetScrollViewContainer());
MenuItemView* submenu_item = menu_item()->AppendSubMenu(0, std::u16string());
submenu_item->UpdateEmptyMenusAndMetrics();
SubmenuView* const submenu_view = submenu_item->GetSubmenu();
const auto insets = submenu_view->GetScrollViewContainer()->GetInsets();
const gfx::Rect bounds(0, 50, 50 + insets.width(), 50 + insets.height());
ShowSubmenu(submenu_view, [&](auto& params) { params.bounds = bounds; });
menu_host_for_submenu(submenu_view)
->SetContentsView(submenu_view->GetScrollViewContainer());
SetPendingStateItem(submenu_item);
auto nested_menu_delegate_1 = std::make_unique<test::TestMenuDelegate>();
auto nested_menu_item_1 =
std::make_unique<MenuItemView>(nested_menu_delegate_1.get());
nested_menu_item_1->set_controller(menu_controller());
SubmenuView* const nested_menu_submenu = nested_menu_item_1->CreateSubmenu();
ShowSubmenu(nested_menu_submenu);
menu_host_for_submenu(nested_menu_submenu)
->SetContentsView(nested_menu_submenu->GetScrollViewContainer());
auto nested_controller_delegate_1 =
std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_controller_delegate_1.get());
menu_controller()->Run(
owner(), nullptr, nested_menu_item_1.get(), gfx::Rect(150, 50, 100, 100),
MenuAnchorPosition::kTopLeft, ui::mojom::MenuSourceType::kNone,
MenuController::MenuType::kContextMenu);
gfx::Point press_location = submenu_view->GetLocalBounds().CenterPoint();
const gfx::Point press_location_for_nested_menu =
View::ConvertPointFromScreen(
nested_menu_submenu,
View::ConvertPointToScreen(submenu_view, press_location));
ProcessMousePressed(
nested_menu_submenu,
ui::MouseEvent(ui::EventType::kMousePressed,
press_location_for_nested_menu,
press_location_for_nested_menu, ui::EventTimeForNow(),
ui::EF_RIGHT_MOUSE_BUTTON, 0));
EXPECT_EQ(nested_controller_delegate_1->on_menu_closed_called(), 1);
EXPECT_EQ(menu_controller_delegate(), current_controller_delegate());
SetState(submenu_item);
press_location.Offset(-5, 0);
ProcessMouseDragged(
submenu_view, ui::MouseEvent(ui::EventType::kMouseDragged, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_RIGHT_MOUSE_BUTTON, 0));
ASSERT_EQ(menu_delegate()->will_hide_menu_count(), 0);
ProcessMouseReleased(
submenu_view,
ui::MouseEvent(ui::EventType::kMouseReleased, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_RIGHT_MOUSE_BUTTON, 0));
EXPECT_EQ(menu_delegate()->show_context_menu_count(), 1);
EXPECT_EQ(menu_delegate()->show_context_menu_source(), submenu_item);
auto nested_menu_delegate_2 = std::make_unique<test::TestMenuDelegate>();
auto nested_menu_item_2 =
std::make_unique<MenuItemView>(nested_menu_delegate_2.get());
nested_menu_item_2->set_controller(menu_controller());
auto nested_controller_delegate_2 =
std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(nested_controller_delegate_2.get());
menu_controller()->Run(
owner(), nullptr, nested_menu_item_2.get(), gfx::Rect(150, 50, 100, 100),
MenuAnchorPosition::kTopLeft, ui::mojom::MenuSourceType::kNone,
MenuController::MenuType::kContextMenu);
TestAsyncEscapeKey();
EXPECT_EQ(nested_controller_delegate_2->on_menu_closed_called(), 1);
EXPECT_EQ(menu_controller_delegate(), current_controller_delegate());
}
#if BUILDFLAG(IS_OZONE)
TEST_F(MenuControllerTest,
TopLevelBookmarkFolderContextMenuShouldNotDismissFolder) {
using SupportsForTest =
ui::OzonePlatform::PlatformProperties::SupportsForTest;
base::AutoReset<SupportsForTest> auto_reset(
&ui::OzonePlatform::PlatformProperties::
override_set_parent_for_non_top_level_windows_for_test,
SupportsForTest::kYes);
MenuItemView* root_item = menu_item();
ASSERT_EQ(nullptr, root_item->GetParentMenuItem());
SubmenuView* const root_submenu = root_item->GetSubmenu();
const auto insets = root_submenu->GetScrollViewContainer()->GetInsets();
const gfx::Rect bounds(0, 50, 80 + insets.width(), 40 + insets.height());
ShowSubmenu(root_submenu, [&](auto& params) { params.bounds = bounds; });
menu_host_for_submenu(root_submenu)
->SetContentsView(root_submenu->GetScrollViewContainer());
auto context_menu = std::make_unique<MenuItemView>(menu_delegate());
context_menu->AppendMenuItem(1, u"Action");
context_menu->set_controller(menu_controller());
auto context_delegate = std::make_unique<TestMenuControllerDelegate>();
menu_controller()->AddNestedDelegate(context_delegate.get());
SetState(root_item);
menu_controller()->Run(
owner(), nullptr, context_menu.get(), gfx::Rect(100, 100, 80, 60),
MenuAnchorPosition::kTopLeft, ui::mojom::MenuSourceType::kMouse,
MenuController::MenuType::kMenuItemContextMenu);
EXPECT_TRUE(menu_controller()->IsContextMenu());
EXPECT_TRUE(root_item->SubmenuIsShowing());
EXPECT_TRUE(context_menu->SubmenuIsShowing());
gfx::Rect submenu_bounds = context_menu->GetSubmenu()->GetBoundsInScreen();
gfx::Point outside_point =
submenu_bounds.bottom_right() + gfx::Vector2d(60, 60);
ProcessMousePressed(
context_menu->GetSubmenu(),
ui::MouseEvent(ui::EventType::kMousePressed, outside_point, outside_point,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_FALSE(context_menu->SubmenuIsShowing());
EXPECT_FALSE(root_item->SubmenuIsShowing());
}
#endif
TEST_F(MenuControllerTest, DragFromViewIntoMenuAndExit) {
auto drag_view = std::make_unique<View>();
drag_view->SetBounds(0, 500, 100, 100);
const gfx::Point press_location = drag_view->GetLocalBounds().CenterPoint();
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const first_item = submenu->GetMenuItemAt(0);
const gfx::Point drag_location = first_item->bounds().CenterPoint();
drag_view->OnMousePressed(ui::MouseEvent(
ui::EventType::kMousePressed, press_location, press_location,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0));
ProcessMouseDragged(
submenu,
ui::MouseEvent(ui::EventType::kMouseDragged, drag_location, drag_location,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_TRUE(first_item->IsSelected());
constexpr gfx::Point kReleaseLocation(200, 50);
ProcessMouseDragged(
submenu, ui::MouseEvent(ui::EventType::kMouseDragged, kReleaseLocation,
kReleaseLocation, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_FALSE(first_item->IsSelected());
ProcessMouseReleased(
submenu, ui::MouseEvent(ui::EventType::kMouseReleased, kReleaseLocation,
kReleaseLocation, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
}
TEST_F(MenuControllerTest, AuraWindowIsInitializedWithMenuHostInitParams) {
constexpr gfx::Rect kAnchorRect(1, 5, 2, 5);
ShowSubmenu(nullptr, [anchor_rect = kAnchorRect](auto& params) {
params.owned_window_anchor.anchor_rect = anchor_rect;
});
auto* property =
menu_item()->GetSubmenu()->GetWidget()->GetNativeWindow()->GetProperty(
aura::client::kOwnedWindowAnchor);
ASSERT_TRUE(property);
EXPECT_EQ(kAnchorRect, property->anchor_rect);
}
TEST_F(MenuControllerTest, ContextMenuInitializesAuraWindowWhenShown) {
MenuBoundsOptions options = {.menu_anchor = MenuAnchorPosition::kTopLeft};
SetUpMenuControllerForCalculateBounds(options, menu_item());
menu_controller()->Run(owner(), nullptr, menu_item(), options.anchor_bounds,
options.menu_anchor, ui::mojom::MenuSourceType::kNone,
MenuController::MenuType::kContextMenu);
SubmenuView* const submenu = menu_item()->GetSubmenu();
const aura::Window* window = submenu->GetWidget()->GetNativeWindow();
const ui::OwnedWindowAnchor* anchor =
window->GetProperty(aura::client::kOwnedWindowAnchor);
EXPECT_TRUE(anchor);
EXPECT_EQ(ui::OwnedWindowAnchorPosition::kBottomLeft,
anchor->anchor_position);
EXPECT_EQ(ui::OwnedWindowAnchorGravity::kBottomRight, anchor->anchor_gravity);
EXPECT_EQ((ui::OwnedWindowConstraintAdjustment::kAdjustmentSlideX |
ui::OwnedWindowConstraintAdjustment::kAdjustmentFlipY |
ui::OwnedWindowConstraintAdjustment::kAdjustmentRezizeY),
anchor->constraint_adjustment);
EXPECT_EQ(CalculateExpectedMenuAnchorRect(menu_item()), anchor->anchor_rect);
MenuItemView* const child_menu = submenu->GetMenuItemAt(0);
child_menu->CreateSubmenu();
ASSERT_NE(nullptr, child_menu->GetParentMenuItem());
options.menu_anchor = MenuAnchorPosition::kTopRight;
SetUpMenuControllerForCalculateBounds(options, child_menu);
menu_controller()->Run(owner(), nullptr, child_menu,
child_menu->GetBoundsInScreen(), options.menu_anchor);
ASSERT_NE(nullptr, child_menu->GetWidget());
window = child_menu->GetSubmenu()->GetWidget()->GetNativeWindow();
anchor = window->GetProperty(aura::client::kOwnedWindowAnchor);
EXPECT_TRUE(anchor);
EXPECT_EQ(ui::OwnedWindowAnchorPosition::kTopRight, anchor->anchor_position);
EXPECT_EQ(ui::OwnedWindowAnchorGravity::kBottomRight, anchor->anchor_gravity);
EXPECT_EQ((ui::OwnedWindowConstraintAdjustment::kAdjustmentSlideY |
ui::OwnedWindowConstraintAdjustment::kAdjustmentFlipX |
ui::OwnedWindowConstraintAdjustment::kAdjustmentResizeX |
ui::OwnedWindowConstraintAdjustment::kAdjustmentRezizeY),
anchor->constraint_adjustment);
EXPECT_EQ(CalculateExpectedMenuAnchorRect(child_menu), anchor->anchor_rect);
}
TEST_F(MenuControllerTest, RootAndChildMenusInitializeAuraWindowWhenShown) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuBoundsOptions options = {
.menu_size = GetPreferredSizeForSubmenu(*submenu),
.menu_anchor = MenuAnchorPosition::kTopLeft};
SetUpMenuControllerForCalculateBounds(options, menu_item());
menu_controller()->Run(owner(), nullptr, menu_item(), options.anchor_bounds,
options.menu_anchor);
const aura::Window* window = submenu->GetWidget()->GetNativeWindow();
const ui::OwnedWindowAnchor* anchor =
window->GetProperty(aura::client::kOwnedWindowAnchor);
EXPECT_TRUE(anchor);
EXPECT_EQ(ui::OwnedWindowAnchorPosition::kBottomLeft,
anchor->anchor_position);
EXPECT_EQ(ui::OwnedWindowAnchorGravity::kBottomRight, anchor->anchor_gravity);
EXPECT_EQ((ui::OwnedWindowConstraintAdjustment::kAdjustmentSlideX |
ui::OwnedWindowConstraintAdjustment::kAdjustmentFlipY |
ui::OwnedWindowConstraintAdjustment::kAdjustmentRezizeY),
anchor->constraint_adjustment);
EXPECT_EQ(CalculateExpectedMenuAnchorRect(menu_item()), anchor->anchor_rect);
MenuItemView* const child_item = submenu->GetMenuItemAt(0);
child_item->AppendMenuItem(1, u"Child one");
SubmenuView* const child_submenu = child_item->GetSubmenu();
ASSERT_NE(nullptr, child_item->GetParentMenuItem());
options.menu_size = GetPreferredSizeForSubmenu(*child_submenu);
options.menu_anchor = MenuAnchorPosition::kTopRight;
SetUpMenuControllerForCalculateBounds(options, child_item);
menu_controller()->Run(owner(), nullptr, child_item,
child_item->GetBoundsInScreen(), options.menu_anchor);
ASSERT_NE(nullptr, child_item->GetWidget());
window = child_submenu->GetWidget()->GetNativeWindow();
anchor = window->GetProperty(aura::client::kOwnedWindowAnchor);
EXPECT_TRUE(anchor);
EXPECT_EQ(ui::OwnedWindowAnchorPosition::kTopRight, anchor->anchor_position);
EXPECT_EQ(ui::OwnedWindowAnchorGravity::kBottomRight, anchor->anchor_gravity);
EXPECT_EQ((ui::OwnedWindowConstraintAdjustment::kAdjustmentSlideY |
ui::OwnedWindowConstraintAdjustment::kAdjustmentFlipX |
ui::OwnedWindowConstraintAdjustment::kAdjustmentResizeX |
ui::OwnedWindowConstraintAdjustment::kAdjustmentRezizeY),
anchor->constraint_adjustment);
const auto anchor_rect = anchor->anchor_rect;
EXPECT_EQ(CalculateExpectedMenuAnchorRect(child_item), anchor->anchor_rect);
child_item->SetY(child_item->y() + 2);
menu_controller()->Run(owner(), nullptr, child_item,
child_item->GetBoundsInScreen(),
MenuAnchorPosition::kTopLeft);
MenuChildrenChanged(child_item);
EXPECT_EQ(CalculateExpectedMenuAnchorRect(child_item), anchor->anchor_rect);
EXPECT_NE(anchor->anchor_rect, anchor_rect);
}
TEST_F(MenuControllerTest, RegisterClickWithChildViews) {
DestroyMenuControllerOnMenuClosed(menu_controller_delegate());
ShowSubmenu();
SubmenuView* submenu = menu_item()->GetSubmenu();
MenuItemView* first_menu_item = submenu->GetMenuItemAt(0);
first_menu_item->AddChildView(std::make_unique<View>());
const gfx::Point press_location = first_menu_item->bounds().CenterPoint();
ProcessMousePressed(
submenu, ui::MouseEvent(ui::EventType::kMousePressed, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
ProcessMouseReleased(
submenu, ui::MouseEvent(ui::EventType::kMouseReleased, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_EQ(menu_controller_delegate()->on_menu_closed_called(), 0);
first_menu_item->SetTriggerActionWithNonIconChildViews(true);
ProcessMousePressed(
submenu, ui::MouseEvent(ui::EventType::kMousePressed, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
ProcessMouseReleased(
submenu, ui::MouseEvent(ui::EventType::kMouseReleased, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_EQ(menu_controller_delegate()->on_menu_closed_called(), 1);
DestroyMenuControllerDelegate();
}
#endif
TEST_F(MenuControllerTest, NoUseAfterFreeWhenMenuCanceledOnMousePress) {
DestroyMenuControllerOnMenuClosed(menu_controller_delegate());
auto item = std::make_unique<MenuItemView>(menu_delegate());
item->set_controller(menu_controller());
item->SetBounds(0, 0, 50, 50);
SubmenuView* const submenu = item->CreateSubmenu();
auto* const canceling_view =
submenu->AddChildView(std::make_unique<CancelMenuOnMousePressView>(
menu_controller()->AsWeakPtr()));
canceling_view->SetBoundsRect(item->GetLocalBounds());
menu_controller()->Run(owner(), nullptr, item.get(), item->bounds(),
MenuAnchorPosition::kTopLeft);
ShowSubmenu(submenu);
const gfx::Point location = canceling_view->bounds().CenterPoint();
EXPECT_TRUE(ProcessMousePressed(
submenu,
ui::MouseEvent(ui::EventType::kMousePressed, location, location,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0)));
submenu->Close();
}
TEST_F(MenuControllerTest, SetSelectionIndices_MenuItemsOnly) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* const item1 = submenu->GetMenuItemAt(0);
MenuItemView* const item2 = submenu->GetMenuItemAt(1);
MenuItemView* const item3 = submenu->GetMenuItemAt(2);
MenuItemView* const item4 = submenu->GetMenuItemAt(3);
OpenMenu(menu_item());
ui::AXNodeData data;
item1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(1, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(3, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item4->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
TEST_F(MenuControllerTest,
SetSelectionIndices_MenuItemsOnly_SkipHiddenAndDisabled) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* const item1 = submenu->GetMenuItemAt(0);
item1->SetEnabled(false);
const MenuItemView* const item2 = submenu->GetMenuItemAt(1);
MenuItemView* const item3 = submenu->GetMenuItemAt(2);
item3->SetVisible(false);
const MenuItemView* const item4 = submenu->GetMenuItemAt(3);
OpenMenu(menu_item());
ui::AXNodeData data;
item2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(1, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item4->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
TEST_F(MenuControllerTest, SetSelectionIndices_Buttons) {
AddButtonMenuItems(false);
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const item1 = submenu->GetMenuItemAt(0);
const MenuItemView* const item2 = submenu->GetMenuItemAt(1);
const MenuItemView* const item3 = submenu->GetMenuItemAt(2);
const MenuItemView* const item4 = submenu->GetMenuItemAt(3);
const MenuItemView* const item5 = submenu->GetMenuItemAt(4);
GET_CHILD_BUTTON(button1, item5, 0);
GET_CHILD_BUTTON(button2, item5, 1);
GET_CHILD_BUTTON(button3, item5, 2);
OpenMenu(menu_item());
ui::AXNodeData data;
item1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(1, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(3, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item4->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
button1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
button2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(6, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
button3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(7, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
TEST_F(MenuControllerTest, SetSelectionIndices_Buttons_SkipHiddenAndDisabled) {
AddButtonMenuItems(false);
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const item1 = submenu->GetMenuItemAt(0);
const MenuItemView* const item2 = submenu->GetMenuItemAt(1);
const MenuItemView* const item3 = submenu->GetMenuItemAt(2);
const MenuItemView* const item4 = submenu->GetMenuItemAt(3);
const MenuItemView* const item5 = submenu->GetMenuItemAt(4);
GET_CHILD_BUTTON(button1, item5, 0);
GET_CHILD_BUTTON(button2, item5, 1);
GET_CHILD_BUTTON(button3, item5, 2);
button1->SetEnabled(false);
button2->SetVisible(false);
OpenMenu(menu_item());
ui::AXNodeData data;
item1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(1, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(3, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item4->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
button3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
TEST_F(MenuControllerTest, SetSelectionIndices_NestedButtons) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const item1 = submenu->GetMenuItemAt(0);
const MenuItemView* const item2 = submenu->GetMenuItemAt(1);
const MenuItemView* const item3 = submenu->GetMenuItemAt(2);
MenuItemView* const item4 = submenu->GetMenuItemAt(3);
auto* const container_view = item4->AddChildView(std::make_unique<View>());
container_view->GetViewAccessibility().SetRole(ax::mojom::Role::kMenu);
container_view->AddChildView(std::make_unique<Label>());
auto* const button1 =
container_view->AddChildView(std::make_unique<LabelButton>());
button1->SetFocusBehavior(View::FocusBehavior::ALWAYS);
button1->GetViewAccessibility().SetRole(ax::mojom::Role::kMenuItem);
auto* const button2 =
container_view->AddChildView(std::make_unique<LabelButton>());
button2->GetViewAccessibility().SetRole(ax::mojom::Role::kMenuItem);
button2->SetFocusBehavior(View::FocusBehavior::ALWAYS);
OpenMenu(menu_item());
ui::AXNodeData data;
item1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(1, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(3, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
data = ui::AXNodeData();
button1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
data = ui::AXNodeData();
button2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(5, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
TEST_F(MenuControllerTest, AccessibleProperties) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuScrollViewContainer* scroll_view_container =
submenu->GetScrollViewContainer();
ui::AXNodeData data;
scroll_view_container->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.role, ax::mojom::Role::kMenuBar);
}
TEST_F(MenuControllerTest, SetSelectionIndices_ChildrenChanged) {
AddButtonMenuItems(false);
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* const item1 = submenu->GetMenuItemAt(0);
MenuItemView* const item2 = submenu->GetMenuItemAt(1);
const MenuItemView* const item3 = submenu->GetMenuItemAt(2);
const MenuItemView* const item4 = submenu->GetMenuItemAt(3);
const MenuItemView* const item5 = submenu->GetMenuItemAt(4);
GET_CHILD_BUTTON(button1, item5, 0);
GET_CHILD_BUTTON(button2, item5, 1);
GET_CHILD_BUTTON(button3, item5, 2);
OpenMenu(menu_item());
const auto expect_coordinates = [](const View* v, std::optional<int> pos,
std::optional<int> size) {
ui::AXNodeData data;
v->GetViewAccessibility().GetAccessibleNodeData(&data);
const auto check_attribute = [&](const auto& expected, auto attribute) {
EXPECT_EQ(expected.has_value(), data.HasIntAttribute(attribute));
if (expected.has_value()) {
EXPECT_EQ(expected.value(), data.GetIntAttribute(attribute));
}
};
check_attribute(pos, ax::mojom::IntAttribute::kPosInSet);
check_attribute(size, ax::mojom::IntAttribute::kSetSize);
};
expect_coordinates(item1, 1, 7);
expect_coordinates(item2, 2, 7);
expect_coordinates(item3, 3, 7);
expect_coordinates(item4, 4, 7);
expect_coordinates(button1, 5, 7);
expect_coordinates(button2, 6, 7);
expect_coordinates(button3, 7, 7);
item1->SetEnabled(false);
button1->SetEnabled(false);
const MenuItemView* const item6 = menu_item()->AppendMenuItem(6, u"Six");
menu_item()->RemoveMenuItem(item2);
MenuChildrenChanged(menu_item());
expect_coordinates(item1, std::nullopt, std::nullopt);
expect_coordinates(button1, std::nullopt, std::nullopt);
expect_coordinates(item3, 1, 5);
expect_coordinates(item4, 2, 5);
expect_coordinates(button2, 3, 5);
expect_coordinates(button3, 4, 5);
expect_coordinates(item6, 5, 5);
}
TEST_F(MenuControllerTest, AccessibilityDoDefaultCallsAccept) {
views::test::DisableMenuClosureAnimations();
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
MenuItemView* const accepted = menu_item()->GetSubmenu()->GetMenuItemAt(0);
ui::AXActionData data;
data.action = ax::mojom::Action::kDoDefault;
accepted->HandleAccessibleAction(data);
views::test::WaitForMenuClosureAnimation();
EXPECT_EQ(1, menu_controller_delegate()->on_menu_closed_called());
EXPECT_EQ(accepted, menu_controller_delegate()->on_menu_closed_menu());
EXPECT_EQ(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
menu_controller_delegate()->on_menu_closed_notify_type());
}
TEST_F(MenuControllerTest, AccessibilityEmitsSelectChildrenChanged) {
const test::AXEventCounter ax_counter(views::AXUpdateNotifier::Get());
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kSelectedChildrenChanged), 0);
DispatchKey(ui::VKEY_DOWN);
EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kSelectedChildrenChanged), 1);
DispatchKey(ui::VKEY_DOWN);
EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kSelectedChildrenChanged), 2);
}
TEST_F(MenuControllerTest, AccessibilityEmitsMenuOpenedClosedEvents) {
const test::AXEventCounter ax_counter(views::AXUpdateNotifier::Get());
EXPECT_EQ(0, ax_counter.GetCount(ax::mojom::Event::kMenuStart));
EXPECT_EQ(0, ax_counter.GetCount(ax::mojom::Event::kMenuEnd));
EXPECT_EQ(0, ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart));
EXPECT_EQ(0, ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd));
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
EXPECT_EQ(1, ax_counter.GetCount(ax::mojom::Event::kMenuStart));
EXPECT_EQ(0, ax_counter.GetCount(ax::mojom::Event::kMenuEnd));
EXPECT_EQ(1, ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart));
EXPECT_EQ(0, ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd));
menu_controller()->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(1, ax_counter.GetCount(ax::mojom::Event::kMenuStart));
EXPECT_EQ(1, ax_counter.GetCount(ax::mojom::Event::kMenuEnd));
EXPECT_EQ(1, ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart));
EXPECT_EQ(1, ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd));
}
TEST_F(MenuControllerTest, AccessibilityDisabledItemsIndices) {
const ::ui::ScopedAXModeSetter ax_mode_setter(ui::AXMode::kNativeAPIs);
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const item1 = submenu->GetMenuItemAt(0);
MenuItemView* const item2 = submenu->GetMenuItemAt(1);
const MenuItemView* const item3 = submenu->GetMenuItemAt(2);
const MenuItemView* const item4 = submenu->GetMenuItemAt(3);
item2->SetEnabled(false);
OpenMenu(menu_item());
ui::AXNodeData data;
item1->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(1, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item2->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item3->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(3, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
item4->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(4, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
#if BUILDFLAG(IS_MAC)
TEST_F(MenuControllerTest, BrowserHotkeysCancelMenusAndAreRedispatched) {
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
int options = ui::EF_COMMAND_DOWN;
ui::KeyEvent press_cmd(ui::EventType::kKeyPressed, ui::VKEY_COMMAND, options);
menu_controller()->OnWillDispatchKeyEvent(&press_cmd);
EXPECT_TRUE(showing());
options |= ui::EF_CONTROL_DOWN;
ui::KeyEvent press_ctrl(ui::EventType::kKeyPressed, ui::VKEY_CONTROL,
options);
menu_controller()->OnWillDispatchKeyEvent(&press_ctrl);
EXPECT_TRUE(showing());
ui::KeyEvent press_f(ui::EventType::kKeyPressed, ui::VKEY_F, options);
menu_controller()->OnWillDispatchKeyEvent(&press_f);
EXPECT_FALSE(showing());
EXPECT_FALSE(press_f.handled());
EXPECT_FALSE(press_f.stopped_propagation());
}
#endif
TEST_F(MenuControllerTest, SubmenuOpenByKey) {
MenuItemView* const child_menu = menu_item()->GetSubmenu()->GetMenuItemAt(0);
const SubmenuView* const submenu = child_menu->CreateSubmenu();
child_menu->AppendMenuItem(5, u"Five");
child_menu->AppendMenuItem(6, u"Six");
OpenMenu(menu_item());
SetState(child_menu);
EXPECT_EQ(1, pending_state_item()->GetCommand());
EXPECT_EQ(nullptr, submenu->host());
DispatchKey(ui::VKEY_RIGHT);
EXPECT_EQ(5, pending_state_item()->GetCommand());
EXPECT_NE(nullptr, submenu->host());
}
class ExecuteCommandWithoutClosingMenuTest : public MenuControllerTest {
public:
void SetUp() override {
MenuControllerTest::SetUp();
views::test::DisableMenuClosureAnimations();
menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MenuAnchorPosition::kTopLeft);
ShowSubmenu();
menu_delegate()->set_should_execute_command_without_closing_menu(true);
}
};
TEST_F(ExecuteCommandWithoutClosingMenuTest, OnClick) {
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const menu_item_view = submenu->GetMenuItemAt(0);
const gfx::Point press_location = menu_item_view->bounds().CenterPoint();
ProcessMousePressed(
submenu, ui::MouseEvent(ui::EventType::kMousePressed, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
ProcessMouseReleased(
submenu, ui::MouseEvent(ui::EventType::kMouseReleased, press_location,
press_location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, 0));
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
EXPECT_TRUE(showing());
EXPECT_EQ(menu_delegate()->execute_command_id(),
menu_item_view->GetCommand());
}
TEST_F(ExecuteCommandWithoutClosingMenuTest, OnTap) {
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
SubmenuView* const submenu = menu_item()->GetSubmenu();
const MenuItemView* const menu_item_view = submenu->GetMenuItemAt(0);
const gfx::Point tap_location = menu_item_view->bounds().CenterPoint();
ProcessGestureEvent(
submenu, ui::GestureEvent(
tap_location.x(), tap_location.y(), 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureTap)));
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
EXPECT_TRUE(showing());
EXPECT_EQ(menu_delegate()->execute_command_id(),
menu_item_view->GetCommand());
}
TEST_F(ExecuteCommandWithoutClosingMenuTest, OnReturnKey) {
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
DispatchKey(ui::VKEY_DOWN);
DispatchKey(ui::VKEY_RETURN);
EXPECT_EQ(0, menu_controller_delegate()->on_menu_closed_called());
EXPECT_TRUE(showing());
EXPECT_EQ(menu_delegate()->execute_command_id(),
menu_item()->GetSubmenu()->GetMenuItemAt(0)->GetCommand());
}
TEST_F(MenuControllerTest, ChildMenuOpenDirectionStateUpdatesCorrectly) {
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(0));
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(1));
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(10));
SetChildMenuOpenDirectionAtDepth(1,
MenuController::MenuOpenDirection::kLeading);
SetChildMenuOpenDirectionAtDepth(
2, MenuController::MenuOpenDirection::kTrailing);
SetChildMenuOpenDirectionAtDepth(3,
MenuController::MenuOpenDirection::kLeading);
SetChildMenuOpenDirectionAtDepth(
4, MenuController::MenuOpenDirection::kTrailing);
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(0));
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(1));
EXPECT_EQ(MenuController::MenuOpenDirection::kTrailing,
GetChildMenuOpenDirectionAtDepth(2));
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(3));
EXPECT_EQ(MenuController::MenuOpenDirection::kTrailing,
GetChildMenuOpenDirectionAtDepth(4));
EXPECT_EQ(MenuController::MenuOpenDirection::kLeading,
GetChildMenuOpenDirectionAtDepth(10));
}
TEST_F(MenuControllerTest, MenuHostHasCorrectZOrderLevel) {
ShowSubmenu();
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuHost* const host = menu_host_for_submenu(submenu);
EXPECT_EQ(ui::ZOrderLevel::kFloatingWindow, host->GetZOrderLevel());
}
TEST_F(MenuControllerTest, RemoveEmptyMenuMenuItemWhileSelected) {
views::MenuItemView* const root_menu = menu_item();
OpenMenu(root_menu);
root_menu->RemoveAllMenuItems();
root_menu->ChildrenChanged();
SubmenuView* const submenu = root_menu->GetSubmenu();
ASSERT_TRUE(submenu);
ASSERT_EQ(1u, submenu->children().size());
auto* const empty_child =
AsViewClass<EmptyMenuMenuItem>(submenu->children()[0]);
ASSERT_NE(empty_child, nullptr);
menu_controller()->SelectItemAndOpenSubmenu(empty_child);
ASSERT_TRUE(empty_child->IsSelected());
views::MenuItemView* const item = root_menu->AppendMenuItem(1, u"item 1");
item->SetVisible(true);
root_menu->ChildrenChanged();
EXPECT_TRUE(root_menu->IsSelected());
ASSERT_EQ(1u, submenu->children().size());
EXPECT_EQ(item, submenu->children()[0]);
}
TEST_F(MenuControllerTest, FirstMenuItemSelectedWhenOpenedFromKeyboard) {
if (!PlatformStyle::kAutoSelectFirstMenuItemFromKeyboard) {
GTEST_SKIP() << "Behavior not present on this platform";
}
MenuItemView* root = menu_item();
MenuItemView* item1 = root->GetSubmenu()->GetMenuItemAt(0);
MenuItemView* item2 = root->GetSubmenu()->GetMenuItemAt(1);
MenuItemView* item3 = root->GetSubmenu()->GetMenuItemAt(2);
menu_controller()->Run(owner(), nullptr, root,
gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::mojom::MenuSourceType::kKeyboard,
MenuController::MenuType::kNormal,
false);
EXPECT_TRUE(item1->IsSelected());
EXPECT_FALSE(item2->IsSelected());
EXPECT_FALSE(item3->IsSelected());
}
TEST_F(MenuControllerTest, NoItemSelectedWhenOpenedFromMouse) {
if (!PlatformStyle::kAutoSelectFirstMenuItemFromKeyboard) {
GTEST_SKIP() << "Behavior not present on this platform";
}
MenuItemView* root = menu_item();
MenuItemView* item1 = root->GetSubmenu()->GetMenuItemAt(0);
MenuItemView* item2 = root->GetSubmenu()->GetMenuItemAt(1);
MenuItemView* item3 = root->GetSubmenu()->GetMenuItemAt(2);
menu_controller()->Run(owner(), nullptr, root,
gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::mojom::MenuSourceType::kMouse,
MenuController::MenuType::kNormal,
false);
EXPECT_FALSE(item1->IsSelected());
EXPECT_FALSE(item2->IsSelected());
EXPECT_FALSE(item3->IsSelected());
}
TEST_F(MenuControllerTest,
FirstMenuItemButtonHotTrackedWhenOpenedFromKeyboard) {
if (!PlatformStyle::kAutoSelectFirstMenuItemFromKeyboard) {
GTEST_SKIP() << "Behavior not present on this platform";
}
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* first_item = submenu->GetMenuItemAt(0);
first_item->SetTitle(std::u16string());
const View* const buttons_view = submenu->children()[0];
first_item
->AddChildView(
std::make_unique<LabelButton>(Button::PressedCallback(), u"Label"))
->SetFocusBehavior(View::FocusBehavior::ALWAYS);
menu_controller()->Run(owner(), nullptr, menu_item(),
gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::mojom::MenuSourceType::kKeyboard,
MenuController::MenuType::kNormal,
false);
EXPECT_TRUE(first_item->IsSelected());
GET_CHILD_BUTTON(button1, buttons_view, 0);
EXPECT_TRUE(button1->IsHotTracked());
}
TEST_F(MenuControllerTest,
FirstMenuItemButtonNotHotTrackedWhenOpenedFromMouse) {
if (!PlatformStyle::kAutoSelectFirstMenuItemFromKeyboard) {
GTEST_SKIP() << "Behavior not present on this platform";
}
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* first_item = submenu->GetMenuItemAt(0);
first_item->SetTitle(std::u16string());
const View* const buttons_view = submenu->children()[0];
first_item
->AddChildView(
std::make_unique<LabelButton>(Button::PressedCallback(), u"Label"))
->SetFocusBehavior(View::FocusBehavior::ALWAYS);
menu_controller()->Run(owner(), nullptr, menu_item(),
gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::mojom::MenuSourceType::kMouse,
MenuController::MenuType::kNormal,
false);
EXPECT_FALSE(first_item->IsSelected());
GET_CHILD_BUTTON(button1, buttons_view, 0);
EXPECT_FALSE(button1->IsHotTracked());
}
}