Add ICU message format support
[chromium-blink-merge.git] / ui / views / controls / menu / menu_controller_unittest.cc
blob60872bf399af31079e99f7d9096d24571cab9fdc
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"
16 #if defined(USE_AURA)
17 #include "ui/aura/scoped_window_targeter.h"
18 #include "ui/aura/window.h"
19 #include "ui/wm/public/dispatcher_client.h"
20 #endif
22 #if defined(OS_WIN)
23 #include "base/message_loop/message_pump_dispatcher.h"
24 #elif defined(USE_X11)
25 #include <X11/Xlib.h>
26 #undef Bool
27 #undef None
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"
34 #endif
36 namespace views {
37 namespace test {
39 namespace {
41 class TestMenuItemView : public MenuItemView {
42 public:
43 TestMenuItemView() : MenuItemView(nullptr) {}
44 ~TestMenuItemView() override {}
46 private:
47 DISALLOW_COPY_AND_ASSIGN(TestMenuItemView);
50 class SubmenuViewShown : public SubmenuView {
51 public:
52 SubmenuViewShown(MenuItemView* parent) : SubmenuView(parent) {}
53 ~SubmenuViewShown() override {}
54 bool IsShowing() override { return true; }
56 private:
57 DISALLOW_COPY_AND_ASSIGN(SubmenuViewShown);
60 class TestPlatformEventSource : public ui::PlatformEventSource {
61 public:
62 TestPlatformEventSource() {
63 #if defined(USE_X11)
64 ui::DeviceDataManagerX11::CreateInstance();
65 #endif
67 ~TestPlatformEventSource() override {}
69 uint32_t Dispatch(const ui::PlatformEvent& event) {
70 return DispatchEvent(event);
73 private:
74 DISALLOW_COPY_AND_ASSIGN(TestPlatformEventSource);
77 #if defined(USE_AURA)
78 class TestDispatcherClient : public aura::client::DispatcherClient {
79 public:
80 TestDispatcherClient() : dispatcher_(nullptr) {}
81 ~TestDispatcherClient() override {}
83 base::MessagePumpDispatcher* dispatcher() {
84 return 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),
96 dispatcher);
99 private:
100 void RunNestedDispatcher(scoped_ptr<base::RunLoop> run_loop,
101 base::MessagePumpDispatcher* dispatcher) {
102 base::AutoReset<base::MessagePumpDispatcher*> reset_dispatcher(&dispatcher_,
103 dispatcher);
104 base::MessageLoopForUI* loop = base::MessageLoopForUI::current();
105 base::MessageLoop::ScopedNestableTaskAllower allow(loop);
106 run_loop->Run();
109 base::MessagePumpDispatcher* dispatcher_;
111 DISALLOW_COPY_AND_ASSIGN(TestDispatcherClient);
113 #endif // USE_AURA
115 } // namespace
117 class TestMenuItemViewShown : public MenuItemView {
118 public:
119 TestMenuItemViewShown() : MenuItemView(nullptr) {
120 submenu_ = new SubmenuViewShown(this);
122 ~TestMenuItemViewShown() override {}
124 private:
125 DISALLOW_COPY_AND_ASSIGN(TestMenuItemViewShown);
128 class MenuControllerTest : public ViewsTestBase {
129 public:
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;
140 DispatchEvent();
141 if (count) {
142 base::MessageLoop::current()->PostTask(
143 FROM_HERE,
144 base::Bind(&MenuControllerTest::Step3_DispatchEvents,
145 base::Unretained(this),
146 count - 1));
147 } else {
148 EXPECT_TRUE(run_loop_->running());
149 run_loop_->Quit();
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(
158 FROM_HERE,
159 base::Bind(&MenuControllerTest::Step3_DispatchEvents,
160 base::Unretained(this),
161 3));
162 run_loop_.reset(new base::RunLoop());
163 run_loop_->Run();
166 void Step1_RunMenu() {
167 base::MessageLoop::current()->PostTask(
168 FROM_HERE,
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);
180 widget->Show();
182 #if defined(USE_AURA)
183 aura::client::SetDispatcherClient(
184 widget->GetNativeWindow()->GetRootWindow(), &dispatcher_client_);
185 #endif
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,
223 int index) {
224 return controller_->FindNextSelectableMenuItem(
225 parent, index, MenuController::INCREMENT_SELECTION_DOWN);
228 MenuItemView* FindPreviousSelectableMenuItem(MenuItemView* parent,
229 int index) {
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);
249 #if defined(USE_X11)
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;
256 DispatchEvent();
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);
264 DispatchEvent();
266 #endif
268 void DispatchEvent() {
269 #if defined(USE_X11)
270 XEvent xevent;
271 memset(&xevent, 0, sizeof(xevent));
272 event_source_.Dispatch(&xevent);
273 #elif defined(OS_WIN)
274 MSG msg;
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
288 // to this function.
289 generator.PressKey(ui::VKEY_UP, 0);
290 #else
291 #error Unsupported platform
292 #endif
295 private:
296 void ResetMenuController() {
297 if (controller_) {
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;
302 delete controller_;
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_;
313 #endif
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(
322 FROM_HERE,
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
328 // event dispatcher.
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(
334 FROM_HERE,
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(
349 FROM_HERE,
350 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect,
351 base::Unretained(this),
352 MenuController::EXIT_NONE));
353 RunMenu(owner.get());
356 #endif
358 #if defined(USE_X11)
360 class TestEventHandler : public ui::EventHandler {
361 public:
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_++;
368 break;
369 case ui::ET_TOUCH_RELEASED:
370 case ui::ET_TOUCH_CANCELLED:
371 outstanding_touches_--;
372 break;
373 default:
374 break;
378 int outstanding_touches() const { return outstanding_touches_; }
380 private:
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(
405 FROM_HERE,
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
418 // disabled.
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.
497 ResetSelection();
500 // Tests that opening menu and pressing 'Down' and 'Up' iterates over enabled
501 // items.
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.
519 // Select next item.
520 IncrementSelection();
521 EXPECT_EQ(2, pending_state_item()->GetCommand());
523 // Skip disabled item.
524 IncrementSelection();
525 EXPECT_EQ(4, pending_state_item()->GetCommand());
527 // Wrap around.
528 IncrementSelection();
529 EXPECT_EQ(1, pending_state_item()->GetCommand());
531 // Move up in the menu.
532 // Wrap around.
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.
545 ResetSelection();
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.
570 ResetSelection();
573 } // namespace test
574 } // namespace views