1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/views/controls/menu/menu_controller.h"
7 #include "base/run_loop.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "ui/events/event_handler.h"
10 #include "ui/events/null_event_targeter.h"
11 #include "ui/events/platform/platform_event_source.h"
12 #include "ui/views/controls/menu/menu_item_view.h"
13 #include "ui/views/controls/menu/submenu_view.h"
14 #include "ui/views/test/views_test_base.h"
17 #include "ui/aura/scoped_window_targeter.h"
18 #include "ui/aura/window.h"
19 #include "ui/wm/public/dispatcher_client.h"
23 #include "base/message_loop/message_pump_dispatcher.h"
24 #elif defined(USE_X11)
28 #include "ui/events/devices/x11/device_data_manager_x11.h"
29 #include "ui/events/test/events_test_utils_x11.h"
30 #elif defined(USE_OZONE)
31 #include "ui/events/event.h"
32 #elif defined(OS_MACOSX)
33 #include "ui/events/test/event_generator.h"
41 class TestMenuItemView
: public MenuItemView
{
43 TestMenuItemView() : MenuItemView(nullptr) {}
44 ~TestMenuItemView() override
{}
47 DISALLOW_COPY_AND_ASSIGN(TestMenuItemView
);
50 class SubmenuViewShown
: public SubmenuView
{
52 SubmenuViewShown(MenuItemView
* parent
) : SubmenuView(parent
) {}
53 ~SubmenuViewShown() override
{}
54 bool IsShowing() override
{ return true; }
57 DISALLOW_COPY_AND_ASSIGN(SubmenuViewShown
);
60 class TestPlatformEventSource
: public ui::PlatformEventSource
{
62 TestPlatformEventSource() {
64 ui::DeviceDataManagerX11::CreateInstance();
67 ~TestPlatformEventSource() override
{}
69 uint32_t Dispatch(const ui::PlatformEvent
& event
) {
70 return DispatchEvent(event
);
74 DISALLOW_COPY_AND_ASSIGN(TestPlatformEventSource
);
78 class TestDispatcherClient
: public aura::client::DispatcherClient
{
80 TestDispatcherClient() : dispatcher_(nullptr) {}
81 ~TestDispatcherClient() override
{}
83 base::MessagePumpDispatcher
* dispatcher() {
87 // aura::client::DispatcherClient:
88 void PrepareNestedLoopClosures(base::MessagePumpDispatcher
* dispatcher
,
89 base::Closure
* run_closure
,
90 base::Closure
* quit_closure
) override
{
91 scoped_ptr
<base::RunLoop
> run_loop(new base::RunLoop());
92 *quit_closure
= run_loop
->QuitClosure();
93 *run_closure
= base::Bind(&TestDispatcherClient::RunNestedDispatcher
,
94 base::Unretained(this),
95 base::Passed(&run_loop
),
100 void RunNestedDispatcher(scoped_ptr
<base::RunLoop
> run_loop
,
101 base::MessagePumpDispatcher
* dispatcher
) {
102 base::AutoReset
<base::MessagePumpDispatcher
*> reset_dispatcher(&dispatcher_
,
104 base::MessageLoopForUI
* loop
= base::MessageLoopForUI::current();
105 base::MessageLoop::ScopedNestableTaskAllower
allow(loop
);
109 base::MessagePumpDispatcher
* dispatcher_
;
111 DISALLOW_COPY_AND_ASSIGN(TestDispatcherClient
);
117 class TestMenuItemViewShown
: public MenuItemView
{
119 TestMenuItemViewShown() : MenuItemView(nullptr) {
120 submenu_
= new SubmenuViewShown(this);
122 ~TestMenuItemViewShown() override
{}
125 DISALLOW_COPY_AND_ASSIGN(TestMenuItemViewShown
);
128 class MenuControllerTest
: public ViewsTestBase
{
130 MenuControllerTest() : controller_(nullptr) {}
131 ~MenuControllerTest() override
{ ResetMenuController(); }
133 // Dispatches |count| number of items, each in a separate iteration of the
134 // message-loop, by posting a task.
135 void Step3_DispatchEvents(int count
) {
136 base::MessageLoopForUI
* loop
= base::MessageLoopForUI::current();
137 base::MessageLoop::ScopedNestableTaskAllower
allow(loop
);
138 controller_
->exit_type_
= MenuController::EXIT_ALL
;
142 base::MessageLoop::current()->PostTask(
144 base::Bind(&MenuControllerTest::Step3_DispatchEvents
,
145 base::Unretained(this),
148 EXPECT_TRUE(run_loop_
->running());
153 // Runs a nested message-loop that does not involve the menu itself.
154 void Step2_RunNestedLoop() {
155 base::MessageLoopForUI
* loop
= base::MessageLoopForUI::current();
156 base::MessageLoop::ScopedNestableTaskAllower
allow(loop
);
157 base::MessageLoop::current()->PostTask(
159 base::Bind(&MenuControllerTest::Step3_DispatchEvents
,
160 base::Unretained(this),
162 run_loop_
.reset(new base::RunLoop());
166 void Step1_RunMenu() {
167 base::MessageLoop::current()->PostTask(
169 base::Bind(&MenuControllerTest::Step2_RunNestedLoop
,
170 base::Unretained(this)));
171 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
172 RunMenu(owner
.get());
175 scoped_ptr
<Widget
> CreateOwnerWidget() {
176 scoped_ptr
<Widget
> widget(new Widget
);
177 Widget::InitParams params
= CreateParams(Widget::InitParams::TYPE_POPUP
);
178 params
.ownership
= Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET
;
179 widget
->Init(params
);
182 #if defined(USE_AURA)
183 aura::client::SetDispatcherClient(
184 widget
->GetNativeWindow()->GetRootWindow(), &dispatcher_client_
);
186 return widget
.Pass();
189 const MenuItemView
* pending_state_item() const {
190 return controller_
->pending_state_
.item
;
193 void SetPendingStateItem(MenuItemView
* item
) {
194 controller_
->pending_state_
.item
= item
;
195 controller_
->pending_state_
.submenu_open
= true;
198 void ResetSelection() {
199 controller_
->SetSelection(nullptr,
200 MenuController::SELECTION_EXIT
|
201 MenuController::SELECTION_UPDATE_IMMEDIATELY
);
204 void IncrementSelection() {
205 controller_
->IncrementSelection(MenuController::INCREMENT_SELECTION_DOWN
);
208 void DecrementSelection() {
209 controller_
->IncrementSelection(MenuController::INCREMENT_SELECTION_UP
);
212 MenuItemView
* FindInitialSelectableMenuItemDown(MenuItemView
* parent
) {
213 return controller_
->FindInitialSelectableMenuItem(
214 parent
, MenuController::INCREMENT_SELECTION_DOWN
);
217 MenuItemView
* FindInitialSelectableMenuItemUp(MenuItemView
* parent
) {
218 return controller_
->FindInitialSelectableMenuItem(
219 parent
, MenuController::INCREMENT_SELECTION_UP
);
222 MenuItemView
* FindNextSelectableMenuItem(MenuItemView
* parent
,
224 return controller_
->FindNextSelectableMenuItem(
225 parent
, index
, MenuController::INCREMENT_SELECTION_DOWN
);
228 MenuItemView
* FindPreviousSelectableMenuItem(MenuItemView
* parent
,
230 return controller_
->FindNextSelectableMenuItem(
231 parent
, index
, MenuController::INCREMENT_SELECTION_UP
);
234 void SetupMenu(views::Widget
* owner
, views::MenuItemView
* item
) {
235 ResetMenuController();
236 controller_
= new MenuController(nullptr, true, nullptr);
237 controller_
->owner_
= owner
;
238 controller_
->showing_
= true;
239 controller_
->SetSelection(item
,
240 MenuController::SELECTION_UPDATE_IMMEDIATELY
);
243 void RunMenu(views::Widget
* owner
) {
244 scoped_ptr
<TestMenuItemView
> menu_item(new TestMenuItemView
);
245 SetupMenu(owner
, menu_item
.get());
246 controller_
->RunMessageLoop(false);
250 void DispatchEscapeAndExpect(MenuController::ExitType exit_type
) {
251 ui::ScopedXI2Event key_event
;
252 key_event
.InitKeyEvent(ui::ET_KEY_PRESSED
, ui::VKEY_ESCAPE
, 0);
253 event_source_
.Dispatch(key_event
);
254 EXPECT_EQ(exit_type
, controller_
->exit_type());
255 controller_
->exit_type_
= MenuController::EXIT_ALL
;
259 void DispatchTouch(int evtype
, int id
) {
260 ui::ScopedXI2Event touch_event
;
261 std::vector
<ui::Valuator
> valuators
;
262 touch_event
.InitTouchEvent(1, evtype
, id
, gfx::Point(10, 10), valuators
);
263 event_source_
.Dispatch(touch_event
);
268 void DispatchEvent() {
271 memset(&xevent
, 0, sizeof(xevent
));
272 event_source_
.Dispatch(&xevent
);
273 #elif defined(OS_WIN)
275 memset(&msg
, 0, sizeof(MSG
));
276 dispatcher_client_
.dispatcher()->Dispatch(msg
);
277 #elif defined(USE_OZONE)
278 ui::KeyEvent
event(' ', ui::VKEY_SPACE
, ui::EF_NONE
);
279 event_source_
.Dispatch(&event
);
280 #elif defined(OS_MACOSX) && !defined(USE_AURA)
281 // Since this is not an interactive test, on Mac there will be no key
282 // window. Any system event will just get ignored, so use the EventGenerator
283 // to generate a dummy event. Without Aura, these will be native events.
284 gfx::NativeWindow window
= controller_
->owner()->GetNativeWindow();
285 ui::test::EventGenerator
generator(window
, window
);
286 // Send "up", since this will not activate a menu item. But note that the
287 // test has already set exit_type_ = EXIT_ALL just before the first call
289 generator
.PressKey(ui::VKEY_UP
, 0);
291 #error Unsupported platform
296 void ResetMenuController() {
298 // These properties are faked by RunMenu for the purposes of testing and
299 // need to be undone before we call the destructor.
300 controller_
->owner_
= nullptr;
301 controller_
->showing_
= false;
303 controller_
= nullptr;
307 // A weak pointer to the MenuController owned by this class.
308 MenuController
* controller_
;
309 scoped_ptr
<base::RunLoop
> run_loop_
;
310 TestPlatformEventSource event_source_
;
311 #if defined(USE_AURA)
312 TestDispatcherClient dispatcher_client_
;
315 DISALLOW_COPY_AND_ASSIGN(MenuControllerTest
);
318 TEST_F(MenuControllerTest
, Basic
) {
319 base::MessageLoop::ScopedNestableTaskAllower
allow_nested(
320 base::MessageLoop::current());
321 message_loop()->PostTask(
323 base::Bind(&MenuControllerTest::Step1_RunMenu
, base::Unretained(this)));
326 #if defined(OS_LINUX) && defined(USE_X11)
327 // Tests that an event targeter which blocks events will be honored by the menu
329 TEST_F(MenuControllerTest
, EventTargeter
) {
331 // Verify that the menu handles the escape key under normal circumstances.
332 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
333 message_loop()->PostTask(
335 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect
,
336 base::Unretained(this),
337 MenuController::EXIT_OUTERMOST
));
338 RunMenu(owner
.get());
342 // With the NULL targeter instantiated and assigned we expect the menu to
343 // not handle the key event.
344 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
345 aura::ScopedWindowTargeter
scoped_targeter(
346 owner
->GetNativeWindow()->GetRootWindow(),
347 scoped_ptr
<ui::EventTargeter
>(new ui::NullEventTargeter
));
348 message_loop()->PostTask(
350 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect
,
351 base::Unretained(this),
352 MenuController::EXIT_NONE
));
353 RunMenu(owner
.get());
360 class TestEventHandler
: public ui::EventHandler
{
362 TestEventHandler() : outstanding_touches_(0) {}
364 void OnTouchEvent(ui::TouchEvent
* event
) override
{
365 switch(event
->type()) {
366 case ui::ET_TOUCH_PRESSED
:
367 outstanding_touches_
++;
369 case ui::ET_TOUCH_RELEASED
:
370 case ui::ET_TOUCH_CANCELLED
:
371 outstanding_touches_
--;
378 int outstanding_touches() const { return outstanding_touches_
; }
381 int outstanding_touches_
;
384 // Tests that touch event ids are released correctly. See crbug.com/439051 for
385 // details. When the ids aren't managed correctly, we get stuck down touches.
386 TEST_F(MenuControllerTest
, TouchIdsReleasedCorrectly
) {
387 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
388 TestEventHandler test_event_handler
;
389 owner
->GetNativeWindow()->GetRootWindow()->AddPreTargetHandler(
390 &test_event_handler
);
392 std::vector
<int> devices
;
393 devices
.push_back(1);
394 ui::SetUpTouchDevicesForTest(devices
);
396 DispatchTouch(XI_TouchBegin
, 0);
397 DispatchTouch(XI_TouchBegin
, 1);
398 DispatchTouch(XI_TouchEnd
, 0);
400 message_loop()->PostTask(FROM_HERE
,
401 base::Bind(&MenuControllerTest::DispatchTouch
,
402 base::Unretained(this), XI_TouchEnd
, 1));
404 message_loop()->PostTask(
406 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect
,
407 base::Unretained(this), MenuController::EXIT_OUTERMOST
));
409 RunMenu(owner
.get());
410 EXPECT_EQ(0, test_event_handler
.outstanding_touches());
412 owner
->GetNativeWindow()->GetRootWindow()->RemovePreTargetHandler(
413 &test_event_handler
);
415 #endif // defined(USE_X11)
417 // Tests that initial selected menu items are correct when items are enabled or
419 TEST_F(MenuControllerTest
, InitialSelectedItem
) {
420 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
421 scoped_ptr
<TestMenuItemView
> menu_item(new TestMenuItemView
);
422 menu_item
->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
423 menu_item
->AppendMenuItemWithLabel(2, base::ASCIIToUTF16("Two"));
424 menu_item
->AppendMenuItemWithLabel(3, base::ASCIIToUTF16("Three"));
425 SetupMenu(owner
.get(), menu_item
.get());
427 // Leave items "Two" and "Three" enabled.
428 menu_item
->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
429 // The first selectable item should be item "Two".
430 MenuItemView
* first_selectable
=
431 FindInitialSelectableMenuItemDown(menu_item
.get());
432 ASSERT_NE(nullptr, first_selectable
);
433 EXPECT_EQ(2, first_selectable
->GetCommand());
434 // The last selectable item should be item "Three".
435 MenuItemView
* last_selectable
=
436 FindInitialSelectableMenuItemUp(menu_item
.get());
437 ASSERT_NE(nullptr, last_selectable
);
438 EXPECT_EQ(3, last_selectable
->GetCommand());
440 // Leave items "One" and "Two" enabled.
441 menu_item
->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(true);
442 menu_item
->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(true);
443 menu_item
->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
444 // The first selectable item should be item "One".
445 first_selectable
= FindInitialSelectableMenuItemDown(menu_item
.get());
446 ASSERT_NE(nullptr, first_selectable
);
447 EXPECT_EQ(1, first_selectable
->GetCommand());
448 // The last selectable item should be item "Two".
449 last_selectable
= FindInitialSelectableMenuItemUp(menu_item
.get());
450 ASSERT_NE(nullptr, last_selectable
);
451 EXPECT_EQ(2, last_selectable
->GetCommand());
453 // Leave only a single item "One" enabled.
454 menu_item
->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(true);
455 menu_item
->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(false);
456 menu_item
->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
457 // The first selectable item should be item "One".
458 first_selectable
= FindInitialSelectableMenuItemDown(menu_item
.get());
459 ASSERT_NE(nullptr, first_selectable
);
460 EXPECT_EQ(1, first_selectable
->GetCommand());
461 // The last selectable item should be item "One".
462 last_selectable
= FindInitialSelectableMenuItemUp(menu_item
.get());
463 ASSERT_NE(nullptr, last_selectable
);
464 EXPECT_EQ(1, last_selectable
->GetCommand());
466 // Leave only a single item "Three" enabled.
467 menu_item
->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
468 menu_item
->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(false);
469 menu_item
->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(true);
470 // The first selectable item should be item "Three".
471 first_selectable
= FindInitialSelectableMenuItemDown(menu_item
.get());
472 ASSERT_NE(nullptr, first_selectable
);
473 EXPECT_EQ(3, first_selectable
->GetCommand());
474 // The last selectable item should be item "Three".
475 last_selectable
= FindInitialSelectableMenuItemUp(menu_item
.get());
476 ASSERT_NE(nullptr, last_selectable
);
477 EXPECT_EQ(3, last_selectable
->GetCommand());
479 // Leave only a single item ("Two") selected. It should be the first and the
480 // last selectable item.
481 menu_item
->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
482 menu_item
->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(true);
483 menu_item
->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
484 first_selectable
= FindInitialSelectableMenuItemDown(menu_item
.get());
485 ASSERT_NE(nullptr, first_selectable
);
486 EXPECT_EQ(2, first_selectable
->GetCommand());
487 last_selectable
= FindInitialSelectableMenuItemUp(menu_item
.get());
488 ASSERT_NE(nullptr, last_selectable
);
489 EXPECT_EQ(2, last_selectable
->GetCommand());
491 // There should be no next or previous selectable item since there is only a
492 // single enabled item in the menu.
493 EXPECT_EQ(nullptr, FindNextSelectableMenuItem(menu_item
.get(), 1));
494 EXPECT_EQ(nullptr, FindPreviousSelectableMenuItem(menu_item
.get(), 1));
496 // Clear references in menu controller to the menu item that is going away.
500 // Tests that opening menu and pressing 'Down' and 'Up' iterates over enabled
502 TEST_F(MenuControllerTest
, NextSelectedItem
) {
503 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
504 scoped_ptr
<TestMenuItemView
> menu_item(new TestMenuItemView
);
505 menu_item
->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
506 menu_item
->AppendMenuItemWithLabel(2, base::ASCIIToUTF16("Two"));
507 menu_item
->AppendMenuItemWithLabel(3, base::ASCIIToUTF16("Three"));
508 menu_item
->AppendMenuItemWithLabel(4, base::ASCIIToUTF16("Four"));
509 // Disabling the item "Three" gets it skipped when using keyboard to navigate.
510 menu_item
->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
512 SetupMenu(owner
.get(), menu_item
.get());
514 // Fake initial hot selection.
515 SetPendingStateItem(menu_item
->GetSubmenu()->GetMenuItemAt(0));
516 EXPECT_EQ(1, pending_state_item()->GetCommand());
518 // Move down in the menu.
520 IncrementSelection();
521 EXPECT_EQ(2, pending_state_item()->GetCommand());
523 // Skip disabled item.
524 IncrementSelection();
525 EXPECT_EQ(4, pending_state_item()->GetCommand());
528 IncrementSelection();
529 EXPECT_EQ(1, pending_state_item()->GetCommand());
531 // Move up in the menu.
533 DecrementSelection();
534 EXPECT_EQ(4, pending_state_item()->GetCommand());
536 // Skip disabled item.
537 DecrementSelection();
538 EXPECT_EQ(2, pending_state_item()->GetCommand());
540 // Select previous item.
541 DecrementSelection();
542 EXPECT_EQ(1, pending_state_item()->GetCommand());
544 // Clear references in menu controller to the menu item that is going away.
548 // Tests that opening menu and pressing 'Up' selects the last enabled menu item.
549 TEST_F(MenuControllerTest
, PreviousSelectedItem
) {
550 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
551 scoped_ptr
<TestMenuItemViewShown
> menu_item(new TestMenuItemViewShown
);
552 menu_item
->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
553 menu_item
->AppendMenuItemWithLabel(2, base::ASCIIToUTF16("Two"));
554 menu_item
->AppendMenuItemWithLabel(3, base::ASCIIToUTF16("Three"));
555 menu_item
->AppendMenuItemWithLabel(4, base::ASCIIToUTF16("Four"));
556 // Disabling the item "Four" gets it skipped when using keyboard to navigate.
557 menu_item
->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false);
559 SetupMenu(owner
.get(), menu_item
.get());
561 // Fake initial root item selection and submenu showing.
562 SetPendingStateItem(menu_item
.get());
563 EXPECT_EQ(0, pending_state_item()->GetCommand());
565 // Move up and select a previous (in our case the last enabled) item.
566 DecrementSelection();
567 EXPECT_EQ(3, pending_state_item()->GetCommand());
569 // Clear references in menu controller to the menu item that is going away.