blob: 923a6cd29c651c23dc0fb170743388f641891625 [file] [log] [blame]
// Copyright 2015 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/menu/menu_runner.h"
#include <stdint.h>
#include <memory>
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/ui_base_types.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/controls/menu/menu_delegate.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_runner_impl.h"
#include "ui/views/controls/menu/menu_types.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/test/menu_test_utils.h"
#include "ui/views/test/test_views.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/native_widget_private.h"
#include "ui/views/widget/widget.h"
namespace views {
namespace test {
class MenuRunnerTest : public ViewsTestBase {
public:
MenuRunnerTest();
~MenuRunnerTest() override;
// Initializes a MenuRunner with |run_types|. It takes ownership of
// |menu_item_view_|.
void InitMenuRunner(int32_t run_types);
MenuItemView* menu_item_view() { return menu_item_view_; }
TestMenuDelegate* menu_delegate() { return menu_delegate_.get(); }
MenuRunner* menu_runner() { return menu_runner_.get(); }
Widget* owner() { return owner_.get(); }
// ViewsTestBase:
void SetUp() override;
void TearDown() override;
private:
// Owned by MenuRunner.
MenuItemView* menu_item_view_;
std::unique_ptr<TestMenuDelegate> menu_delegate_;
std::unique_ptr<MenuRunner> menu_runner_;
std::unique_ptr<Widget> owner_;
DISALLOW_COPY_AND_ASSIGN(MenuRunnerTest);
};
MenuRunnerTest::MenuRunnerTest() {}
MenuRunnerTest::~MenuRunnerTest() {}
void MenuRunnerTest::InitMenuRunner(int32_t run_types) {
menu_runner_.reset(new MenuRunner(menu_item_view_, run_types));
}
void MenuRunnerTest::SetUp() {
ViewsTestBase::SetUp();
menu_delegate_.reset(new TestMenuDelegate);
menu_item_view_ = new MenuItemView(menu_delegate_.get());
menu_item_view_->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
menu_item_view_->AppendMenuItemWithLabel(2,
base::WideToUTF16(L"\x062f\x0648"));
owner_.reset(new Widget);
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP);
params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
owner_->Init(params);
owner_->Show();
}
void MenuRunnerTest::TearDown() {
owner_->CloseNow();
ViewsTestBase::TearDown();
}
// Tests that MenuRunner is still running after the call to RunMenuAt when
// initialized with MenuRunner::ASYNC, and that MenuDelegate is notified upon
// the closing of the menu.
TEST_F(MenuRunnerTest, AsynchronousRun) {
InitMenuRunner(MenuRunner::ASYNC);
MenuRunner* runner = menu_runner();
MenuRunner::RunResult result = runner->RunMenuAt(
owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT, ui::MENU_SOURCE_NONE);
EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
EXPECT_TRUE(runner->IsRunning());
runner->Cancel();
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(1, delegate->on_menu_closed_called());
EXPECT_EQ(nullptr, delegate->on_menu_closed_menu());
EXPECT_EQ(MenuRunner::NORMAL_EXIT, delegate->on_menu_closed_run_result());
}
// Tests that when a menu is run asynchronously, key events are handled properly
// by testing that Escape key closes the menu.
TEST_F(MenuRunnerTest, AsynchronousKeyEventHandling) {
// TODO: test uses GetContext(), which is not applicable to aura-mus.
// http://crbug.com/663809.
if (IsAuraMusClient())
return;
InitMenuRunner(MenuRunner::ASYNC);
MenuRunner* runner = menu_runner();
MenuRunner::RunResult result = runner->RunMenuAt(
owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT, ui::MENU_SOURCE_NONE);
EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
EXPECT_TRUE(runner->IsRunning());
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
generator.PressKey(ui::VKEY_ESCAPE, 0);
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(1, delegate->on_menu_closed_called());
EXPECT_EQ(nullptr, delegate->on_menu_closed_menu());
EXPECT_EQ(MenuRunner::NORMAL_EXIT, delegate->on_menu_closed_run_result());
}
// Tests that a key press on a US keyboard layout activates the correct menu
// item.
TEST_F(MenuRunnerTest, LatinMnemonic) {
// TODO: test uses GetContext(), which is not applicable to aura-mus.
// http://crbug.com/663809.
if (IsAuraMusClient())
return;
InitMenuRunner(MenuRunner::ASYNC);
MenuRunner* runner = menu_runner();
MenuRunner::RunResult result = runner->RunMenuAt(
owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT, ui::MENU_SOURCE_NONE);
EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
EXPECT_TRUE(runner->IsRunning());
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
generator.PressKey(ui::VKEY_O, 0);
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(1, delegate->execute_command_id());
EXPECT_EQ(1, delegate->on_menu_closed_called());
EXPECT_NE(nullptr, delegate->on_menu_closed_menu());
EXPECT_EQ(MenuRunner::NORMAL_EXIT, delegate->on_menu_closed_run_result());
}
// Tests that a key press on a non-US keyboard layout activates the correct menu
// item.
TEST_F(MenuRunnerTest, NonLatinMnemonic) {
// TODO: test uses GetContext(), which is not applicable to aura-mus.
// http://crbug.com/663809.
if (IsAuraMusClient())
return;
InitMenuRunner(MenuRunner::ASYNC);
MenuRunner* runner = menu_runner();
MenuRunner::RunResult result = runner->RunMenuAt(
owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT, ui::MENU_SOURCE_NONE);
EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
EXPECT_TRUE(runner->IsRunning());
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
ui::KeyEvent key_press(0x062f, ui::VKEY_N, 0);
generator.Dispatch(&key_press);
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(2, delegate->execute_command_id());
EXPECT_EQ(1, delegate->on_menu_closed_called());
EXPECT_NE(nullptr, delegate->on_menu_closed_menu());
EXPECT_EQ(MenuRunner::NORMAL_EXIT, delegate->on_menu_closed_run_result());
}
// Tests that attempting to nest a menu within a drag-and-drop menu does not
// cause a crash. Instead the drag and drop action should be canceled, and the
// new menu should be openned.
TEST_F(MenuRunnerTest, NestingDuringDrag) {
InitMenuRunner(MenuRunner::FOR_DROP);
MenuRunner* runner = menu_runner();
MenuRunner::RunResult result = runner->RunMenuAt(
owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT, ui::MENU_SOURCE_NONE);
EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
EXPECT_TRUE(runner->IsRunning());
std::unique_ptr<TestMenuDelegate> nested_delegate(new TestMenuDelegate);
MenuItemView* nested_menu = new MenuItemView(nested_delegate.get());
std::unique_ptr<MenuRunner> nested_runner(
new MenuRunner(nested_menu, MenuRunner::IS_NESTED | MenuRunner::ASYNC));
result = nested_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MENU_ANCHOR_TOPLEFT, ui::MENU_SOURCE_NONE);
EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
EXPECT_TRUE(nested_runner->IsRunning());
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(1, delegate->on_menu_closed_called());
EXPECT_NE(nullptr, delegate->on_menu_closed_menu());
EXPECT_EQ(MenuRunner::NORMAL_EXIT, delegate->on_menu_closed_run_result());
}
namespace {
// An EventHandler that launches a menu in response to a mouse press.
class MenuLauncherEventHandler : public ui::EventHandler {
public:
MenuLauncherEventHandler(MenuRunner* runner, Widget* owner)
: runner_(runner), owner_(owner) {}
~MenuLauncherEventHandler() override {}
private:
// ui::EventHandler:
void OnMouseEvent(ui::MouseEvent* event) override {
if (event->type() == ui::ET_MOUSE_PRESSED) {
runner_->RunMenuAt(owner_, nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT,
ui::MENU_SOURCE_NONE);
event->SetHandled();
}
}
MenuRunner* runner_;
Widget* owner_;
DISALLOW_COPY_AND_ASSIGN(MenuLauncherEventHandler);
};
} // namespace
// Test harness that includes a parent Widget and View invoking the menu.
class MenuRunnerWidgetTest : public MenuRunnerTest {
public:
MenuRunnerWidgetTest() {}
Widget* widget() { return widget_; }
EventCountView* event_count_view() { return event_count_view_; }
std::unique_ptr<ui::test::EventGenerator> EventGeneratorForWidget(
Widget* widget) {
return base::MakeUnique<ui::test::EventGenerator>(
IsMus() || IsAuraMusClient() ? widget->GetNativeWindow() : GetContext(),
widget->GetNativeWindow());
}
void AddMenuLauncherEventHandler(Widget* widget) {
consumer_ =
base::MakeUnique<MenuLauncherEventHandler>(menu_runner(), widget);
event_count_view_->AddPostTargetHandler(consumer_.get());
}
// ViewsTestBase:
void SetUp() override {
MenuRunnerTest::SetUp();
widget_ = new Widget;
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
widget_->Init(params);
widget_->Show();
widget_->SetSize(gfx::Size(300, 300));
event_count_view_ = new EventCountView();
event_count_view_->SetBounds(0, 0, 300, 300);
widget_->GetRootView()->AddChildView(event_count_view_);
InitMenuRunner(MenuRunner::ASYNC);
}
void TearDown() override {
widget_->CloseNow();
MenuRunnerTest::TearDown();
}
private:
Widget* widget_ = nullptr;
EventCountView* event_count_view_ = nullptr;
std::unique_ptr<MenuLauncherEventHandler> consumer_;
DISALLOW_COPY_AND_ASSIGN(MenuRunnerWidgetTest);
};
// Tests that when a mouse press launches a menu, that the target widget does
// not take explicit capture, nor closes the menu.
TEST_F(MenuRunnerWidgetTest, WidgetDoesntTakeCapture) {
AddMenuLauncherEventHandler(owner());
EXPECT_EQ(nullptr, internal::NativeWidgetPrivate::GetGlobalCapture(
widget()->GetNativeView()));
auto generator(EventGeneratorForWidget(widget()));
// Implicit capture should not be held by |widget|.
generator->PressLeftButton();
EXPECT_EQ(1, event_count_view()->GetEventCount(ui::ET_MOUSE_PRESSED));
EXPECT_NE(widget()->GetNativeView(),
internal::NativeWidgetPrivate::GetGlobalCapture(
widget()->GetNativeView()));
// The menu should still be open.
TestMenuDelegate* delegate = menu_delegate();
EXPECT_TRUE(menu_runner()->IsRunning());
EXPECT_EQ(0, delegate->on_menu_closed_called());
}
// Tests that after showing a menu on mouse press, that the subsequent mouse
// will be delivered to the correct view, and not to the one that showed the
// menu.
//
// The original bug is reproducible only when showing the menu on mouse press,
// as RootView::OnMouseReleased() doesn't have the same behavior.
TEST_F(MenuRunnerWidgetTest, ClearsMouseHandlerOnRun) {
AddMenuLauncherEventHandler(widget());
// Create a second view that's supposed to get the second mouse press.
EventCountView* second_event_count_view = new EventCountView();
widget()->GetRootView()->AddChildView(second_event_count_view);
widget()->SetBounds(gfx::Rect(0, 0, 200, 100));
event_count_view()->SetBounds(0, 0, 100, 100);
second_event_count_view->SetBounds(100, 0, 100, 100);
// Click on the first view to show the menu.
auto generator(EventGeneratorForWidget(widget()));
generator->MoveMouseTo(event_count_view()->bounds().CenterPoint());
generator->PressLeftButton();
// Pretend we dismissed the menu using normal means, as it doesn't matter.
EXPECT_TRUE(menu_runner()->IsRunning());
menu_runner()->Cancel();
// EventGenerator won't allow us to re-send the left button press without
// releasing it first. We can't send the release event using the same
// generator as it would be handled by the RootView in the main Widget.
// In actual application the RootView doesn't see the release event.
generator.reset();
generator = EventGeneratorForWidget(widget());
generator->MoveMouseTo(second_event_count_view->bounds().CenterPoint());
generator->PressLeftButton();
EXPECT_EQ(1, second_event_count_view->GetEventCount(ui::ET_MOUSE_PRESSED));
}
typedef MenuRunnerTest MenuRunnerImplTest;
// Tests that when nested menu runners are destroyed out of order, that
// MenuController is not accessed after it has been destroyed. This should not
// crash on ASAN bots.
TEST_F(MenuRunnerImplTest, NestedMenuRunnersDestroyedOutOfOrder) {
internal::MenuRunnerImpl* menu_runner =
new internal::MenuRunnerImpl(menu_item_view());
EXPECT_EQ(MenuRunner::NORMAL_EXIT,
menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MENU_ANCHOR_TOPLEFT, MenuRunner::ASYNC));
std::unique_ptr<TestMenuDelegate> menu_delegate2(new TestMenuDelegate);
MenuItemView* menu_item_view2 = new MenuItemView(menu_delegate2.get());
menu_item_view2->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
internal::MenuRunnerImpl* menu_runner2 =
new internal::MenuRunnerImpl(menu_item_view2);
EXPECT_EQ(MenuRunner::NORMAL_EXIT,
menu_runner2->RunMenuAt(owner(), nullptr, gfx::Rect(),
MENU_ANCHOR_TOPLEFT,
MenuRunner::ASYNC | MenuRunner::IS_NESTED));
// Hide the controller so we can test out of order destruction.
MenuControllerTestApi menu_controller;
menu_controller.Hide();
// This destroyed MenuController
menu_runner->OnMenuClosed(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
nullptr, 0);
// This should not access the destroyed MenuController
menu_runner2->Release();
menu_runner->Release();
}
} // namespace test
} // namespace views