blob: ab7a6b49152a5ac156ca6325dbbba8bfb6a17b18 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "base/test/bind.h"
#include "build/buildflag.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/toolbar/bookmark_sub_menu_model.h"
#include "chrome/browser/ui/views/toolbar/browser_app_menu_button.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_model_observer.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/base/test/ui_controls.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/view_utils.h"
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
// Observes a widget to track when a drag is complete.
class DragWaiter : public views::WidgetObserver {
public:
explicit DragWaiter(views::Widget* widget) : widget_(widget) {
widget_->AddObserver(this);
}
~DragWaiter() override {
if (widget_) {
widget_->RemoveObserver(this);
}
}
DragWaiter(const DragWaiter&) = delete;
void operator=(const DragWaiter&) = delete;
void OnWidgetDestroying(views::Widget* widget) override {
if (drag_loop_ && drag_loop_->running()) {
drag_loop_->Quit();
}
widget_->RemoveObserver(this);
widget_ = nullptr;
}
void OnWidgetDragComplete(views::Widget* widget) override {
drag_complete_ = true;
if (drag_loop_ && drag_loop_->running()) {
drag_loop_->Quit();
}
}
void Wait() {
if (!drag_complete_) {
drag_loop_ = std::make_unique<base::RunLoop>(
base::RunLoop::Type::kNestableTasksAllowed);
drag_loop_->Run();
}
}
private:
raw_ptr<views::Widget> widget_ = nullptr;
bool drag_complete_ = false;
std::unique_ptr<base::RunLoop> drag_loop_;
};
class AppMenuDragAndDropInteractiveTest : public InteractiveBrowserTest {
public:
using DragObserver =
views::test::PollingViewObserver<bool, views::MenuItemView>;
AppMenuDragAndDropInteractiveTest() {
// Disabled to hide the comparison tables submenu.
scoped_feature_list_.InitAndDisableFeature(
commerce::kProductSpecifications);
}
~AppMenuDragAndDropInteractiveTest() override = default;
AppMenuDragAndDropInteractiveTest(const AppMenuDragAndDropInteractiveTest&) =
delete;
void operator=(const AppMenuDragAndDropInteractiveTest&) = delete;
// Names the menu item at a given index of `target_view`'s submenu.
// The usual `NameChildView` doesn't work for this case because submenus
// have their own widgets and are not part of the parent menu item's view
// hierarchy.
auto NameSubmenuChild(ElementSpecifier target_view,
std::string name,
size_t index) {
return Steps(NameViewRelative(
target_view, name,
base::BindLambdaForTesting([=](views::View* view) -> views::View* {
views::MenuItemView* const menu =
views::AsViewClass<views::MenuItemView>(view);
CHECK(menu);
CHECK(menu->HasSubmenu());
return menu->GetSubmenu()->GetMenuItemAt(index);
})));
}
// Returns a relative-position callback that gets the top-center point of
// a view. The value is slightly adjusted to prevent rounding errors on
// scaled devices.
auto TopCenter() {
return base::BindLambdaForTesting([](views::View* el) {
return el->GetBoundsInScreen().top_center() + gfx::Vector2d(0, 2);
});
}
auto CenterPoint() {
return base::BindLambdaForTesting([](views::View* view) {
return view->GetBoundsInScreen().CenterPoint();
});
}
auto RegisterDragWaiter(ElementSpecifier target_view) {
return WithView(target_view, [&](views::View* view) {
auto* menu = static_cast<views::MenuItemView*>(view);
drag_waiter_ = std::make_unique<DragWaiter>(menu->GetWidget());
});
}
auto RemoveDragWaiter() {
return Do([&]() { drag_waiter_.reset(); });
}
// The original "DragMouseTo" method has issues on various platforms. This
// method uses custom logic to perform the drag.
auto DragMouseTo(ElementSpecifier dragged_view,
ElementSpecifier target_view,
base::RepeatingCallback<gfx::Point(views::View*)> pos) {
return Steps(
RegisterDragWaiter(dragged_view),
WithView(target_view,
[this, pos = std::move(pos)](views::View* view) {
base::RunLoop press_loop(
base::RunLoop::Type::kNestableTasksAllowed);
EXPECT_TRUE(ui_controls::SendMouseEventsNotifyWhenDone(
ui_controls::MouseButton::LEFT,
ui_controls::MouseButtonState::DOWN,
press_loop.QuitClosure()));
press_loop.Run();
// On Mac, no initial mouse movement is needed in order to test drag events. The
// test element can be moved directly to the final target location.
#if !BUILDFLAG(IS_MAC)
gfx::Rect bounds = view->GetBoundsInScreen();
gfx::Point start_location(bounds.width() / 2,
bounds.height() / 2);
gfx::Point initial_target_location =
start_location + gfx::Vector2d(10, 10);
// Send an initial mouse movement to start the drag.
EXPECT_TRUE(
ui_controls::SendMouseMove(initial_target_location.x(),
initial_target_location.y()));
#endif //! BUILDFLAG(IS_MAC)
// Send another mouse movement to the target desitnation.
gfx::Point target_location = std::move(pos).Run(view);
EXPECT_TRUE(ui_controls::SendMouseMove(target_location.x(),
target_location.y()));
// Release the mouse movement.
EXPECT_TRUE(ui_controls::SendMouseEvents(
ui_controls::MouseButton::LEFT,
ui_controls::MouseButtonState::UP));
// Await drag completion.
drag_waiter_->Wait();
}),
RemoveDragWaiter());
}
private:
std::unique_ptr<DragWaiter> drag_waiter_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// TODO(crbug.com/375959961): For X11, the menu is always closed on drag
// completion because the native widget's state is not properly updated.
// TODO(crbug.com/388531778): DND tests are flaky on Windows. This should be
// re-enabled once de-flaked.
#if BUILDFLAG(IS_OZONE_X11) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_OZONE_WAYLAND)
#define MAYBE_DISABLED(test_name) DISABLED_##test_name
#else
#define MAYBE_DISABLED(test_name) test_name
#endif
IN_PROC_BROWSER_TEST_F(AppMenuDragAndDropInteractiveTest,
MAYBE_DISABLED(BookmarksDragAndDrop)) {
// Add two bookmarks nodes to the bookmarks bar.
bookmarks::BookmarkModel* const model =
BookmarkModelFactory::GetForBrowserContext(browser()->profile());
const bookmarks::BookmarkNode* const bb_node = model->bookmark_bar_node();
model->AddFolder(bb_node, 0, u"a");
model->AddFolder(bb_node, 1, u"b");
static constexpr std::string kANodeMenuId = "a_node";
static constexpr std::string kBNodeMenuId = "b_node";
RunTestSequence(
// Open the bookmarks menu in the App menu.
PressButton(kToolbarAppMenuButtonElementId),
SelectMenuItem(AppMenuModel::kBookmarksMenuItem),
// The 9th and 10th menu items of the bookmarks menu should be the two
// bookmarks added to the bookmarks bar.
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kANodeMenuId, 8u),
CheckViewProperty(kANodeMenuId, &views::MenuItemView::title, u"a"),
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kBNodeMenuId, 9u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"b"),
// Drag bookmark "b" above bookmark "a".
MoveMouseTo(kBNodeMenuId),
DragMouseTo(kBNodeMenuId, kANodeMenuId, TopCenter()),
// The order of the bookmarks should now be swapped.
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kANodeMenuId, 9u),
CheckViewProperty(kANodeMenuId, &views::MenuItemView::title, u"a"),
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kBNodeMenuId, 8u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"b"));
}
IN_PROC_BROWSER_TEST_F(AppMenuDragAndDropInteractiveTest,
MAYBE_DISABLED(BookmarksDragAndDropToNestedFolder)) {
// Add two bookmarks nodes to the bookmarks bar.
bookmarks::BookmarkModel* const model =
BookmarkModelFactory::GetForBrowserContext(browser()->profile());
const bookmarks::BookmarkNode* const bb_node = model->bookmark_bar_node();
model->AddFolder(bb_node, 0, u"a");
model->AddFolder(bb_node, 1, u"b");
static constexpr std::string kANodeMenuId = "a_node";
static constexpr std::string kBNodeMenuId = "b_node";
RunTestSequence(
// Open the bookmarks menu in the App menu.
PressButton(kToolbarAppMenuButtonElementId),
SelectMenuItem(AppMenuModel::kBookmarksMenuItem),
// The 9th and 10th menu items of the bookmarks menu should be the two
// bookmarks added to the bookmarks bar.
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kANodeMenuId, 8u),
CheckViewProperty(kANodeMenuId, &views::MenuItemView::title, u"a"),
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kBNodeMenuId, 9u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"b"),
// Drag bookmark "a" onto bookmark folder "b".
MoveMouseTo(kBNodeMenuId),
DragMouseTo(kBNodeMenuId, kANodeMenuId, CenterPoint()),
// Bookmark folder "b" should now be the 9th menu item, and bookmark "a"
// should be in its submenu.
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kANodeMenuId, 8u),
CheckViewProperty(kANodeMenuId, &views::MenuItemView::title, u"a"),
NameSubmenuChild(kANodeMenuId, kBNodeMenuId, 0u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"b"));
}
IN_PROC_BROWSER_TEST_F(AppMenuDragAndDropInteractiveTest,
MAYBE_DISABLED(BookmarksDragAndDropFromNestedFolder)) {
// Add one bookmark folder to the bookmarks bar, and add a bookmark node to
// the new folder.
bookmarks::BookmarkModel* const model =
BookmarkModelFactory::GetForBrowserContext(browser()->profile());
const bookmarks::BookmarkNode* const bb_node = model->bookmark_bar_node();
model->AddFolder(bb_node, 0, u"a");
model->AddFolder(bb_node->children()[0].get(), 0, u"b");
static constexpr std::string kANodeMenuId = "a_node";
static constexpr std::string kBNodeMenuId = "b_node";
RunTestSequence(
// Open the bookmarks menu in the App menu.
PressButton(kToolbarAppMenuButtonElementId),
SelectMenuItem(AppMenuModel::kBookmarksMenuItem),
// The 9th menu item of the bookmarks menu should be the bookmark "a".
// The 1st menu item of bookmark folder "a" should be bookmark "b".
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kANodeMenuId, 8u),
CheckViewProperty(kANodeMenuId, &views::MenuItemView::title, u"a"),
SelectMenuItem(kANodeMenuId), // Open bookmark folder "a".
NameSubmenuChild(kANodeMenuId, kBNodeMenuId, 0u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"b"),
// Drag bookmark "b" out of bookmark folder "a".
MoveMouseTo(kBNodeMenuId),
DragMouseTo(kBNodeMenuId, kANodeMenuId, TopCenter()),
// Bookmark "b" should now be the 9th menu item and bookmark folder "a"
// should be the 10th.
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kBNodeMenuId, 8u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"b"),
NameSubmenuChild(AppMenuModel::kBookmarksMenuItem, kBNodeMenuId, 9u),
CheckViewProperty(kBNodeMenuId, &views::MenuItemView::title, u"a"));
}
using AppMenuInteractiveTest = InteractiveBrowserTest;
IN_PROC_BROWSER_TEST_F(AppMenuInteractiveTest, DoNotCrashOnBrowserClose) {
RunTestSequence(
// Open the App menu.
PressButton(kToolbarAppMenuButtonElementId),
// Close all browsers, ensure the browser process does not crash.
Do([]() {
chrome::CloseAllBrowsers();
ui_test_utils::WaitForBrowserToClose();
}));
}