blob: 07930f4e94adc5b6dbb5d02e20867616e99e55b6 [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 <utility>
#include "base/callback_helpers.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/simple_test_tick_clock.h"
#include "build/build_config.h"
#include "ui/base/ui_base_types.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/menu/menu_controller.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/controls/menu/test_menu_item_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"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/widget/widget_utils.h"
namespace views {
namespace test {
class MenuRunnerTest : public ViewsTestBase {
public:
MenuRunnerTest() = default;
~MenuRunnerTest() override = default;
// Initializes the delegates and views needed for a menu. It does not create
// the MenuRunner.
void InitMenuViews() {
menu_delegate_ = std::make_unique<TestMenuDelegate>();
menu_item_view_ = new views::TestMenuItemView(menu_delegate_.get());
menu_item_view_->AppendMenuItem(1, u"One");
menu_item_view_->AppendMenuItem(2, u"\x062f\x0648");
owner_ = std::make_unique<Widget>();
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP);
params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
owner_->Init(std::move(params));
owner_->Show();
}
// Initializes all delegates and views needed for a menu. A MenuRunner is also
// created with |run_types|, it takes ownership of |menu_item_view_|.
void InitMenuRunner(int32_t run_types) {
InitMenuViews();
menu_runner_ = std::make_unique<MenuRunner>(menu_item_view_, run_types);
}
views::TestMenuItemView* 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 TearDown() override {
if (owner_)
owner_->CloseNow();
ViewsTestBase::TearDown();
}
bool IsItemSelected(int command_id) {
MenuItemView* item = menu_item_view()->GetMenuItemByID(command_id);
return item ? item->IsSelected() : false;
}
// Menus that use prefix selection don't support mnemonics - the input is
// always part of the prefix.
bool MenuSupportsMnemonics() {
return !MenuConfig::instance().all_menus_use_prefix_selection;
}
private:
// Owned by menu_runner_.
views::TestMenuItemView* menu_item_view_ = nullptr;
std::unique_ptr<TestMenuDelegate> menu_delegate_;
std::unique_ptr<MenuRunner> menu_runner_;
std::unique_ptr<Widget> owner_;
DISALLOW_COPY_AND_ASSIGN(MenuRunnerTest);
};
// Tests that MenuRunner is still running after the call to RunMenuAt when
// initialized with , and that MenuDelegate is notified upon
// the closing of the menu.
TEST_F(MenuRunnerTest, AsynchronousRun) {
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
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());
}
// 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) {
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
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());
}
// Tests that a key press on a US keyboard layout activates the correct menu
// item.
TEST_F(MenuRunnerTest, LatinMnemonic) {
if (!MenuSupportsMnemonics())
return;
views::test::DisableMenuClosureAnimations();
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
EXPECT_TRUE(runner->IsRunning());
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
generator.PressKey(ui::VKEY_O, 0);
views::test::WaitForMenuClosureAnimation();
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());
}
#if !defined(OS_WIN)
// Tests that a key press on a non-US keyboard layout activates the correct menu
// item. Disabled on Windows because a WM_CHAR event does not activate an item.
TEST_F(MenuRunnerTest, NonLatinMnemonic) {
if (!MenuSupportsMnemonics())
return;
views::test::DisableMenuClosureAnimations();
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
EXPECT_TRUE(runner->IsRunning());
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
ui::KeyEvent key_press(0x062f, ui::VKEY_N, ui::DomCode::NONE, 0);
generator.Dispatch(&key_press);
views::test::WaitForMenuClosureAnimation();
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());
}
#endif // !defined(OS_WIN)
TEST_F(MenuRunnerTest, MenuItemViewShowsMnemonics) {
if (!MenuSupportsMnemonics())
return;
InitMenuRunner(MenuRunner::HAS_MNEMONICS | MenuRunner::SHOULD_SHOW_MNEMONICS);
menu_runner()->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, ui::MENU_SOURCE_NONE);
EXPECT_TRUE(menu_item_view()->show_mnemonics());
}
TEST_F(MenuRunnerTest, MenuItemViewDoesNotShowMnemonics) {
if (!MenuSupportsMnemonics())
return;
InitMenuRunner(MenuRunner::HAS_MNEMONICS);
menu_runner()->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, ui::MENU_SOURCE_NONE);
EXPECT_FALSE(menu_item_view()->show_mnemonics());
}
TEST_F(MenuRunnerTest, PrefixSelect) {
if (!MenuConfig::instance().all_menus_use_prefix_selection)
return;
base::SimpleTestTickClock clock;
// This test has a menu with three items:
// { 1, "One" }
// { 2, "\x062f\x0648" }
// { 3, "One Two" }
// It progressively prefix searches for "One " (note the space) and ensures
// that the right item is found.
views::test::DisableMenuClosureAnimations();
InitMenuRunner(0);
menu_item_view()->AppendMenuItem(3, u"One Two");
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
EXPECT_TRUE(runner->IsRunning());
menu_item_view()
->GetSubmenu()
->GetPrefixSelector()
->set_tick_clock_for_testing(&clock);
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
generator.PressKey(ui::VKEY_O, 0);
EXPECT_TRUE(IsItemSelected(1));
generator.PressKey(ui::VKEY_N, 0);
generator.PressKey(ui::VKEY_E, 0);
EXPECT_TRUE(IsItemSelected(1));
generator.PressKey(ui::VKEY_SPACE, 0);
EXPECT_TRUE(IsItemSelected(3));
// Wait out the PrefixSelector's timeout.
clock.Advance(base::TimeDelta::FromSeconds(10));
// Send Space to activate the selected menu item.
generator.PressKey(ui::VKEY_SPACE, 0);
views::test::WaitForMenuClosureAnimation();
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(3, delegate->execute_command_id());
EXPECT_EQ(1, delegate->on_menu_closed_called());
EXPECT_NE(nullptr, delegate->on_menu_closed_menu());
}
// This test is Mac-specific: Mac is the only platform where VKEY_SPACE
// activates menu items.
#if defined(OS_APPLE)
TEST_F(MenuRunnerTest, SpaceActivatesItem) {
if (!MenuConfig::instance().all_menus_use_prefix_selection)
return;
views::test::DisableMenuClosureAnimations();
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
EXPECT_TRUE(runner->IsRunning());
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
generator.PressKey(ui::VKEY_DOWN, 0);
EXPECT_TRUE(IsItemSelected(1));
generator.PressKey(ui::VKEY_SPACE, 0);
views::test::WaitForMenuClosureAnimation();
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());
}
#endif // OS_APPLE
// 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();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
ui::MENU_SOURCE_NONE);
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));
nested_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, ui::MENU_SOURCE_NONE);
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());
}
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 = default;
private:
// ui::EventHandler:
void OnMouseEvent(ui::MouseEvent* event) override {
if (event->type() == ui::ET_MOUSE_PRESSED) {
runner_->RunMenuAt(owner_, nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, 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() = default;
Widget* widget() { return widget_; }
EventCountView* event_count_view() { return event_count_view_; }
std::unique_ptr<ui::test::EventGenerator> EventGeneratorForWidget(
Widget* widget) {
return std::make_unique<ui::test::EventGenerator>(
GetContext(), widget->GetNativeWindow());
}
void AddMenuLauncherEventHandler(Widget* widget) {
consumer_ =
std::make_unique<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(std::move(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(0);
}
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(gfx::kNullNativeView,
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));
}
class MenuRunnerImplTest : public MenuRunnerTest {
public:
MenuRunnerImplTest() = default;
~MenuRunnerImplTest() override = default;
void SetUp() override;
private:
DISALLOW_COPY_AND_ASSIGN(MenuRunnerImplTest);
};
void MenuRunnerImplTest::SetUp() {
MenuRunnerTest::SetUp();
InitMenuViews();
}
// 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());
menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, 0);
std::unique_ptr<TestMenuDelegate> menu_delegate2(new TestMenuDelegate);
MenuItemView* menu_item_view2 = new MenuItemView(menu_delegate2.get());
menu_item_view2->AppendMenuItem(1, u"One");
internal::MenuRunnerImpl* menu_runner2 =
new internal::MenuRunnerImpl(menu_item_view2);
menu_runner2->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, MenuRunner::IS_NESTED);
// Hide the controller so we can test out of order destruction.
MenuControllerTestApi menu_controller;
menu_controller.SetShowing(false);
// 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();
}
// Tests that when there are two separate MenuControllers, and the active one is
// deleted first, that shutting down the MenuRunner of the original
// MenuController properly closes its controller. This should not crash on ASAN
// bots.
TEST_F(MenuRunnerImplTest, MenuRunnerDestroyedWithNoActiveController) {
internal::MenuRunnerImpl* menu_runner =
new internal::MenuRunnerImpl(menu_item_view());
menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, 0);
// Hide the menu, and clear its item selection state.
MenuControllerTestApi menu_controller;
menu_controller.SetShowing(false);
menu_controller.ClearState();
std::unique_ptr<TestMenuDelegate> menu_delegate2(new TestMenuDelegate);
MenuItemView* menu_item_view2 = new MenuItemView(menu_delegate2.get());
menu_item_view2->AppendMenuItem(1, u"One");
internal::MenuRunnerImpl* menu_runner2 =
new internal::MenuRunnerImpl(menu_item_view2);
menu_runner2->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, MenuRunner::FOR_DROP);
EXPECT_NE(menu_controller.controller(), MenuController::GetActiveInstance());
menu_controller.SetShowing(true);
// Close the runner with the active menu first.
menu_runner2->Release();
// Even though there is no active menu, this should still cleanup the
// controller that it created.
menu_runner->Release();
// This is not expected to run, however this is from the origin ASAN stack
// traces. So regressions will be caught with the same stack trace.
if (menu_controller.controller())
menu_controller.controller()->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(nullptr, menu_controller.controller());
}
// Test class which overrides the ViewsDelegate. Allowing to simulate shutdown
// during its release.
class MenuRunnerDestructionTest : public MenuRunnerTest {
public:
MenuRunnerDestructionTest() = default;
~MenuRunnerDestructionTest() override = default;
ReleaseRefTestViewsDelegate* test_views_delegate() {
return test_views_delegate_;
}
base::WeakPtr<internal::MenuRunnerImpl> MenuRunnerAsWeakPtr(
internal::MenuRunnerImpl* menu_runner);
// ViewsTestBase:
void SetUp() override;
private:
// Not owned
ReleaseRefTestViewsDelegate* test_views_delegate_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(MenuRunnerDestructionTest);
};
base::WeakPtr<internal::MenuRunnerImpl>
MenuRunnerDestructionTest::MenuRunnerAsWeakPtr(
internal::MenuRunnerImpl* menu_runner) {
return menu_runner->weak_factory_.GetWeakPtr();
}
void MenuRunnerDestructionTest::SetUp() {
auto test_views_delegate = std::make_unique<ReleaseRefTestViewsDelegate>();
test_views_delegate_ = test_views_delegate.get();
set_views_delegate(std::move(test_views_delegate));
MenuRunnerTest::SetUp();
InitMenuViews();
}
// Tests that when ViewsDelegate is released that a nested Cancel of the
// MenuRunner does not occur.
TEST_F(MenuRunnerDestructionTest, MenuRunnerDestroyedDuringReleaseRef) {
internal::MenuRunnerImpl* menu_runner =
new internal::MenuRunnerImpl(menu_item_view());
menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, 0);
base::RunLoop run_loop;
test_views_delegate()->set_release_ref_callback(
base::BindLambdaForTesting([&]() {
run_loop.Quit();
menu_runner->Release();
}));
base::WeakPtr<internal::MenuRunnerImpl> ref(MenuRunnerAsWeakPtr(menu_runner));
MenuControllerTestApi menu_controller;
// This will release the ref on ViewsDelegate. The test version will release
// |menu_runner| simulating device shutdown.
menu_controller.controller()->Cancel(MenuController::ExitType::kAll);
// Both the |menu_runner| and |menu_controller| should have been deleted.
EXPECT_EQ(nullptr, menu_controller.controller());
run_loop.Run();
EXPECT_EQ(nullptr, ref);
}
TEST_F(MenuRunnerImplTest, FocusOnMenuClose) {
internal::MenuRunnerImpl* menu_runner =
new internal::MenuRunnerImpl(menu_item_view());
// Create test button that has focus.
auto button_managed = std::make_unique<LabelButton>();
button_managed->SetID(1);
button_managed->SetSize(gfx::Size(20, 20));
LabelButton* button =
owner()->GetRootView()->AddChildView(std::move(button_managed));
button->SetFocusBehavior(View::FocusBehavior::ALWAYS);
button->GetWidget()->widget_delegate()->SetCanActivate(true);
button->GetWidget()->Activate();
button->RequestFocus();
// Open the menu.
menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, 0);
MenuControllerTestApi menu_controller;
menu_controller.SetShowing(false);
// Test that closing the menu sends the kFocusAfterMenuClose event.
bool focus_after_menu_close_sent = false;
ViewAccessibility::AccessibilityEventsCallback accessibility_events_callback =
base::BindRepeating(
[](bool* focus_after_menu_close_sent,
const ui::AXPlatformNodeDelegate* delegate,
const ax::mojom::Event event_type) {
if (event_type == ax::mojom::Event::kFocusAfterMenuClose)
*focus_after_menu_close_sent = true;
},
&focus_after_menu_close_sent);
button->GetViewAccessibility().set_accessibility_events_callback(
std::move(accessibility_events_callback));
menu_runner->OnMenuClosed(internal::MenuControllerDelegate::NOTIFY_DELEGATE,
nullptr, 0);
EXPECT_TRUE(focus_after_menu_close_sent);
// Set the callback to a no-op to avoid accessing
// "focus_after_menu_close_sent" after this test has completed.
button->GetViewAccessibility().set_accessibility_events_callback(
base::DoNothing());
menu_runner->Release();
}
TEST_F(MenuRunnerImplTest, FocusOnMenuCloseDeleteAfterRun) {
// Create test button that has focus.
LabelButton* button = new LabelButton(
Button::PressedCallback(), std::u16string(), style::CONTEXT_BUTTON);
button->SetID(1);
button->SetSize(gfx::Size(20, 20));
owner()->GetRootView()->AddChildView(button);
button->SetFocusBehavior(View::FocusBehavior::ALWAYS);
button->GetWidget()->widget_delegate()->SetCanActivate(true);
button->GetWidget()->Activate();
button->RequestFocus();
internal::MenuRunnerImpl* menu_runner =
new internal::MenuRunnerImpl(menu_item_view());
menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, 0);
// Hide the menu, and clear its item selection state.
MenuControllerTestApi menu_controller;
menu_controller.SetShowing(false);
menu_controller.ClearState();
std::unique_ptr<TestMenuDelegate> menu_delegate2(new TestMenuDelegate);
MenuItemView* menu_item_view2 = new MenuItemView(menu_delegate2.get());
menu_item_view2->AppendMenuItem(1, u"One");
internal::MenuRunnerImpl* menu_runner2 =
new internal::MenuRunnerImpl(menu_item_view2);
menu_runner2->RunMenuAt(owner(), nullptr, gfx::Rect(),
MenuAnchorPosition::kTopLeft, MenuRunner::FOR_DROP);
EXPECT_NE(menu_controller.controller(), MenuController::GetActiveInstance());
menu_controller.SetShowing(true);
// Test that closing the menu sends the kFocusAfterMenuClose event.
bool focus_after_menu_close_sent = false;
ViewAccessibility::AccessibilityEventsCallback accessibility_events_callback =
base::BindRepeating(
[](bool* focus_after_menu_close_sent,
const ui::AXPlatformNodeDelegate* delegate,
const ax::mojom::Event event_type) {
if (event_type == ax::mojom::Event::kFocusAfterMenuClose)
*focus_after_menu_close_sent = true;
},
&focus_after_menu_close_sent);
button->GetViewAccessibility().set_accessibility_events_callback(
std::move(accessibility_events_callback));
menu_runner2->Release();
EXPECT_TRUE(focus_after_menu_close_sent);
focus_after_menu_close_sent = false;
menu_runner->Release();
EXPECT_TRUE(focus_after_menu_close_sent);
// Set the callback to a no-op to avoid accessing
// "focus_after_menu_close_sent" after this test has completed.
button->GetViewAccessibility().set_accessibility_events_callback(
base::DoNothing());
// This is not expected to run, however this is from the origin ASAN stack
// traces. So regressions will be caught with the same stack trace.
if (menu_controller.controller())
menu_controller.controller()->Cancel(MenuController::ExitType::kAll);
EXPECT_EQ(nullptr, menu_controller.controller());
}
} // namespace test
} // namespace views