| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/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/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) |
| // Some platforms, such as Wayland, disallow client applications to manipulate |
| // global screen coordinates, requiring menus to be positioned relative to |
| // their parent windows. See comment in ozone_platform_wayland.cc. |
| 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; |
| } |
| |
| // Unfortunately a macro rather than a function, to work around ASSERT_ not |
| // working in non-void functions. |
| #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) |
| |
| // Test implementation of MenuControllerDelegate that only reports the values |
| // called of OnMenuClosed. |
| 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_; |
| } |
| |
| // On a subsequent call to OnMenuClosed |controller| will be deleted. |
| void set_on_menu_closed_callback(base::RepeatingClosure callback) { |
| on_menu_closed_callback_ = std::move(callback); |
| } |
| |
| // internal::MenuControllerDelegate: |
| void OnMenuClosed(NotifyType type, |
| MenuItemView* menu, |
| int mouse_event_flags) override; |
| void SiblingMenuCreated(MenuItemView* menu) override; |
| |
| private: |
| // Number of times OnMenuClosed has been called. |
| int on_menu_closed_called_ = 0; |
| |
| // The values passed on the last call of OnMenuClosed. |
| NotifyType on_menu_closed_notify_type_ = NOTIFY_DELEGATE; |
| raw_ptr<MenuItemView> on_menu_closed_menu_ = nullptr; |
| int on_menu_closed_mouse_event_flags_ = 0; |
| |
| // Optional callback triggered during OnMenuClosed |
| 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; |
| }; |
| |
| // A test widget that counts gesture events. |
| 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) |
| // A DragDropClient which does not trigger a nested run loop. Instead a |
| // callback is triggered during StartDragAndDrop in order to allow testing. |
| class TestDragDropClient : public aura::client::DragDropClient { |
| public: |
| explicit TestDragDropClient(base::RepeatingClosure callback) |
| : start_drag_and_drop_callback_(std::move(callback)) {} |
| |
| // aura::client::DragDropClient: |
| 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 // defined(USE_AURA) |
| |
| // View which cancels the menu it belongs to on mouse press. |
| class CancelMenuOnMousePressView : public View { |
| METADATA_HEADER(CancelMenuOnMousePressView, View) |
| |
| public: |
| explicit CancelMenuOnMousePressView(base::WeakPtr<MenuController> controller) |
| : controller_(controller) {} |
| |
| // View: |
| bool OnMousePressed(const ui::MouseEvent& event) override; |
| gfx::Size CalculatePreferredSize( |
| const SizeBounds& /*available_size*/) 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& /*available_size*/) const { |
| // This is needed to prevent the view from being "squashed" to zero height |
| // when the menu which owns it is shown. In such state the logic which |
| // determines if the menu contains the mouse press location doesn't work. |
| return size(); |
| } |
| |
| BEGIN_METADATA(CancelMenuOnMousePressView) |
| END_METADATA |
| |
| } // namespace |
| |
| 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; |
| |
| // ViewsTestBase: |
| 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) |
| // Verifies that a non-nested menu fully closes when receiving an escape key. |
| void TestAsyncEscapeKey(); |
| |
| // Verifies that an open menu receives a cancel event, and closes. |
| void TestCancelEvent(); |
| #endif // defined(USE_AURA) |
| |
| // Verifies the state of the |menu_controller_| before destroying it. |
| void VerifyDragCompleteThenDestroy(); |
| |
| // Sets up |menu_controller_delegate_| to be destroyed when OnMenuClosed is |
| // called. |
| void TestDragCompleteThenDestroyOnMenuClosed(); |
| |
| // Tests destroying the active |menu_controller_| and replacing it with a new |
| // active instance. |
| void TestMenuControllerReplacementDuringDrag(); |
| |
| // Tests that the menu does not destroy itself when canceled during a drag. |
| void TestCancelAllDuringDrag(); |
| |
| // Tests that destroying the menu during ViewsDelegate::ReleaseRef does not |
| // cause a crash. |
| 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); |
| |
| // Given an onscreen menu `item` with screen bounds `parent_bounds`, verifies |
| // that the submenu opened from `item` fits inside `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()); |
| } |
| |
| // These functions expect `event` to be in coordinates of `source`. |
| 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); |
| |
| // Causes the |menu_controller_| to begin dragging. Use TestDragDropClient to |
| // avoid nesting message loops. |
| 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_; |
| } |
| |
| // Displays a submenu with appropriate params. If the first arg is null (the |
| // default), displays `menu_item()->GetSubmenu()`. Supply a second arg if you |
| // want a callback to modify the init params before calling |
| // `SubmenuView::ShowAt()`. |
| 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); |
| } |
| |
| // Adds a menu item having buttons as children and returns it. If |
| // `single_child` is true, the hosting menu item has only one child button. |
| 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_` must be torn down before `ViewsTestBase::TearDown()`, since |
| // it may transitively own a `Compositor` that is registered with a context |
| // factory that `TearDown()` will destroy. |
| 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); |
| } |
| |
| // static |
| 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 // defined(USE_AURA) |
| |
| 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(/*for_drop=*/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() { |
| // |test_views_delegate_| is owned by views::ViewsTestBase and not deleted |
| // until TearDown. MenuControllerTest outlives it. |
| 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(); |
| }; |
| |
| // Simulate a bottom shelf with a tall menu. |
| const gfx::Point monitor_center = monitor_bounds.CenterPoint(); |
| test_within_bounds( |
| gfx::Point(monitor_center.x(), monitor_bounds.bottom() - kButtonSize)); |
| |
| // Simulate a left shelf with a tall menu. |
| test_within_bounds(gfx::Point(monitor_bounds.x(), monitor_center.y())); |
| |
| // Simulate right shelf with a tall menu. |
| 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}; |
| |
| // Put the parent menu onscreen so that `item` is in a widget hierarchy and |
| // `GetBoundsInScreen()` will work. |
| SubmenuView* const submenu = item->GetParentMenuItem()->GetSubmenu(); |
| ShowSubmenu(submenu, [&](auto& params) { |
| // Set the bounds such that `item` (which is assumed to be the sole |
| // content of `submenu`) has screen bounds `parent_bounds`. |
| 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) { |
| // Unretained() is safe here as the test should outlive the delegate. If not |
| // we want to know. |
| 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) { |
| // Must set both `state_` and `pending_state_` in case, while processing, the |
| // controller commits the pending state. |
| 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")) |
| // This is an in-menu button. Hence it must be always focusable. |
| ->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(); |
| } |
| |
| // static |
| 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) |
| // Increase the submenu shadow elevation and change the shadow style to |
| // ChromeOS system UI shadow style when using Ash System UI layout. |
| 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); |
| } |
| |
| // Creates the menu controller with `for_drop` set to true. I.e., the menu is |
| // being shown because something was dragged over it. |
| 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) |
| // Tests that an event targeter which blocks events will be honored by the menu |
| // event dispatcher. |
| TEST_F(MenuControllerTest, EventTargeter) { |
| { |
| // With the aura::NullWindowTargeter instantiated and assigned we expect |
| // the menu to not handle the key event. |
| aura::ScopedWindowTargeter scoped_targeter( |
| GetRootWindow(owner()), std::make_unique<aura::NullWindowTargeter>()); |
| PressKey(ui::VKEY_ESCAPE); |
| EXPECT_EQ(MenuController::ExitType::kNone, menu_exit_type()); |
| } |
| |
| // Now that the targeter has been destroyed, expect to exit the menu |
| // normally when hitting escape. |
| TestAsyncEscapeKey(); |
| EXPECT_EQ(MenuController::ExitType::kAll, menu_exit_type()); |
| } |
| #endif // defined(USE_AURA) |
| |
| #if BUILDFLAG(IS_OZONE_X11) |
| // Tests that touch event ids are released correctly. See crbug.com/439051 for |
| // details. When the ids aren't managed correctly, we get stuck down touches. |
| TEST_F(MenuControllerTest, TouchIdsReleasedCorrectly) { |
| // Run this test only for X11. |
| 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 // BUILDFLAG(IS_OZONE_X11) |
| |
| // Tests that initial selected menu items are correct when items are enabled or |
| // disabled. |
| TEST_F(MenuControllerTest, InitialSelectedItem) { |
| // Leave items "Two", "Three", and "Four" enabled. |
| 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()); |
| }; |
| // The first selectable item should be item "Two". |
| check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 2); |
| // The last selectable item should be item "Four". |
| check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 4); |
| |
| // Leave items "One" and "Two" enabled. |
| submenu->GetMenuItemAt(0)->SetEnabled(true); |
| submenu->GetMenuItemAt(2)->SetEnabled(false); |
| submenu->GetMenuItemAt(3)->SetEnabled(false); |
| // The first selectable item should be item "One". |
| check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 1); |
| // The last selectable item should be item "Two". |
| check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 2); |
| |
| // Leave only a single item "One" enabled. |
| submenu->GetMenuItemAt(1)->SetEnabled(false); |
| // The first selectable item should be item "One". |
| check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 1); |
| // The last selectable item should be item "One". |
| check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 1); |
| |
| // Leave only a single item "Three" enabled. |
| submenu->GetMenuItemAt(0)->SetEnabled(false); |
| submenu->GetMenuItemAt(2)->SetEnabled(true); |
| // The first selectable item should be item "Three". |
| check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 3); |
| // The last selectable item should be item "Three". |
| check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 3); |
| |
| // Leave only a single item ("Two") selected. |
| submenu->GetMenuItemAt(1)->SetEnabled(true); |
| submenu->GetMenuItemAt(2)->SetEnabled(false); |
| // The first selectable item should be item "Two". |
| check_has_command(FindInitialSelectableMenuItemDown(menu_item()), 2); |
| // The last selectable item should be item "Two". |
| check_has_command(FindInitialSelectableMenuItemUp(menu_item()), 2); |
| } |
| |
| // Verifies that the scroll arrow is shown when the menu content |
| // does not fit within the available bounds. |
| // (https://crbug.com/338585369) |
| 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)); |
| // Show the menu at its preferred size without restriction |
| submenu->ShowAt(params); |
| EXPECT_FALSE(scroll_container->scroll_down_button()->GetVisible()); |
| // decrease the available space by 1 so the contents no longer fit |
| params.bounds.set_height(params.bounds.height() - 1); |
| submenu->ShowAt(params); |
| EXPECT_TRUE(scroll_container->scroll_down_button()->GetVisible()); |
| } |
| |
| // Verifies that the context menu bubble should prioritize its cached menu |
| // position (above or below the anchor) after its size updates |
| // (https://crbug.com/1126244). |
| 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(/*is_submenu=*/false); |
| |
| // Calculate the suitable anchor point to ensure that if the menu shows below |
| // the anchor point, the bottom of the menu should be one pixel off the |
| // bottom of the display. It means that there is insufficient space for the |
| // menu below the anchor. |
| 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}; |
| |
| // Case 1: There is insufficient space for the menu below `anchor_point` and |
| // there is no cached menu position. The menu should show above the anchor. |
| 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()); |
| |
| // Case 2: There is insufficient space for the menu below `anchor_point`. The |
| // cached position is below the anchor. The menu should show above the anchor. |
| options.menu_position = MenuItemView::MenuPosition::kBelowBounds; |
| CalculateBubbleMenuBoundsWithoutInsets(options); |
| EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds, |
| menu_item_actual_position()); |
| |
| // Case 3: There is enough space for the menu below `anchor_point`. The cached |
| // menu position is above the anchor. The menu should show above the anchor. |
| 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()); |
| } |
| |
| // Verifies that the context menu bubble position, |
| // MenuAnchorPosition::kBubbleBottomRight, does not shift as items are removed. |
| // The menu position will shift if items are added and the menu no longer fits |
| // in its previous 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(/*is_submenu=*/false); |
| |
| // Calculate the suitable anchor point to ensure that if the menu shows below |
| // the anchor point, the bottom of the menu should be one pixel off the |
| // bottom of the display. It means that there is insufficient space for the |
| // menu below the anchor. |
| 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}; |
| |
| // Case 1: There is insufficient space for the menu below `anchor_point` and |
| // there is no cached menu position. The menu should show above the anchor. |
| 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()); |
| |
| // Case 2: There is insufficient space for the menu below `anchor_point`. The |
| // cached position is below the anchor. The menu should show above the anchor |
| // point. |
| options.menu_position = MenuItemView::MenuPosition::kBelowBounds; |
| CalculateBubbleMenuBoundsWithoutInsets(options); |
| EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds, |
| menu_item_actual_position()); |
| |
| // Case 3: There is enough space for the menu below `anchor_point`. The cached |
| // menu position is above the anchor. The menu should show above the anchor. |
| 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()); |
| |
| // Case 4: There is enough space for the menu below `anchor_point`. The cached |
| // menu position is below the anchor. The menu should show below the anchor. |
| options.menu_position = MenuItemView::MenuPosition::kBelowBounds; |
| CalculateBubbleMenuBoundsWithoutInsets(options); |
| EXPECT_EQ(MenuItemView::MenuPosition::kBelowBounds, |
| menu_item_actual_position()); |
| } |
| |
| // Tests that opening the menu and pressing 'Home' selects the first menu item. |
| TEST_F(MenuControllerTest, FirstSelectedItem) { |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| SetPendingStateItem(submenu->GetMenuItemAt(0)); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Select the first menu item. |
| DispatchKey(ui::VKEY_HOME); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Fake initial root item selection and submenu showing. |
| SetPendingStateItem(menu_item()); |
| EXPECT_EQ(0, pending_state_item()->GetCommand()); |
| |
| // Select the first menu item. |
| DispatchKey(ui::VKEY_HOME); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Select the last item. |
| SetPendingStateItem(submenu->GetMenuItemAt(3)); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| // Select the first menu item. |
| DispatchKey(ui::VKEY_HOME); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| } |
| |
| // Tests that opening the menu and pressing 'End' selects the last enabled menu |
| // item. |
| TEST_F(MenuControllerTest, LastSelectedItem) { |
| // Fake initial root item selection and submenu showing. |
| ShowSubmenu(); |
| SetPendingStateItem(menu_item()); |
| EXPECT_EQ(0, pending_state_item()->GetCommand()); |
| |
| // Select the last menu item. |
| DispatchKey(ui::VKEY_END); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| // Select the last item. |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| SetPendingStateItem(submenu->GetMenuItemAt(3)); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| // Select the last menu item. |
| DispatchKey(ui::VKEY_END); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| // Select the first item. |
| SetPendingStateItem(submenu->GetMenuItemAt(0)); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Select the last menu item. |
| DispatchKey(ui::VKEY_END); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| } |
| |
| // MenuController tests which set expectations about how menu item selection |
| // behaves should verify test cases work as intended for all supported selection |
| // mechanisms. |
| class MenuControllerSelectionTest : public MenuControllerTest { |
| public: |
| MenuControllerSelectionTest() = default; |
| |
| protected: |
| // Models a mechanism by which menu item selection can be incremented and/or |
| // decremented. |
| struct SelectionMechanism { |
| base::RepeatingClosure IncrementSelection; |
| base::RepeatingClosure DecrementSelection; |
| }; |
| |
| // Returns all mechanisms by which menu item selection can be incremented |
| // and/or decremented. |
| const std::vector<SelectionMechanism>& selection_mechanisms() { |
| return selection_mechanisms_; |
| } |
| |
| private: |
| const std::vector<SelectionMechanism> selection_mechanisms_ = { |
| // Updates selection via IncrementSelection()/DecrementSelection(). |
| {base::BindRepeating(&MenuControllerSelectionTest::IncrementSelection, |
| base::Unretained(this)), |
| base::BindRepeating(&MenuControllerSelectionTest::DecrementSelection, |
| base::Unretained(this))}, |
| // Updates selection via down/up arrow keys. |
| {base::BindRepeating(&MenuControllerTest::DispatchKey, |
| base::Unretained(this), |
| ui::VKEY_DOWN), |
| base::BindRepeating(&MenuControllerTest::DispatchKey, |
| base::Unretained(this), |
| ui::VKEY_UP)}, |
| // Updates selection via next/prior keys. |
| {base::BindRepeating(&MenuControllerTest::DispatchKey, |
| base::Unretained(this), |
| ui::VKEY_NEXT), |
| base::BindRepeating(&MenuControllerTest::DispatchKey, |
| base::Unretained(this), |
| ui::VKEY_PRIOR)}}; |
| }; |
| |
| // Tests that opening menu and exercising various mechanisms to update |
| // selection iterates over enabled items. |
| TEST_F(MenuControllerSelectionTest, NextSelectedItem) { |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| for (const auto& selection_mechanism : selection_mechanisms()) { |
| // Disabling the item "Three" gets it skipped when using keyboard to |
| // navigate. |
| submenu->GetMenuItemAt(2)->SetEnabled(false); |
| |
| // Fake initial hot selection. |
| SetPendingStateItem(submenu->GetMenuItemAt(0)); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Move down in the menu. |
| // Select next item. |
| selection_mechanism.IncrementSelection.Run(); |
| EXPECT_EQ(2, pending_state_item()->GetCommand()); |
| |
| // Skip disabled item. |
| selection_mechanism.IncrementSelection.Run(); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| selection_mechanism.IncrementSelection.Run(); |
| if (SelectionWraps()) { |
| // Wrap around. |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Move up in the menu. |
| // Wrap around. |
| selection_mechanism.DecrementSelection.Run(); |
| } |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| // Skip disabled item. |
| selection_mechanism.DecrementSelection.Run(); |
| EXPECT_EQ(2, pending_state_item()->GetCommand()); |
| |
| // Select previous item. |
| selection_mechanism.DecrementSelection.Run(); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| } |
| } |
| |
| // Tests that opening menu and exercising various mechanisms to decrement |
| // selection selects the last enabled menu item. |
| TEST_F(MenuControllerSelectionTest, PreviousSelectedItem) { |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| for (const auto& selection_mechanism : selection_mechanisms()) { |
| // Disabling the item "Four" gets it skipped when using keyboard to |
| // navigate. |
| submenu->GetMenuItemAt(3)->SetEnabled(false); |
| |
| // Fake initial root item selection and submenu showing. |
| SetPendingStateItem(menu_item()); |
| EXPECT_EQ(0, pending_state_item()->GetCommand()); |
| |
| // Move up and select a previous (in our case the last enabled) item. |
| selection_mechanism.DecrementSelection.Run(); |
| EXPECT_EQ(3, pending_state_item()->GetCommand()); |
| } |
| } |
| |
| // Tests that the APIs related to the current selected item work correctly. |
| TEST_F(MenuControllerTest, CurrentSelectedItem) { |
| ShowSubmenu(); |
| SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| |
| // Select the first menu-item. |
| DispatchKey(ui::VKEY_HOME); |
| EXPECT_EQ(pending_state_item(), menu_controller()->GetSelectedMenuItem()); |
| |
| // The API should let the submenu stay open if already so, but clear any |
| // selections within it. |
| 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()); |
| } |
| |
| // Tests that opening menu and calling SelectByChar works correctly. |
| TEST_F(MenuControllerTest, SelectByChar) { |
| SetComboboxType(MenuController::ComboboxType::kReadonly); |
| ShowSubmenu(); |
| |
| // Handle null character should do nothing. |
| SelectByChar(0); |
| EXPECT_EQ(0, pending_state_item()->GetCommand()); |
| |
| // Handle searching for 'f'; should find "Four". |
| SelectByChar('f'); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| } |
| |
| TEST_F(MenuControllerTest, SelectChildButtonView) { |
| AddButtonMenuItems(/*single_child=*/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); |
| |
| // Handle searching for 'f'; should find "Four". |
| SelectByChar('f'); |
| EXPECT_EQ(4, pending_state_item()->GetCommand()); |
| |
| EXPECT_FALSE(button1->IsHotTracked()); |
| EXPECT_FALSE(button2->IsHotTracked()); |
| EXPECT_FALSE(button3->IsHotTracked()); |
| |
| // Move selection to |button1|. |
| IncrementSelection(); |
| EXPECT_EQ(5, pending_state_item()->GetCommand()); |
| EXPECT_TRUE(button1->IsHotTracked()); |
| EXPECT_FALSE(button2->IsHotTracked()); |
| EXPECT_FALSE(button3->IsHotTracked()); |
| |
| // Move selection to |button2|. |
| IncrementSelection(); |
| EXPECT_EQ(5, pending_state_item()->GetCommand()); |
| EXPECT_FALSE(button1->IsHotTracked()); |
| EXPECT_TRUE(button2->IsHotTracked()); |
| EXPECT_FALSE(button3->IsHotTracked()); |
| |
| // Move selection to |button3|. |
| IncrementSelection(); |
| EXPECT_EQ(5, pending_state_item()->GetCommand()); |
| EXPECT_FALSE(button1->IsHotTracked()); |
| EXPECT_FALSE(button2->IsHotTracked()); |
| EXPECT_TRUE(button3->IsHotTracked()); |
| |
| // Move a mouse to hot track the |button1|. |
| 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()); |
| |
| // Incrementing selection should move hot tracking to the second button |
| // (next after the first button). |
| IncrementSelection(); |
| EXPECT_EQ(5, pending_state_item()->GetCommand()); |
| EXPECT_FALSE(button1->IsHotTracked()); |
| EXPECT_TRUE(button2->IsHotTracked()); |
| EXPECT_FALSE(button3->IsHotTracked()); |
| |
| // Increment selection twice to wrap around. |
| IncrementSelection(); |
| IncrementSelection(); |
| EXPECT_EQ(SelectionWraps() ? 1 : 5, pending_state_item()->GetCommand()); |
| } |
| |
| TEST_F(MenuControllerTest, DeleteChildButtonView) { |
| AddButtonMenuItems(/*single_child=*/false); |
| |
| // Handle searching for 'f'; should find "Four". |
| 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()); |
| |
| // Increment twice to move selection to |button2|. |
| IncrementSelection(); |
| IncrementSelection(); |
| EXPECT_EQ(5, pending_state_item()->GetCommand()); |
| EXPECT_FALSE(button1->IsHotTracked()); |
| EXPECT_TRUE(button2->IsHotTracked()); |
| EXPECT_FALSE(button3->IsHotTracked()); |
| |
| // Delete |button2| while it is hot-tracked. |
| // This should update MenuController via ViewHierarchyChanged and reset |
| // |hot_button_|. |
| delete button2; |
| |
| // Incrementing selection should now set hot-tracked item to |button1|. |
| // It should not crash. |
| IncrementSelection(); |
| EXPECT_EQ(5, pending_state_item()->GetCommand()); |
| EXPECT_TRUE(button1->IsHotTracked()); |
| EXPECT_FALSE(button3->IsHotTracked()); |
| } |
| |
| // Verifies that the child button is hot tracked after the host menu item is |
| // selected by `MenuController::SelectItemAndOpenSubmenu()`. |
| TEST_F(MenuControllerTest, ChildButtonHotTrackedAfterMenuItemSelection) { |
| // Add a menu item which owns a button as child. |
| MenuItemView* const hosting_menu_item = |
| AddButtonMenuItems(/*single_child=*/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()); |
| } |
| |
| // Verifies that the child button of the menu item which is under mouse |
| // hovering is hot tracked (https://crbug.com/1135000). |
| TEST_F(MenuControllerTest, ChildButtonHotTrackedAfterMouseMove) { |
| // Add a menu item which owns a button as child. |
| const MenuItemView* const hosting_menu_item = |
| AddButtonMenuItems(/*single_child=*/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)); |
| |
| // After the mouse moves to `button`, `button` should be hot tracked. |
| EXPECT_EQ(button, hot_button()); |
| EXPECT_TRUE(button->IsHotTracked()); |
| } |
| |
| // Creates a menu with Button child views, simulates running a nested |
| // menu and tests that existing the nested run restores hot-tracked child |
| // view. |
| TEST_F(MenuControllerTest, ChildButtonHotTrackedWhenNested) { |
| AddButtonMenuItems(/*single_child=*/false); |
| |
| // Handle searching for 'f'; should find "Four". |
| 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()); |
| |
| // Increment twice to move selection to |button2|. |
| 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); |
| |
| // |button2| should stay in hot-tracked state but menu controller should not |
| // track it anymore (preventing resetting hot-tracked state when changing |
| // selection while a nested run is active). |
| EXPECT_TRUE(button2->IsHotTracked()); |
| EXPECT_EQ(nullptr, hot_button()); |
| |
| // Setting hot-tracked button while nested should get reverted when nested |
| // menu run ends. |
| SetHotTrackedButton(button1); |
| EXPECT_TRUE(button1->IsHotTracked()); |
| EXPECT_EQ(button1, hot_button()); |
| |
| // Setting the hot tracked state twice on the same button via the |
| // menu controller should still set the hot tracked state on the button |
| // again. |
| 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()); |
| } |
| |
| // Tests that a menu opened asynchronously, will notify its |
| // MenuControllerDelegate when Accept is called. |
| 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()); |
| } |
| |
| // Tests that a menu opened asynchronously, will notify its |
| // MenuControllerDelegate when CancelAll is called. |
| 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()); |
| } |
| |
| // Tests that canceling a nested menu restores the previous |
| // MenuControllerDelegate, and notifies each delegate. |
| 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()); |
| } |
| |
| // Tests that dropping within an asynchronous menu does not hide the menu. |
| 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, |
| /*drag_image_layer_owner=*/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()); |
| } |
| |
| // Tests that dragging within an asynchronous menu notifies the |
| // MenuControllerDelegate for shutdown. |
| 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()); |
| } |
| |
| // Tests that completing drag with `should_close` set to false will not close |
| // the menu. |
| TEST_F(MenuControllerTest, AsynchronousDragCompleteWithoutClose) { |
| TestDragCompleteThenDestroyOnMenuClosed(); |
| |
| menu_controller()->OnDragWillStart(); |
| menu_controller()->OnDragComplete(false); |
| |
| // TODO(crbug.com/375959961): For X11, the menu is closed on drag completion |
| // because the native widget's state is not properly updated. |
| EXPECT_EQ(BUILDFLAG(IS_OZONE_X11) ? 1 : 0, |
| menu_controller_delegate()->on_menu_closed_called()); |
| } |
| |
| // Tests that if Cancel is called during a drag, that OnMenuClosed is still |
| // notified when the drag completes. |
| 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()); |
| } |
| |
| // Tests that if a menu is destroyed while drag operations are occurring, that |
| // the MenuHost does not crash as the drag completes. |
| 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); |
| } |
| |
| // Tests that getting the drop callback does not hide the menu. |
| 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, |
| /*drag_image_layer_owner=*/nullptr); |
| EXPECT_TRUE(menu_delegate->is_drop_performed()); |
| } |
| |
| // Tests that getting the drop callback hides the menu when it's being shown for |
| // a drop. |
| 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, |
| /*drag_image_layer_owner=*/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)); |
| |
| // The command shouldn't be executed if this menu is open for a drop. |
| EXPECT_EQ(menu_delegate()->execute_command_id(), |
| test::TestMenuDelegate::kInvalidExecuteCommandId); |
| EXPECT_EQ(menu_controller_delegate()->on_menu_closed_called(), 0); |
| EXPECT_TRUE(showing()); |
| } |
| |
| // Widget destruction and cleanup occurs on the MessageLoop after the |
| // MenuController has been destroyed. A MenuHostRootView should not attempt to |
| // access a destroyed MenuController. This test should not cause a crash. |
| TEST_F(MenuControllerTest, HostReceivesInputBeforeDestruction) { |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| const gfx::Point location = |
| submenu->bounds().bottom_right() + gfx::Vector2d(1, 1); |
| |
| // Normally created as the full Widget is brought up. Explicitly created |
| // here for testing. |
| std::unique_ptr<MenuHostRootView> root_view( |
| CreateMenuHostRootView(menu_host_for_submenu(submenu))); |
| DestroyMenuController(); |
| |
| // This should not attempt to access the destroyed MenuController and should |
| // not crash. |
| root_view->OnMouseMoved(ui::MouseEvent(ui::EventType::kMouseMoved, location, |
| location, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, 0)); |
| } |
| |
| // Tests that an asynchronous menu nested within an asynchronous menu closes |
| // both menus, and notifies both delegates. |
| TEST_F(MenuControllerTest, DoubleAsynchronousNested) { |
| // Nested run |
| 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()); |
| } |
| |
| // Tests that setting send_gesture_events_to_owner flag forwards gesture |
| // events to owner and the forwarding stops when the current gesture sequence |
| // ends. |
| 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)); |
| |
| // Gesture events should not be forwarded if the flag is not set. |
| EXPECT_EQ(owner_gesture_count(), 0); |
| EXPECT_FALSE(menu_controller()->send_gesture_events_to_owner()); |
| ProcessGestureEvent(submenu, event); |
| EXPECT_EQ(owner_gesture_count(), 0); |
| |
| // The menu's owner should receive gestures triggered outside the menu. |
| 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); |
| |
| // EventType::kGestureEnd resets the |send_gesture_events_to_owner_| flag, so |
| // further gesture events should not be sent to the owner. |
| ProcessGestureEvent(submenu, event2); |
| EXPECT_EQ(owner_gesture_count(), 2); |
| } |
| |
| #if defined(USE_AURA) |
| // Tests that setting `send_gesture_events_to_owner` flag forwards gesture |
| // events to the NativeView specified for gestures and not the owner's |
| // NativeView. |
| 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()); |
| |
| // Ensure menu is closed before running with the menu with `child_window` as |
| // the NativeView for gestures. |
| 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)); |
| |
| // Gesture events should not be forwarded to either the `child_window` or |
| // the hosts native window if the flag is not set. |
| 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()); |
| |
| // The `child_window` should receive gestures triggered outside the menu. |
| 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()); |
| |
| // EventType::kGestureEnd resets the `send_gesture_events_to_owner_` flag, so |
| // further gesture events should not be sent to the `child_window`. |
| ProcessGestureEvent(submenu, event2); |
| EXPECT_EQ(0, owner_gesture_count()); |
| EXPECT_EQ(0, child_delegate.GetGestureCountAndReset()); |
| } |
| #endif |
| |
| // Tests that touch outside menu does not closes the menu when forwarding |
| // gesture events to owner. |
| TEST_F(MenuControllerTest, NoTouchCloseWhenSendingGesturesToOwner) { |
| views::test::DisableMenuClosureAnimations(); |
| |
| // Owner wants the gesture events. |
| menu_controller()->set_send_gesture_events_to_owner(true); |
| |
| // Show a sub menu and touch outside of it. |
| 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); |
| |
| // Menu should still be visible. |
| EXPECT_TRUE(showing()); |
| |
| // The current gesture sequence ends. |
| ProcessGestureEvent( |
| submenu, |
| ui::GestureEvent(location.x(), location.y(), 0, ui::EventTimeForNow(), |
| ui::GestureEventDetails(ui::EventType::kGestureEnd))); |
| |
| // Touch outside again and menu should be closed. |
| ProcessTouchEvent(submenu, touch_event); |
| views::test::WaitForMenuClosureAnimation(); |
| EXPECT_FALSE(showing()); |
| EXPECT_EQ(MenuController::ExitType::kAll, menu_controller()->exit_type()); |
| } |
| |
| // Tests that a nested menu does not crash when trying to repost events that |
| // occur outside of the bounds of the menu. Instead a proper shutdown should |
| // occur. |
| 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); |
| |
| // Show a sub menu to target with a pointer selection. However have the |
| // event occur outside of the bounds of the entire menu. |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| const gfx::Point location = |
| submenu->GetLocalBounds().bottom_right() + gfx::Vector2d(1, 1); |
| |
| // When attempting to select outside of all menus this should lead to a |
| // shutdown. This should not crash while attempting to repost the event. |
| 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()); |
| } |
| |
| // Tests that an asynchronous menu reposts touch events that occur outside of |
| // the bounds of the menu, and that the menu closes. |
| TEST_F(MenuControllerTest, AsynchronousTouchEventRepostEvent) { |
| views::test::DisableMenuClosureAnimations(); |
| |
| // Show a sub menu to target with a touch event. However have the event |
| // occur outside of the bounds of the entire menu. |
| 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()); |
| } |
| |
| // Tests that having the MenuController deleted during RepostEvent does not |
| // cause a crash. ASAN bots should not detect use-after-free in |
| // MenuController. |
| 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); |
| |
| // Show a sub menu to target with a pointer selection. However have the |
| // event occur outside of the bounds of the entire menu. |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| const gfx::Point location = |
| submenu->GetLocalBounds().bottom_right() + gfx::Vector2d(1, 1); |
| |
| // This will lead to MenuController being deleted during the event repost. |
| // The remainder of this test, and TearDown should not crash. |
| DestroyMenuControllerOnMenuClosed(nested_delegate.get()); |
| |
| // When attempting to select outside of all menus this should lead to a |
| // shutdown. This should not crash while attempting to repost the event. |
| SetSelectionOnPointerDown( |
| submenu, |
| ui::MouseEvent(ui::EventType::kMousePressed, location, location, |
| ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0)); |
| views::test::WaitForMenuClosureAnimation(); |
| |
| // Close to remove observers before test TearDown |
| submenu->Close(); |
| EXPECT_EQ(1, nested_delegate->on_menu_closed_called()); |
| } |
| |
| // Tests that having the MenuController deleted during OnGestureEvent does not |
| // cause a crash. ASAN bots should not detect use-after-free in |
| // MenuController. |
| 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); |
| |
| // Show a sub menu to target with a tap event. |
| ShowSubmenu(); |
| SubmenuView* const submenu = menu_item()->GetSubmenu(); |
| const gfx::Point location = submenu->GetMenuItemAt(0)->bounds().CenterPoint(); |
| |
| // This will lead to MenuController being deleted during the processing of |
| // the gesture event. The remainder of this test, and TearDown should not |
| // crash. |
| DestroyMenuControllerOnMenuClosed(nested_delegate.get()); |
| ProcessGestureEvent( |
| submenu, |
| ui::GestureEvent(location.x(), location.y(), 0, ui::EventTimeForNow(), |
| ui::GestureEventDetails(ui::EventType::kGestureTap))); |
| views::test::WaitForMenuClosureAnimation(); |
| |
| // Close to remove observers before test TearDown |
| submenu->Close(); |
| EXPECT_EQ(1, nested_delegate->on_menu_closed_called()); |
| } |
| |
| // Test that the menu is properly placed where it best fits. |
| TEST_F(MenuControllerTest, CalculateMenuBoundsBestFitTest) { |
| const bool ignore_screen_bounds_for_menus = |
| ShouldIgnoreScreenBoundsForMenus(); |
| |
| // Fits in all locations -> placed below. |
| 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)); |
| |
| // Fits above and to both sides -> placed above. |
| 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)); |
| |
| // Fits on both sides, prefer right -> placed right. |
| 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)); |
| |
| // Fits only on left -> placed left. |
| 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)); |
| |
| // Fits on both sides, prefer left -> placed left. |
| 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)); |
| |
| // Fits only on right -> placed right. |
| 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)); |
| } |
| |
| // Tests that the menu is properly placed according to its anchor. |
| 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)); |
| |
| // Menu will be placed above or below with an offset. |
| options.menu_anchor = MenuAnchorPosition::kBottomCenter; |
| constexpr int kTouchYPadding = 15; |
| |
| // Menu fits above -> placed above. |
| 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)); |
| |
| // Menu does not fit above -> placed below. |
| 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)); |
| } |
| |
| // Regression test for https://crbug.com/1217711 |
| TEST_F(MenuControllerTest, MenuAnchorPositionFlippedInRtl) { |
| ASSERT_FALSE(base::i18n::IsRTL()); |
| |
| // Test the AdjustAnchorPositionForRtl() method directly, rather than |
| // running the menu, because it's awkward to access the menu's window. Also, |
| // the menu bounds are already tested separately. |
| 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); |
| |
| // Anchor positions are left/right flipped in RTL. |
| 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 that menus show up on screen with non-zero sized anchors. |
| TEST_P(MenuControllerTest, TestMenuFitsOnScreen) { |
| // Simulate multiple display layouts. |
| 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 that menus show up on screen with zero sized anchors. |
| TEST_P(MenuControllerTest, TestMenuFitsOnScreenSmallAnchor) { |
| // Simulate multiple display layouts. |
| 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 that menus fit a small screen. |
| TEST_P(MenuControllerTest, TestMenuFitsOnSmallScreen) { |
| // Simulate multiple display layouts. |
| 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 that submenus are displayed within the screen bounds on smaller |
| // screens. |
| 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}) { |
| // Simulate multiple display layouts. |
| 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); |
| } |
| |
| // Extra wide menu: test with insufficient room on both sides. |
| TestSubmenuFitsOnScreen( |
| sub_item, monitor_bounds, |
| gfx::Rect(gfx::Point(x_min + (x_max - x_min) / 4, y_min), |
| parent_size_wide), |
| menu_position); |
| } |
| } |
| } |
| } |
| |
| // Test that a menu that was originally drawn below the anchor does not get |
| // squished or move above the anchor when it grows vertically and horizontally |
| // beyond the monitor bounds. |
| TEST_F(MenuControllerTest, GrowingMenuMovesLaterallyNotVertically) { |
| // We can't know the position of windows in Wayland. Thus, this case is not |
| // valid for Wayland. |
| if (ShouldIgnoreScreenBoundsForMenus()) { |
| return; |
| } |
| |
| MenuBoundsOptions options = { |
| // The anchor should be near the bottom right side of the screen. |
| .anchor_bounds = gfx::Rect(80, 70, 15, 10), |
| .monitor_bounds = gfx::Rect(0, 0, 100, 100), |
| // The menu should fit the available space, below the anchor. |
| .menu_size = gfx::Size(20, 20), |
| }; |
| |
| // Ensure the menu is initially drawn below the bounds, and the MenuPosition |
| // is set to MenuPosition::kBelowBounds; |
| EXPECT_EQ(gfx::Rect(80, 80, 20, 20), CalculateMenuBounds(options)); |
| EXPECT_EQ(MenuItemView::MenuPosition::kBelowBounds, |
| menu_item_actual_position()); |
| |
| // The menu bounds are larger than the remaining space on the monitor. This |
| // simulates the case where the menu has been grown vertically and |
| // horizontally to where it would no longer fit on the screen. |
| options.menu_size = gfx::Size(50, 50); |
| options.menu_position = MenuItemView::MenuPosition::kBelowBounds; |
| |
| // The menu bounds should move left to show the wider menu, and grow to fill |
| // the remaining vertical space without moving upwards. |
| EXPECT_EQ(gfx::Rect(50, 80, 50, 20), CalculateMenuBounds(options)); |
| } |
| |
| #if defined(USE_AURA) |
| // This tests that mouse moved events from the initial position of the mouse |
| // when the menu was shown don't select the menu item at the mouse position. |
| TEST_F(MenuControllerTest, MouseAtMenuItemOnShow) { |
| // Most tests create an already shown menu but this test needs one that's |
| // not shown, so it can show it. The mouse position is remembered when |
| // the menu is shown. |
| 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()); |
| |
| // Move the mouse to where the first menu item will be shown, |
| // and show the menu. |
| 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()); |
| |
| // Synthesize an event at the mouse position when the menu was opened. |
| // It should be ignored, and selected item shouldn't change. |
| 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()); |
| |
| // Synthesize an event at a slightly different mouse position. It |
| // should cause the item under the cursor to be selected. |
| location.Offset(0, 1); |
| ProcessMouseMoved( |
| submenu, ui::MouseEvent(ui::EventType::kMouseMoved, location, location, |
| ui::EventTimeForNow(), 0, 0)); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| } |
| |
| // Tests that when an asynchronous menu receives a cancel event, that it |
| // closes. |
| 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()); |
| } |
| |
| // TODO(pkasting): The test below fails most of the time on Wayland; not clear |
| // it's important to support this case. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) && !BUILDFLAG(IS_OZONE_WAYLAND) |
| class DesktopMenuControllerTest : public MenuControllerTest { |
| public: |
| // MenuControllerTest: |
| void SetUp() override { |
| set_native_widget_type(NativeWidgetType::kDesktop); |
| MenuControllerTest::SetUp(); |
| } |
| }; |
| |
| // Tests that menus without parent widgets do not crash in |
| // MenuPreTargetHandler. Having neither parent nor context pointers when |
| // creating a Widget is only valid in desktop Aura. |
| TEST_F(DesktopMenuControllerTest, RunWithoutWidgetDoesntCrash) { |
| ExitMenuRun(); |
| menu_controller()->Run(nullptr, nullptr, menu_item(), gfx::Rect(), |
| MenuAnchorPosition::kTopLeft); |
| } |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) && !BUILDFLAG(IS_OZONE_WAYLAND) |
| |
| // Tests that if a MenuController is destroying during drag/drop, and another |
| // MenuController becomes active, that the exiting of drag does not cause a |
| // crash. |
| TEST_F(MenuControllerTest, MenuControllerReplacedDuringDrag) { |
| // Build the menu so that the appropriate root window is available to set |
| // the drag drop client on. |
| AddButtonMenuItems(/*single_child=*/false); |
| TestDragDropClient drag_drop_client(base::BindRepeating( |
| &MenuControllerTest::TestMenuControllerReplacementDuringDrag, |
| base::Unretained(this))); |
| aura::client::SetDragDropClient( |
| GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client); |
| StartDrag(); |
| } |
| |
| // Tests that if a CancelAll is called during drag-and-drop that it does not |
| // destroy the MenuController. On Windows and Linux this destruction also |
| // destroys the Widget used for drag-and-drop, thereby ending the drag. |
| TEST_F(MenuControllerTest, CancelAllDuringDrag) { |
| // Build the menu so that the appropriate root window is available to set |
| // the drag drop client on. |
| AddButtonMenuItems(/*single_child=*/false); |
| TestDragDropClient drag_drop_client(base::BindRepeating( |
| &MenuControllerTest::TestCancelAllDuringDrag, base::Unretained(this))); |
| aura::client::SetDragDropClient( |
| GetRootWindow(menu_item()->GetSubmenu()->GetWidget()), &drag_drop_client); |
| StartDrag(); |
| } |
| |
| // Tests that capture is restored to the submenu after a drag and drop. |
| TEST_F(MenuControllerTest, RestoreCaptureAfterDrag) { |
| // Build the menu so that the appropriate root window is available to set |
| // the drag drop client on. |
| AddButtonMenuItems(/*single_child=*/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(); |
| |
| // TODO(crbug.com/375959961): For X11, the menu is closed on drag completion |
| // because the native widget's state is not properly updated. |
| EXPECT_NE(base_host->HasCapture(), BUILDFLAG(IS_OZONE_X11)); |
| } |
| |
| // Tests that capture is not restored to the submenu after a drag and drop where |
| // the submenu is no longer showing. |
| TEST_F(MenuControllerTest, DontRestoreCaptureOnHiddenHostAfterDrag) { |
| // Build the menu so that the appropriate root window is available to set |
| // the drag drop client on. |
| AddButtonMenuItems(/*single_child=*/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()); |
| } |
| |
| // Tests that capture is not given to the submenu after drag and drop if the |
| // submenu didn't have capture before. |
| TEST_F(MenuControllerTest, HostWithoutCaptureAfterDrag) { |
| // Build the menu so that the appropriate root window is available to set |
| // the drag drop client on. |
| AddButtonMenuItems(/*single_child=*/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()); |
| } |
| |
| // Tests that when releasing the ref on ViewsDelegate and MenuController is |
| // deleted, that shutdown occurs without crashing. |
| TEST_F(MenuControllerTest, DestroyedDuringViewsRelease) { |
| ExitMenuRun(); |
| menu_controller()->Run(owner(), nullptr, menu_item(), gfx::Rect(), |
| MenuAnchorPosition::kTopLeft); |
| TestDestroyedDuringViewsRelease(); |
| } |
| |
| // Tests that when a context menu is opened above an empty menu item, and a |
| // right-click occurs over the empty item, that the bottom menu is not hidden, |
| // that a request to relaunch the context menu is received, and that |
| // subsequently pressing ESC does not crash the browser. |
| TEST_F(MenuControllerTest, RepostEventToEmptyMenuItem) { |
| // Setup a submenu. Additionally hook up appropriate Widget and View |
| // containers, with bounds, so that hit testing works. |
| ShowSubmenu(); |
| SubmenuView* const base_submenu = menu_item()->GetSubmenu(); |
| menu_host_for_submenu(base_submenu) |
| ->SetContentsView(base_submenu->GetScrollViewContainer()); |
| |
| // Build the submenu to have an empty menu item. Additionally hook up |
| // appropriate Widget and View containers with bounds, so that hit testing |
| // works. |
| 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()); |
| // TODO(pkasting): The bounds manipulation in this whole test is suspicious; |
| // understand it more deeply, see why the lambda here is needed and if it |
| // can be removed or any of this test simplified/clarified. |
| ShowSubmenu(submenu_view, [&](auto& params) { params.bounds = bounds; }); |
| menu_host_for_submenu(submenu_view) |
| ->SetContentsView(submenu_view->GetScrollViewContainer()); |
| |
| // Set that the last selection target was the item which launches the |
| // submenu, as the empty item can never become a target. |
| SetPendingStateItem(submenu_item); |
| |
| // Nest a context menu. |
| 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); |
| |
| // Press down outside of the context menu, and within the empty menu item. |
| // This should close the first context menu. |
| 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()); |
| |
| // While the current state is the menu item which launched the sub menu, |
| // cause a drag in the empty menu item. This should not hide the menu. |
| 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); |
| |
| // Release the mouse in the empty menu item, triggering a context menu |
| // request. |
| 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); |
| |
| // Nest a context menu. |
| 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); |
| |
| // The escape key should only close the nested menu. SelectByChar should not |
| // crash. |
| TestAsyncEscapeKey(); |
| EXPECT_EQ(nested_controller_delegate_2->on_menu_closed_called(), 1); |
| EXPECT_EQ(menu_controller_delegate(), current_controller_delegate()); |
| } |
| |
| #if BUILDFLAG(IS_OZONE) |
| // Tests that if a context menu is opened above a submenu from a top level |
| // bookmark folder with no parent, and a right-click occurs over the submenu, |
| // the folder does not get dismissed. This is a regression test for |
| // https://crbug.com/446633193 and https://crbug.com/446647004 where a top level |
| // empty folder causes issues. |
| TEST_F(MenuControllerTest, |
| TopLevelBookmarkFolderContextMenuShouldNotDismissFolder) { |
| // Override the platform property to force |
| // PlatformSetsParentForNonTopLevelWindows() |
| // to return true for this test. |
| 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()); |
| |
| // Setup a submenu. Additionally hook up appropriate Widget and View |
| // containers, with bounds, so that hit testing works. |
| 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()); |
| |
| // Create context menu |
| 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 // BUILDFLAG(IS_OZONE) |
| |
| // Drag the mouse from an external view into a menu |
| // When the mouse leaves the menu while still in the process of dragging |
| // the menu item view highlight should turn off |
| 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(); |
| |
| // Begin drag on an external view |
| drag_view->OnMousePressed(ui::MouseEvent( |
| ui::EventType::kMousePressed, press_location, press_location, |
| ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0)); |
| |
| // Drag into a menu item |
| ProcessMouseDragged( |
| submenu, |
| ui::MouseEvent(ui::EventType::kMouseDragged, drag_location, drag_location, |
| ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0)); |
| EXPECT_TRUE(first_item->IsSelected()); |
| |
| // Drag out of the menu item |
| 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()); |
| |
| // Complete drag with release |
| ProcessMouseReleased( |
| submenu, ui::MouseEvent(ui::EventType::kMouseReleased, kReleaseLocation, |
| kReleaseLocation, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, 0)); |
| } |
| |
| // Tests that |MenuHost::InitParams| are correctly forwarded to the created |
| // |aura::Window|. |
| 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); |
| } |
| |
| // Tests that |aura::Window| has the correct properties when a context menu is |
| // shown. |
| TEST_F(MenuControllerTest, ContextMenuInitializesAuraWindowWhenShown) { |
| // Checking that context menu properties are calculated correctly. |
| 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); |
| |
| // Checking that child menu properties are calculated correctly. |
| 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); |
| } |
| |
| // Tests that |aura::Window| has the correct properties when a root or a child |
| // menu is shown. |
| TEST_F(MenuControllerTest, RootAndChildMenusInitializeAuraWindowWhenShown) { |
| // Checking that root menu properties are calculated correctly. |
| 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); |
| |
| // Checking that child menu properties are calculated correctly. |
| 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); |
| |
| // Try to reposition the existing menu. Its anchor must change. |
| 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); |
| // New anchor mustn't be the same as the old one. |
| EXPECT_NE(anchor->anchor_rect, anchor_rect); |
| } |
| |
| // Test that if `SetTriggerActionWithNonIconChildViews` true that even with |
| // child views the click will be registered. Detect that the click was |
| // registered by checking if the TestMenuControllerDelegate received the signal |
| // that the menu should be closed. |
| 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)); |
| // No signal when there's a child view and |
| // SetTriggerActionWithNonIconChildViews is false. |
| 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)); |
| // We should receive a signal to close the menu when |
| // SetTriggerActionWithNonIconChildViews is true. |
| EXPECT_EQ(menu_controller_delegate()->on_menu_closed_called(), 1); |
| // Because the menu has been closed, destroy the menu controller delegate as |
| // well to avoid dangling pointers to the menu. |
| DestroyMenuControllerDelegate(); |
| } |
| |
| #endif // defined(USE_AURA) |
| |
| // Tests that having the MenuController deleted during OnMousePressed does not |
| // cause a crash. ASAN bots should not detect use-after-free in |
| // MenuController. |
| TEST_F(MenuControllerTest, NoUseAfterFreeWhenMenuCanceledOnMousePress) { |
| DestroyMenuControllerOnMenuClosed(menu_controller_delegate()); |
| |
| // Creating own MenuItem for a minimal test environment. |
| 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); |
| |
| // Simulate a mouse press in the middle of the |closing_widget|. |
| 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))); |
| |
| // Close to remove observers before test TearDown. |
| 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(/*single_child=*/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(/*single_child=*/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); |
| |
| // This simulates how buttons are nested in views in the main app menu. |
| auto* const container_view = item4->AddChildView(std::make_unique<View>()); |
| container_view->GetViewAccessibility().SetRole(ax::mojom::Role::kMenu); |
| |
| // There's usually a label before the traversable elements. |
| container_view->AddChildView(std::make_unique<Label>()); |
| |
| // Add two focusable buttons (buttons in menus are always focusable). |
| 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(/*single_child=*/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); |
| |
| // Simulate a menu model update. |
| item1->SetEnabled(false); |
| button1->SetEnabled(false); |
| const MenuItemView* const item6 = menu_item()->AppendMenuItem(6, u"Six"); |
| menu_item()->RemoveMenuItem(item2); |
| MenuChildrenChanged(menu_item()); |
| |
| // Verify that disabled menu items no longer have PosInSet or SetSize. |
| 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); |
| } |
| |
| // Tests that a menu opened asynchronously, will notify its |
| // MenuControllerDelegate when accessibility performs a do default action. |
| 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 that the kSelectedChildrenChanged event is emitted on |
| // the root menu item when the selected menu item changes. |
| 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); |
| |
| // Arrow down to select an item checking the event has been emitted. |
| 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 that in accessibility mode disabled menu items are taken into account |
| // during items indices assignment. |
| 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) |
| // This test exercises a Mac-specific behavior, by which hotkeys using |
| // modifiers cause menus to close and the hotkeys to be handled by the browser |
| // window. This specific test case tries using cmd-ctrl-f, which normally |
| // means "Fullscreen". |
| 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()); // ensure the command press itself doesn't cancel |
| |
| 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) { |
| // Create a submenu. |
| 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"); |
| |
| // Open the menu and select the menu item that has a submenu. |
| OpenMenu(menu_item()); |
| SetState(child_menu); |
| EXPECT_EQ(1, pending_state_item()->GetCommand()); |
| EXPECT_EQ(nullptr, submenu->host()); |
| |
| // Dispatch a key to open the submenu. |
| 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()); |
| } |
| |
| // Simple test to ensure child menu open direction is correctly set and |
| // retrieved. |
| TEST_F(MenuControllerTest, ChildMenuOpenDirectionStateUpdatesCorrectly) { |
| // Before any open directions have been set, the leading direction should |
| // be used as the default for any depth value. |
| EXPECT_EQ(MenuController::MenuOpenDirection::kLeading, |
| GetChildMenuOpenDirectionAtDepth(0)); |
| EXPECT_EQ(MenuController::MenuOpenDirection::kLeading, |
| GetChildMenuOpenDirectionAtDepth(1)); |
| EXPECT_EQ(MenuController::MenuOpenDirection::kLeading, |
| GetChildMenuOpenDirectionAtDepth(10)); |
| |
| // Set alternating open directions, this should be correctly reflected in |
| // subsequent open direction queries. |
| 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); |
| |
| // Ensure that the menu host has the correct z order level. |
| EXPECT_EQ(ui::ZOrderLevel::kFloatingWindow, host->GetZOrderLevel()); |
| } |
| |
| // Tests that updating an empty menu's children, while the "empty item" |
| // placeholder is selected correctly transfers selection to the parent. |
| TEST_F(MenuControllerTest, RemoveEmptyMenuMenuItemWhileSelected) { |
| views::MenuItemView* const root_menu = menu_item(); |
| OpenMenu(root_menu); |
| |
| // Remove all items. |
| root_menu->RemoveAllMenuItems(); |
| root_menu->ChildrenChanged(); |
| SubmenuView* const submenu = root_menu->GetSubmenu(); |
| ASSERT_TRUE(submenu); |
| |
| // The menu should only have an empty-menu menu item as a placeholder. |
| 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()); |
| |
| // Adding a new item will remove the empty-menu menu item. |
| // Selection should be transferred gracefully. |
| views::MenuItemView* const item = root_menu->AppendMenuItem(1, u"item 1"); |
| item->SetVisible(true); |
| root_menu->ChildrenChanged(); |
| |
| // The parent should be selected and the remaining view should be the added |
| // menu item. |
| EXPECT_TRUE(root_menu->IsSelected()); |
| ASSERT_EQ(1u, submenu->children().size()); |
| EXPECT_EQ(item, submenu->children()[0]); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| // The following tests are only relevant on platforms that select the first |
| // menu item when a menu is opened via keyboard input. |
| TEST_F(MenuControllerTest, FirstMenuItemSelectedWhenOpenedFromKeyboard) { |
| // Use existing menu items from the test setup. |
| MenuItemView* root = menu_item(); |
| MenuItemView* item1 = root->GetSubmenu()->GetMenuItemAt(0); |
| MenuItemView* item2 = root->GetSubmenu()->GetMenuItemAt(1); |
| MenuItemView* item3 = root->GetSubmenu()->GetMenuItemAt(2); |
| |
| // Open the menu via keyboard input. |
| menu_controller()->Run(owner(), /*button_controller=*/nullptr, root, |
| gfx::Rect(), MenuAnchorPosition::kTopLeft, |
| ui::mojom::MenuSourceType::kKeyboard, |
| /*menu_type=*/MenuController::MenuType::kNormal, |
| /*is_nested_drag=*/false); |
| |
| EXPECT_TRUE(item1->IsSelected()); |
| EXPECT_FALSE(item2->IsSelected()); |
| EXPECT_FALSE(item3->IsSelected()); |
| } |
| |
| TEST_F(MenuControllerTest, NoItemSelectedWhenOpenedFromMouse) { |
| // Use existing menu items from the test setup. |
| MenuItemView* root = menu_item(); |
| MenuItemView* item1 = root->GetSubmenu()->GetMenuItemAt(0); |
| MenuItemView* item2 = root->GetSubmenu()->GetMenuItemAt(1); |
| MenuItemView* item3 = root->GetSubmenu()->GetMenuItemAt(2); |
| |
| // Open the menu via mouse click. |
| menu_controller()->Run(owner(), /*button_controller=*/nullptr, root, |
| gfx::Rect(), MenuAnchorPosition::kTopLeft, |
| ui::mojom::MenuSourceType::kMouse, |
| /*menu_type=*/MenuController::MenuType::kNormal, |
| /*is_nested_drag=*/false); |
| |
| EXPECT_FALSE(item1->IsSelected()); |
| EXPECT_FALSE(item2->IsSelected()); |
| EXPECT_FALSE(item3->IsSelected()); |
| } |
| |
| TEST_F(MenuControllerTest, |
| FirstMenuItemButtonHotTrackedWhenOpenedFromKeyboard) { |
| // Set up a menu with one button in the first menu item. |
| 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")) |
| // This is an in-menu button. Hence it must be always focusable. |
| ->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| |
| // Open the menu via keyboard input. |
| menu_controller()->Run(owner(), /*button_controller=*/nullptr, menu_item(), |
| gfx::Rect(), MenuAnchorPosition::kTopLeft, |
| ui::mojom::MenuSourceType::kKeyboard, |
| /*menu_type=*/MenuController::MenuType::kNormal, |
| /*is_nested_drag=*/false); |
| |
| EXPECT_TRUE(first_item->IsSelected()); |
| |
| // Access the button inside the first menu item. |
| GET_CHILD_BUTTON(button1, buttons_view, 0); |
| |
| EXPECT_TRUE(button1->IsHotTracked()); |
| } |
| |
| TEST_F(MenuControllerTest, |
| FirstMenuItemButtonNotHotTrackedWhenOpenedFromMouse) { |
| // Set up a menu with one button in the first menu item. |
| 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")) |
| // This is an in-menu button. Hence it must be always focusable. |
| ->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| |
| // Open the menu via mouse input. |
| menu_controller()->Run(owner(), /*button_controller=*/nullptr, menu_item(), |
| gfx::Rect(), MenuAnchorPosition::kTopLeft, |
| ui::mojom::MenuSourceType::kMouse, |
| /*menu_type=*/MenuController::MenuType::kNormal, |
| /*is_nested_drag=*/false); |
| |
| EXPECT_FALSE(first_item->IsSelected()); |
| |
| // Access the button inside the first menu item. |
| GET_CHILD_BUTTON(button1, buttons_view, 0); |
| |
| EXPECT_FALSE(button1->IsHotTracked()); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| } // namespace views |