blob: 1b0383dbf26d2d607c4ddaddf8ca5c10f3d815dd [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/button/menu_button.h"
#include <memory>
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/animation/test/ink_drop_host_view_test_api.h"
#include "ui/views/animation/test/test_ink_drop.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/menu_button_controller.h"
#include "ui/views/drag_controller.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/widget_utils.h"
#if defined(USE_AURA)
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/client/drag_drop_client_observer.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/events/event.h"
#include "ui/events/event_handler.h"
#endif
using base::ASCIIToUTF16;
namespace views {
class TestMenuButton : public MenuButton {
public:
TestMenuButton()
: TestMenuButton(base::BindRepeating(&TestMenuButton::ButtonPressed,
base::Unretained(this))) {}
explicit TestMenuButton(PressedCallback callback)
: MenuButton(std::move(callback), std::u16string(u"button")) {}
TestMenuButton(const TestMenuButton&) = delete;
TestMenuButton& operator=(const TestMenuButton&) = delete;
~TestMenuButton() override = default;
bool clicked() const { return clicked_; }
Button::ButtonState last_state() const { return last_state_; }
ui::EventType last_event_type() const { return last_event_type_; }
void Reset() {
clicked_ = false;
last_state_ = Button::STATE_NORMAL;
last_event_type_ = ui::ET_UNKNOWN;
}
private:
void ButtonPressed(const ui::Event& event) {
clicked_ = true;
last_state_ = GetState();
last_event_type_ = event.type();
}
bool clicked_ = false;
Button::ButtonState last_state_ = Button::STATE_NORMAL;
ui::EventType last_event_type_ = ui::ET_UNKNOWN;
};
class MenuButtonTest : public ViewsTestBase {
public:
MenuButtonTest() = default;
MenuButtonTest(const MenuButtonTest&) = delete;
MenuButtonTest& operator=(const MenuButtonTest&) = delete;
~MenuButtonTest() override = default;
void TearDown() override {
generator_.reset();
if (widget_ && !widget_->IsClosed())
widget_->Close();
ViewsTestBase::TearDown();
}
protected:
Widget* widget() { return widget_; }
TestMenuButton* button() { return button_; }
ui::test::EventGenerator* generator() { return generator_.get(); }
test::TestInkDrop* ink_drop() { return ink_drop_; }
gfx::Point GetOutOfButtonLocation() const {
return gfx::Point(button_->x() - 1, button_->y() - 1);
}
void CreateWidget() {
DCHECK(!widget_);
widget_ = new Widget;
Widget::InitParams params =
CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.bounds = gfx::Rect(0, 0, 200, 200);
widget_->Init(std::move(params));
}
void ConfigureMenuButton(std::unique_ptr<TestMenuButton> button) {
CreateWidget();
generator_ =
std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_));
// Set initial mouse location in a consistent way so that the menu button we
// are about to create initializes its hover state in a consistent manner.
generator_->set_current_screen_location(gfx::Point(10, 10));
button_ = widget_->SetContentsView(std::move(button));
button_->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
auto ink_drop = std::make_unique<test::TestInkDrop>();
ink_drop_ = ink_drop.get();
test::InkDropHostViewTestApi(button_).SetInkDrop(std::move(ink_drop));
widget_->Show();
}
private:
Widget* widget_ = nullptr; // Owned by self.
TestMenuButton* button_ = nullptr; // Owned by |widget_|.
std::unique_ptr<ui::test::EventGenerator> generator_;
test::TestInkDrop* ink_drop_ = nullptr; // Owned by |button_|.
};
// A Button that will acquire a PressedLock in the pressed callback and
// optionally release it as well.
class PressStateButton : public TestMenuButton {
public:
explicit PressStateButton(bool release_lock)
: TestMenuButton(base::BindRepeating(&PressStateButton::ButtonPressed,
base::Unretained(this))),
release_lock_(release_lock) {}
PressStateButton(const PressStateButton&) = delete;
PressStateButton& operator=(const PressStateButton&) = delete;
~PressStateButton() override = default;
void ReleasePressedLock() { pressed_lock_.reset(); }
private:
void ButtonPressed() {
pressed_lock_ = button_controller()->TakeLock();
if (release_lock_)
ReleasePressedLock();
}
bool release_lock_;
std::unique_ptr<MenuButtonController::PressedLock> pressed_lock_;
};
// Basic implementation of a DragController, to test input behaviour for
// MenuButtons that can be dragged.
class TestDragController : public DragController {
public:
TestDragController() = default;
TestDragController(const TestDragController&) = delete;
TestDragController& operator=(const TestDragController&) = delete;
~TestDragController() override = default;
void WriteDragDataForView(View* sender,
const gfx::Point& press_pt,
ui::OSExchangeData* data) override {}
int GetDragOperationsForView(View* sender, const gfx::Point& p) override {
return ui::DragDropTypes::DRAG_MOVE;
}
bool CanStartDragForView(View* sender,
const gfx::Point& press_pt,
const gfx::Point& p) override {
return true;
}
};
#if defined(USE_AURA)
// Basic implementation of a DragDropClient, tracking the state of the drag
// operation. While dragging addition mouse events are consumed, preventing the
// target view from receiving them.
class TestDragDropClient : public aura::client::DragDropClient,
public ui::EventHandler {
public:
TestDragDropClient();
TestDragDropClient(const TestDragDropClient&) = delete;
TestDragDropClient& operator=(const TestDragDropClient&) = delete;
~TestDragDropClient() override;
// aura::client::DragDropClient:
int StartDragAndDrop(std::unique_ptr<ui::OSExchangeData> data,
aura::Window* root_window,
aura::Window* source_window,
const gfx::Point& screen_location,
int operation,
ui::mojom::DragEventSource source) override;
void DragCancel() override;
bool IsDragDropInProgress() override;
void AddObserver(aura::client::DragDropClientObserver* observer) override {}
void RemoveObserver(aura::client::DragDropClientObserver* observer) override {
}
// ui::EventHandler:
void OnMouseEvent(ui::MouseEvent* event) override;
private:
// True while receiving ui::LocatedEvents for drag operations.
bool drag_in_progress_ = false;
// Target window where drag operations are occurring.
aura::Window* target_ = nullptr;
};
TestDragDropClient::TestDragDropClient() = default;
TestDragDropClient::~TestDragDropClient() = default;
int TestDragDropClient::StartDragAndDrop(
std::unique_ptr<ui::OSExchangeData> data,
aura::Window* root_window,
aura::Window* source_window,
const gfx::Point& screen_location,
int operation,
ui::mojom::DragEventSource source) {
if (IsDragDropInProgress())
return ui::DragDropTypes::DRAG_NONE;
drag_in_progress_ = true;
target_ = root_window;
return operation;
}
void TestDragDropClient::DragCancel() {
drag_in_progress_ = false;
}
bool TestDragDropClient::IsDragDropInProgress() {
return drag_in_progress_;
}
void TestDragDropClient::OnMouseEvent(ui::MouseEvent* event) {
if (!IsDragDropInProgress())
return;
switch (event->type()) {
case ui::ET_MOUSE_DRAGGED:
event->StopPropagation();
break;
case ui::ET_MOUSE_RELEASED:
drag_in_progress_ = false;
event->StopPropagation();
break;
default:
break;
}
}
#endif // defined(USE_AURA)
// Tests if the callback is called correctly when a mouse click happens on a
// MenuButton.
TEST_F(MenuButtonTest, ActivateDropDownOnMouseClick) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
generator()->ClickLeftButton();
EXPECT_TRUE(button()->clicked());
EXPECT_EQ(Button::STATE_HOVERED, button()->last_state());
}
TEST_F(MenuButtonTest, ActivateOnKeyPress) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
EXPECT_FALSE(button()->clicked());
button()->OnKeyPressed(ui::KeyEvent(
ui::ET_KEY_PRESSED, ui::KeyboardCode::VKEY_SPACE, ui::DomCode::SPACE, 0));
EXPECT_TRUE(button()->clicked());
button()->Reset();
EXPECT_FALSE(button()->clicked());
button()->OnKeyPressed(ui::KeyEvent(ui::ET_KEY_PRESSED,
ui::KeyboardCode::VKEY_RETURN,
ui::DomCode::ENTER, 0));
EXPECT_EQ(PlatformStyle::kReturnClicksFocusedControl, button()->clicked());
}
// Tests that the ink drop center point is set from the mouse click point.
TEST_F(MenuButtonTest, InkDropCenterSetFromClick) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
gfx::Point click_point(6, 8);
generator()->MoveMouseTo(click_point);
generator()->ClickLeftButton();
EXPECT_TRUE(button()->clicked());
EXPECT_EQ(click_point, test::InkDropHostViewTestApi(button())
.GetInkDropCenterBasedOnLastEvent());
}
// Tests that the ink drop center point is set from the PressedLock constructor.
TEST_F(MenuButtonTest, InkDropCenterSetFromClickWithPressedLock) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
gfx::Point click_point(11, 7);
ui::MouseEvent click_event(ui::EventType::ET_MOUSE_PRESSED, click_point,
click_point, base::TimeTicks(), 0, 0);
MenuButtonController::PressedLock pressed_lock(button()->button_controller(),
false, &click_event);
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
EXPECT_EQ(click_point, test::InkDropHostViewTestApi(button())
.GetInkDropCenterBasedOnLastEvent());
}
// Test that the MenuButton stays pressed while there are any PressedLocks.
TEST_F(MenuButtonTest, ButtonStateForMenuButtonsWithPressedLocks) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
// Move the mouse over the button; the button should be in a hovered state.
generator()->MoveMouseTo(gfx::Point(10, 10));
EXPECT_EQ(Button::STATE_HOVERED, button()->GetState());
// Introduce a PressedLock, which should make the button pressed.
auto pressed_lock1 = std::make_unique<MenuButtonController::PressedLock>(
button()->button_controller());
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
// Even if we move the mouse outside of the button, it should remain pressed.
generator()->MoveMouseTo(gfx::Point(300, 10));
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
// Creating a new lock should obviously keep the button pressed.
auto pressed_lock2 = std::make_unique<MenuButtonController::PressedLock>(
button()->button_controller());
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
// The button should remain pressed while any locks are active.
pressed_lock1.reset();
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
// Resetting the final lock should return the button's state to normal...
pressed_lock2.reset();
EXPECT_EQ(Button::STATE_NORMAL, button()->GetState());
// ...And it should respond to mouse movement again.
generator()->MoveMouseTo(gfx::Point(10, 10));
EXPECT_EQ(Button::STATE_HOVERED, button()->GetState());
// Test that the button returns to the appropriate state after the press; if
// the mouse ends over the button, the button should be hovered.
pressed_lock1 = button()->button_controller()->TakeLock();
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
pressed_lock1.reset();
EXPECT_EQ(Button::STATE_HOVERED, button()->GetState());
// If the button is disabled before the pressed lock, it should be disabled
// after the pressed lock.
button()->SetState(Button::STATE_DISABLED);
pressed_lock1 = button()->button_controller()->TakeLock();
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
pressed_lock1.reset();
EXPECT_EQ(Button::STATE_DISABLED, button()->GetState());
generator()->MoveMouseTo(gfx::Point(300, 10));
// Edge case: the button is disabled, a pressed lock is added, and then the
// button is re-enabled. It should be enabled after the lock is removed.
pressed_lock1 = button()->button_controller()->TakeLock();
EXPECT_EQ(Button::STATE_PRESSED, button()->GetState());
button()->SetState(Button::STATE_NORMAL);
pressed_lock1.reset();
EXPECT_EQ(Button::STATE_NORMAL, button()->GetState());
}
// Test that the MenuButton does not become pressed if it can be dragged, until
// a release occurs.
TEST_F(MenuButtonTest, DraggableMenuButtonActivatesOnRelease) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
TestDragController drag_controller;
button()->set_drag_controller(&drag_controller);
generator()->PressLeftButton();
EXPECT_FALSE(button()->clicked());
generator()->ReleaseLeftButton();
EXPECT_TRUE(button()->clicked());
EXPECT_EQ(Button::STATE_HOVERED, button()->last_state());
}
TEST_F(MenuButtonTest, InkDropStateForMenuButtonActivationsWithoutCallback) {
ConfigureMenuButton(
std::make_unique<TestMenuButton>(Button::PressedCallback()));
ink_drop()->AnimateToState(InkDropState::ACTION_PENDING);
button()->Activate(nullptr);
EXPECT_EQ(InkDropState::HIDDEN, ink_drop()->GetTargetInkDropState());
}
TEST_F(MenuButtonTest,
InkDropStateForMenuButtonActivationsWithCallbackThatDoesntAcquireALock) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
button()->Activate(nullptr);
EXPECT_EQ(InkDropState::ACTION_TRIGGERED,
ink_drop()->GetTargetInkDropState());
}
TEST_F(
MenuButtonTest,
InkDropStateForMenuButtonActivationsWithCallbackThatDoesntReleaseAllLocks) {
ConfigureMenuButton(std::make_unique<PressStateButton>(false));
button()->Activate(nullptr);
EXPECT_EQ(InkDropState::ACTIVATED, ink_drop()->GetTargetInkDropState());
}
TEST_F(MenuButtonTest,
InkDropStateForMenuButtonActivationsWithCallbackThatReleasesAllLocks) {
ConfigureMenuButton(std::make_unique<PressStateButton>(true));
button()->Activate(nullptr);
EXPECT_EQ(InkDropState::DEACTIVATED, ink_drop()->GetTargetInkDropState());
}
TEST_F(MenuButtonTest, InkDropStateForMenuButtonsWithPressedLocks) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
auto pressed_lock1 = std::make_unique<MenuButtonController::PressedLock>(
button()->button_controller());
EXPECT_EQ(InkDropState::ACTIVATED, ink_drop()->GetTargetInkDropState());
auto pressed_lock2 = std::make_unique<MenuButtonController::PressedLock>(
button()->button_controller());
EXPECT_EQ(InkDropState::ACTIVATED, ink_drop()->GetTargetInkDropState());
pressed_lock1.reset();
EXPECT_EQ(InkDropState::ACTIVATED, ink_drop()->GetTargetInkDropState());
pressed_lock2.reset();
EXPECT_EQ(InkDropState::DEACTIVATED, ink_drop()->GetTargetInkDropState());
}
// Verifies only one ink drop animation is triggered when multiple PressedLocks
// are attached to a MenuButton.
TEST_F(MenuButtonTest, OneInkDropAnimationForReentrantPressedLocks) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
auto pressed_lock1 = std::make_unique<MenuButtonController::PressedLock>(
button()->button_controller());
EXPECT_EQ(InkDropState::ACTIVATED, ink_drop()->GetTargetInkDropState());
ink_drop()->AnimateToState(InkDropState::ACTION_PENDING);
auto pressed_lock2 = std::make_unique<MenuButtonController::PressedLock>(
button()->button_controller());
EXPECT_EQ(InkDropState::ACTION_PENDING, ink_drop()->GetTargetInkDropState());
}
// Verifies the InkDropState is left as ACTIVATED if a PressedLock is active
// before another Activation occurs.
TEST_F(MenuButtonTest,
InkDropStateForMenuButtonWithPressedLockBeforeActivation) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
MenuButtonController::PressedLock lock(button()->button_controller());
button()->Activate(nullptr);
EXPECT_EQ(InkDropState::ACTIVATED, ink_drop()->GetTargetInkDropState());
}
#if defined(USE_AURA)
// Tests that the MenuButton does not become pressed if it can be dragged, and a
// DragDropClient is processing the events.
TEST_F(MenuButtonTest, DraggableMenuButtonDoesNotActivateOnDrag) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
TestDragController drag_controller;
button()->set_drag_controller(&drag_controller);
TestDragDropClient drag_client;
SetDragDropClient(GetContext(), &drag_client);
button()->AddPreTargetHandler(&drag_client,
ui::EventTarget::Priority::kSystem);
generator()->DragMouseBy(10, 0);
EXPECT_FALSE(button()->clicked());
EXPECT_EQ(Button::STATE_NORMAL, button()->last_state());
button()->RemovePreTargetHandler(&drag_client);
}
#endif // USE_AURA
// No touch on desktop Mac. Tracked in http://crbug.com/445520.
#if !defined(OS_APPLE) || defined(USE_AURA)
// Tests if the callback is notified correctly when a gesture tap happens on a
// MenuButton that has a callback.
TEST_F(MenuButtonTest, ActivateDropDownOnGestureTap) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
// Move the mouse outside the menu button so that it doesn't impact the
// button state.
generator()->MoveMouseTo(400, 400);
EXPECT_FALSE(button()->IsMouseHovered());
generator()->GestureTapAt(gfx::Point(10, 10));
// Check that MenuButton has notified the callback, while it was in pressed
// state.
EXPECT_TRUE(button()->clicked());
EXPECT_EQ(Button::STATE_HOVERED, button()->last_state());
// The button should go back to its normal state since the gesture ended.
EXPECT_EQ(Button::STATE_NORMAL, button()->GetState());
}
// Tests that the button enters a hovered state upon a tap down, before becoming
// pressed at activation.
TEST_F(MenuButtonTest, TouchFeedbackDuringTap) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
generator()->PressTouch();
EXPECT_EQ(Button::STATE_HOVERED, button()->GetState());
generator()->ReleaseTouch();
EXPECT_EQ(Button::STATE_HOVERED, button()->last_state());
}
// Tests that a move event that exits the button returns it to the normal state,
// and that the button did not activate the callback.
TEST_F(MenuButtonTest, TouchFeedbackDuringTapCancel) {
ConfigureMenuButton(std::make_unique<TestMenuButton>());
generator()->PressTouch();
EXPECT_EQ(Button::STATE_HOVERED, button()->GetState());
generator()->MoveTouch(gfx::Point(10, 30));
generator()->ReleaseTouch();
EXPECT_EQ(Button::STATE_NORMAL, button()->GetState());
EXPECT_FALSE(button()->clicked());
}
#endif // !defined(OS_APPLE) || defined(USE_AURA)
TEST_F(MenuButtonTest, InkDropHoverWhenShowingMenu) {
ConfigureMenuButton(std::make_unique<PressStateButton>(false));
generator()->MoveMouseTo(GetOutOfButtonLocation());
EXPECT_FALSE(ink_drop()->is_hovered());
generator()->MoveMouseTo(button()->bounds().CenterPoint());
EXPECT_TRUE(ink_drop()->is_hovered());
generator()->PressLeftButton();
EXPECT_FALSE(ink_drop()->is_hovered());
}
TEST_F(MenuButtonTest, InkDropIsHoveredAfterDismissingMenuWhenMouseOverButton) {
auto press_state_button = std::make_unique<PressStateButton>(false);
auto* test_button = press_state_button.get();
ConfigureMenuButton(std::move(press_state_button));
generator()->MoveMouseTo(button()->bounds().CenterPoint());
generator()->PressLeftButton();
EXPECT_FALSE(ink_drop()->is_hovered());
test_button->ReleasePressedLock();
EXPECT_TRUE(ink_drop()->is_hovered());
}
TEST_F(MenuButtonTest,
InkDropIsntHoveredAfterDismissingMenuWhenMouseOutsideButton) {
auto press_state_button = std::make_unique<PressStateButton>(false);
auto* test_button = press_state_button.get();
ConfigureMenuButton(std::move(press_state_button));
generator()->MoveMouseTo(button()->bounds().CenterPoint());
generator()->PressLeftButton();
generator()->MoveMouseTo(GetOutOfButtonLocation());
test_button->ReleasePressedLock();
EXPECT_FALSE(ink_drop()->is_hovered());
}
// This test ensures there isn't a UAF in MenuButton::OnGestureEvent() if the
// button callback deletes the MenuButton.
TEST_F(MenuButtonTest, DestroyButtonInGesture) {
std::unique_ptr<TestMenuButton> test_menu_button =
std::make_unique<TestMenuButton>(base::BindRepeating(
[](std::unique_ptr<TestMenuButton>* button) { button->reset(); },
&test_menu_button));
ConfigureMenuButton(std::move(test_menu_button));
ui::GestureEvent gesture_event(0, 0, 0, base::TimeTicks::Now(),
ui::GestureEventDetails(ui::ET_GESTURE_TAP));
button()->OnGestureEvent(&gesture_event);
}
} // namespace views