1 // Copyright 2013 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. 4 5 #include "ui/views/controls/combobox/combobox.h" 6 7 #include <set> 8 9 #include "base/basictypes.h" 10 #include "base/strings/utf_string_conversions.h" 11 #include "ui/base/models/combobox_model.h" 12 #include "ui/events/event.h" 13 #include "ui/events/keycodes/keyboard_codes.h" 14 #include "ui/views/controls/combobox/combobox_listener.h" 15 #include "ui/views/controls/menu/menu_runner.h" 16 #include "ui/views/controls/menu/menu_runner_handler.h" 17 #include "ui/views/ime/mock_input_method.h" 18 #include "ui/views/test/menu_runner_test_api.h" 19 #include "ui/views/test/views_test_base.h" 20 #include "ui/views/widget/widget.h" 21 22 namespace views { 23 24 namespace { 25 26 // An dummy implementation of MenuRunnerHandler to check if the dropdown menu is 27 // shown or not. 28 class TestMenuRunnerHandler : public MenuRunnerHandler { 29 public: 30 TestMenuRunnerHandler() 31 : executed_(false) {} 32 33 bool executed() const { return executed_; } 34 35 virtual MenuRunner::RunResult RunMenuAt(Widget* parent, 36 MenuButton* button, 37 const gfx::Rect& bounds, 38 MenuItemView::AnchorPosition anchor, 39 ui::MenuSourceType source_type, 40 int32 types) OVERRIDE { 41 executed_ = true; 42 return MenuRunner::NORMAL_EXIT; 43 } 44 45 private: 46 bool executed_; 47 48 DISALLOW_COPY_AND_ASSIGN(TestMenuRunnerHandler); 49 }; 50 51 // A wrapper of Combobox to intercept the result of OnKeyPressed() and 52 // OnKeyReleased() methods. 53 class TestCombobox : public Combobox { 54 public: 55 explicit TestCombobox(ui::ComboboxModel* model) 56 : Combobox(model), 57 key_handled_(false), 58 key_received_(false) { 59 } 60 61 virtual bool OnKeyPressed(const ui::KeyEvent& e) OVERRIDE { 62 key_received_ = true; 63 key_handled_ = Combobox::OnKeyPressed(e); 64 return key_handled_; 65 } 66 67 virtual bool OnKeyReleased(const ui::KeyEvent& e) OVERRIDE { 68 key_received_ = true; 69 key_handled_ = Combobox::OnKeyReleased(e); 70 return key_handled_; 71 } 72 73 bool key_handled() const { return key_handled_; } 74 bool key_received() const { return key_received_; } 75 76 void clear() { 77 key_received_ = key_handled_ = false; 78 } 79 80 private: 81 bool key_handled_; 82 bool key_received_; 83 84 DISALLOW_COPY_AND_ASSIGN(TestCombobox); 85 }; 86 87 // A concrete class is needed to test the combobox. 88 class TestComboboxModel : public ui::ComboboxModel { 89 public: 90 TestComboboxModel() {} 91 virtual ~TestComboboxModel() {} 92 93 // ui::ComboboxModel: 94 virtual int GetItemCount() const OVERRIDE { 95 return 10; 96 } 97 virtual string16 GetItemAt(int index) OVERRIDE { 98 if (IsItemSeparatorAt(index)) { 99 NOTREACHED(); 100 return ASCIIToUTF16("SEPARATOR"); 101 } 102 return ASCIIToUTF16(index % 2 == 0 ? "PEANUT BUTTER" : "JELLY"); 103 } 104 virtual bool IsItemSeparatorAt(int index) OVERRIDE { 105 return separators_.find(index) != separators_.end(); 106 } 107 108 void SetSeparators(const std::set<int>& separators) { 109 separators_ = separators; 110 } 111 112 private: 113 std::set<int> separators_; 114 115 DISALLOW_COPY_AND_ASSIGN(TestComboboxModel); 116 }; 117 118 class EvilListener : public ComboboxListener { 119 public: 120 EvilListener() : deleted_(false) {}; 121 virtual ~EvilListener() {}; 122 123 // ComboboxListener: 124 virtual void OnSelectedIndexChanged(Combobox* combobox) OVERRIDE { 125 delete combobox; 126 deleted_ = true; 127 } 128 129 bool deleted() const { return deleted_; } 130 131 private: 132 bool deleted_; 133 134 DISALLOW_COPY_AND_ASSIGN(EvilListener); 135 }; 136 137 class TestComboboxListener : public views::ComboboxListener { 138 public: 139 TestComboboxListener() 140 : on_selected_index_changed_called_(false), 141 on_combobox_text_button_clicked_called_(false) { 142 } 143 virtual ~TestComboboxListener() {} 144 145 virtual void OnSelectedIndexChanged(views::Combobox* combobox) OVERRIDE { 146 on_selected_index_changed_called_ = true; 147 } 148 149 virtual void OnComboboxTextButtonClicked(views::Combobox* combobox) OVERRIDE { 150 on_combobox_text_button_clicked_called_ = true; 151 } 152 153 bool on_selected_index_changed_called() const { 154 return on_selected_index_changed_called_; 155 } 156 157 bool on_combobox_text_button_clicked_called() const { 158 return on_combobox_text_button_clicked_called_; 159 } 160 161 private: 162 bool on_selected_index_changed_called_; 163 bool on_combobox_text_button_clicked_called_; 164 165 private: 166 DISALLOW_COPY_AND_ASSIGN(TestComboboxListener); 167 }; 168 169 } // namespace 170 171 class ComboboxTest : public ViewsTestBase { 172 public: 173 ComboboxTest() : widget_(NULL), combobox_(NULL), input_method_(NULL) {} 174 175 virtual void TearDown() OVERRIDE { 176 if (widget_) 177 widget_->Close(); 178 ViewsTestBase::TearDown(); 179 } 180 181 void InitCombobox() { 182 model_.reset(new TestComboboxModel()); 183 184 ASSERT_FALSE(combobox_); 185 combobox_ = new TestCombobox(model_.get()); 186 combobox_->set_id(1); 187 188 widget_ = new Widget; 189 Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); 190 params.bounds = gfx::Rect(200, 200, 200, 200); 191 widget_->Init(params); 192 View* container = new View(); 193 widget_->SetContentsView(container); 194 container->AddChildView(combobox_); 195 196 input_method_ = new MockInputMethod(); 197 widget_->ReplaceInputMethod(input_method_); 198 199 // Assumes the Widget is always focused. 200 input_method_->OnFocus(); 201 202 combobox_->RequestFocus(); 203 combobox_->SizeToPreferredSize(); 204 } 205 206 protected: 207 void SendKeyEvent(ui::KeyboardCode key_code) { 208 SendKeyEventWithType(key_code, ui::ET_KEY_PRESSED); 209 } 210 211 void SendKeyEventWithType(ui::KeyboardCode key_code, ui::EventType type) { 212 ui::KeyEvent event(type, key_code, 0, false); 213 input_method_->DispatchKeyEvent(event); 214 } 215 216 View* GetFocusedView() { 217 return widget_->GetFocusManager()->GetFocusedView(); 218 } 219 220 void PerformClick(const gfx::Point& point) { 221 ui::MouseEvent pressed_event = ui::MouseEvent(ui::ET_MOUSE_PRESSED, point, 222 point, 223 ui::EF_LEFT_MOUSE_BUTTON); 224 widget_->OnMouseEvent(&pressed_event); 225 ui::MouseEvent released_event = ui::MouseEvent(ui::ET_MOUSE_RELEASED, point, 226 point, 227 ui::EF_LEFT_MOUSE_BUTTON); 228 widget_->OnMouseEvent(&released_event); 229 } 230 231 // We need widget to populate wrapper class. 232 Widget* widget_; 233 234 // |combobox_| will be allocated InitCombobox() and then owned by |widget_|. 235 TestCombobox* combobox_; 236 237 // Combobox does not take ownership of the model, hence it needs to be scoped. 238 scoped_ptr<TestComboboxModel> model_; 239 240 // For testing input method related behaviors. 241 MockInputMethod* input_method_; 242 }; 243 244 TEST_F(ComboboxTest, KeyTest) { 245 InitCombobox(); 246 SendKeyEvent(ui::VKEY_END); 247 EXPECT_EQ(combobox_->selected_index() + 1, model_->GetItemCount()); 248 SendKeyEvent(ui::VKEY_HOME); 249 EXPECT_EQ(combobox_->selected_index(), 0); 250 SendKeyEvent(ui::VKEY_DOWN); 251 SendKeyEvent(ui::VKEY_DOWN); 252 EXPECT_EQ(combobox_->selected_index(), 2); 253 SendKeyEvent(ui::VKEY_RIGHT); 254 EXPECT_EQ(combobox_->selected_index(), 2); 255 SendKeyEvent(ui::VKEY_LEFT); 256 EXPECT_EQ(combobox_->selected_index(), 2); 257 SendKeyEvent(ui::VKEY_UP); 258 EXPECT_EQ(combobox_->selected_index(), 1); 259 SendKeyEvent(ui::VKEY_PRIOR); 260 EXPECT_EQ(combobox_->selected_index(), 0); 261 SendKeyEvent(ui::VKEY_NEXT); 262 EXPECT_EQ(combobox_->selected_index(), model_->GetItemCount() - 1); 263 } 264 265 // Check that if a combobox is disabled before it has a native wrapper, then the 266 // native wrapper inherits the disabled state when it gets created. 267 TEST_F(ComboboxTest, DisabilityTest) { 268 model_.reset(new TestComboboxModel()); 269 270 ASSERT_FALSE(combobox_); 271 combobox_ = new TestCombobox(model_.get()); 272 combobox_->SetEnabled(false); 273 274 widget_ = new Widget; 275 Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); 276 params.bounds = gfx::Rect(100, 100, 100, 100); 277 widget_->Init(params); 278 View* container = new View(); 279 widget_->SetContentsView(container); 280 container->AddChildView(combobox_); 281 EXPECT_FALSE(combobox_->enabled()); 282 } 283 284 // Verifies that we don't select a separator line in combobox when navigating 285 // through keyboard. 286 TEST_F(ComboboxTest, SkipSeparatorSimple) { 287 InitCombobox(); 288 std::set<int> separators; 289 separators.insert(2); 290 model_->SetSeparators(separators); 291 EXPECT_EQ(0, combobox_->selected_index()); 292 SendKeyEvent(ui::VKEY_DOWN); 293 EXPECT_EQ(1, combobox_->selected_index()); 294 SendKeyEvent(ui::VKEY_DOWN); 295 EXPECT_EQ(3, combobox_->selected_index()); 296 SendKeyEvent(ui::VKEY_UP); 297 EXPECT_EQ(1, combobox_->selected_index()); 298 SendKeyEvent(ui::VKEY_HOME); 299 EXPECT_EQ(0, combobox_->selected_index()); 300 SendKeyEvent(ui::VKEY_PRIOR); 301 EXPECT_EQ(0, combobox_->selected_index()); 302 SendKeyEvent(ui::VKEY_END); 303 EXPECT_EQ(9, combobox_->selected_index()); 304 } 305 306 // Verifies that we never select the separator that is in the beginning of the 307 // combobox list when navigating through keyboard. 308 TEST_F(ComboboxTest, SkipSeparatorBeginning) { 309 InitCombobox(); 310 std::set<int> separators; 311 separators.insert(0); 312 model_->SetSeparators(separators); 313 EXPECT_EQ(0, combobox_->selected_index()); 314 SendKeyEvent(ui::VKEY_DOWN); 315 EXPECT_EQ(1, combobox_->selected_index()); 316 SendKeyEvent(ui::VKEY_DOWN); 317 EXPECT_EQ(2, combobox_->selected_index()); 318 SendKeyEvent(ui::VKEY_UP); 319 EXPECT_EQ(1, combobox_->selected_index()); 320 SendKeyEvent(ui::VKEY_HOME); 321 EXPECT_EQ(1, combobox_->selected_index()); 322 SendKeyEvent(ui::VKEY_PRIOR); 323 EXPECT_EQ(1, combobox_->selected_index()); 324 SendKeyEvent(ui::VKEY_END); 325 EXPECT_EQ(9, combobox_->selected_index()); 326 } 327 328 // Verifies that we never select the separator that is in the end of the 329 // combobox list when navigating through keyboard. 330 TEST_F(ComboboxTest, SkipSeparatorEnd) { 331 InitCombobox(); 332 std::set<int> separators; 333 separators.insert(model_->GetItemCount() - 1); 334 model_->SetSeparators(separators); 335 combobox_->SetSelectedIndex(8); 336 SendKeyEvent(ui::VKEY_DOWN); 337 EXPECT_EQ(8, combobox_->selected_index()); 338 SendKeyEvent(ui::VKEY_UP); 339 EXPECT_EQ(7, combobox_->selected_index()); 340 SendKeyEvent(ui::VKEY_END); 341 EXPECT_EQ(8, combobox_->selected_index()); 342 } 343 344 // Verifies that we never select any of the adjacent separators (multiple 345 // consecutive) that appear in the beginning of the combobox list when 346 // navigating through keyboard. 347 TEST_F(ComboboxTest, SkipMultipleSeparatorsAtBeginning) { 348 InitCombobox(); 349 std::set<int> separators; 350 separators.insert(0); 351 separators.insert(1); 352 separators.insert(2); 353 model_->SetSeparators(separators); 354 EXPECT_EQ(0, combobox_->selected_index()); 355 SendKeyEvent(ui::VKEY_DOWN); 356 EXPECT_EQ(3, combobox_->selected_index()); 357 SendKeyEvent(ui::VKEY_UP); 358 EXPECT_EQ(3, combobox_->selected_index()); 359 SendKeyEvent(ui::VKEY_NEXT); 360 EXPECT_EQ(9, combobox_->selected_index()); 361 SendKeyEvent(ui::VKEY_HOME); 362 EXPECT_EQ(3, combobox_->selected_index()); 363 SendKeyEvent(ui::VKEY_END); 364 EXPECT_EQ(9, combobox_->selected_index()); 365 SendKeyEvent(ui::VKEY_PRIOR); 366 EXPECT_EQ(3, combobox_->selected_index()); 367 } 368 369 // Verifies that we never select any of the adjacent separators (multiple 370 // consecutive) that appear in the middle of the combobox list when navigating 371 // through keyboard. 372 TEST_F(ComboboxTest, SkipMultipleAdjacentSeparatorsAtMiddle) { 373 InitCombobox(); 374 std::set<int> separators; 375 separators.insert(4); 376 separators.insert(5); 377 separators.insert(6); 378 model_->SetSeparators(separators); 379 combobox_->SetSelectedIndex(3); 380 SendKeyEvent(ui::VKEY_DOWN); 381 EXPECT_EQ(7, combobox_->selected_index()); 382 SendKeyEvent(ui::VKEY_UP); 383 EXPECT_EQ(3, combobox_->selected_index()); 384 } 385 386 // Verifies that we never select any of the adjacent separators (multiple 387 // consecutive) that appear in the end of the combobox list when navigating 388 // through keyboard. 389 TEST_F(ComboboxTest, SkipMultipleSeparatorsAtEnd) { 390 InitCombobox(); 391 std::set<int> separators; 392 separators.insert(7); 393 separators.insert(8); 394 separators.insert(9); 395 model_->SetSeparators(separators); 396 combobox_->SetSelectedIndex(6); 397 SendKeyEvent(ui::VKEY_DOWN); 398 EXPECT_EQ(6, combobox_->selected_index()); 399 SendKeyEvent(ui::VKEY_UP); 400 EXPECT_EQ(5, combobox_->selected_index()); 401 SendKeyEvent(ui::VKEY_HOME); 402 EXPECT_EQ(0, combobox_->selected_index()); 403 SendKeyEvent(ui::VKEY_NEXT); 404 EXPECT_EQ(6, combobox_->selected_index()); 405 SendKeyEvent(ui::VKEY_PRIOR); 406 EXPECT_EQ(0, combobox_->selected_index()); 407 SendKeyEvent(ui::VKEY_END); 408 EXPECT_EQ(6, combobox_->selected_index()); 409 } 410 411 TEST_F(ComboboxTest, GetTextForRowTest) { 412 InitCombobox(); 413 std::set<int> separators; 414 separators.insert(0); 415 separators.insert(1); 416 separators.insert(9); 417 model_->SetSeparators(separators); 418 for (int i = 0; i < combobox_->GetRowCount(); ++i) { 419 if (separators.count(i) != 0) { 420 EXPECT_TRUE(combobox_->GetTextForRow(i).empty()) << i; 421 } else { 422 EXPECT_EQ(ASCIIToUTF16(i % 2 == 0 ? "PEANUT BUTTER" : "JELLY"), 423 combobox_->GetTextForRow(i)) << i; 424 } 425 } 426 } 427 428 // Verifies selecting the first matching value (and returning whether found). 429 TEST_F(ComboboxTest, SelectValue) { 430 InitCombobox(); 431 ASSERT_EQ(model_->GetDefaultIndex(), combobox_->selected_index()); 432 EXPECT_TRUE(combobox_->SelectValue(ASCIIToUTF16("PEANUT BUTTER"))); 433 EXPECT_EQ(0, combobox_->selected_index()); 434 EXPECT_TRUE(combobox_->SelectValue(ASCIIToUTF16("JELLY"))); 435 EXPECT_EQ(1, combobox_->selected_index()); 436 EXPECT_FALSE(combobox_->SelectValue(ASCIIToUTF16("BANANAS"))); 437 EXPECT_EQ(1, combobox_->selected_index()); 438 } 439 440 TEST_F(ComboboxTest, ListenerHandlesDelete) { 441 TestComboboxModel model; 442 TestCombobox* combobox = new TestCombobox(&model); // Deleted on change. 443 EvilListener evil_listener; 444 combobox->set_listener(&evil_listener); 445 ASSERT_NO_FATAL_FAILURE(combobox->ExecuteCommand(2)); 446 EXPECT_TRUE(evil_listener.deleted()); 447 } 448 449 TEST_F(ComboboxTest, Click) { 450 InitCombobox(); 451 452 TestComboboxListener listener; 453 combobox_->set_listener(&listener); 454 455 combobox_->Layout(); 456 457 // Click the left side. The menu is shown. 458 TestMenuRunnerHandler* test_menu_runner_handler = new TestMenuRunnerHandler(); 459 scoped_ptr<MenuRunnerHandler> menu_runner_handler(test_menu_runner_handler); 460 test::MenuRunnerTestAPI test_api( 461 combobox_->dropdown_list_menu_runner_.get()); 462 test_api.SetMenuRunnerHandler(menu_runner_handler.Pass()); 463 PerformClick(gfx::Point(combobox_->x() + 1, 464 combobox_->y() + combobox_->height() / 2)); 465 EXPECT_FALSE(listener.on_combobox_text_button_clicked_called()); 466 EXPECT_TRUE(test_menu_runner_handler->executed()); 467 } 468 469 TEST_F(ComboboxTest, NotifyOnClickWithReturnKey) { 470 InitCombobox(); 471 472 TestComboboxListener listener; 473 combobox_->set_listener(&listener); 474 475 // With STYLE_SHOW_DROP_DOWN_ON_CLICK, the click event is ignored. 476 SendKeyEvent(ui::VKEY_RETURN); 477 EXPECT_FALSE(listener.on_combobox_text_button_clicked_called()); 478 479 // With STYLE_NOTIFY_ON_CLICK, the click event is notified. 480 combobox_->SetStyle(Combobox::STYLE_NOTIFY_ON_CLICK); 481 SendKeyEvent(ui::VKEY_RETURN); 482 EXPECT_TRUE(listener.on_combobox_text_button_clicked_called()); 483 } 484 485 TEST_F(ComboboxTest, NotifyOnClickWithSpaceKey) { 486 InitCombobox(); 487 488 TestComboboxListener listener; 489 combobox_->set_listener(&listener); 490 491 // With STYLE_SHOW_DROP_DOWN_ON_CLICK, the click event is ignored. 492 SendKeyEvent(ui::VKEY_SPACE); 493 EXPECT_FALSE(listener.on_combobox_text_button_clicked_called()); 494 SendKeyEventWithType(ui::VKEY_SPACE, ui::ET_KEY_RELEASED); 495 EXPECT_FALSE(listener.on_combobox_text_button_clicked_called()); 496 497 // With STYLE_NOTIFY_ON_CLICK, the click event is notified after releasing. 498 combobox_->SetStyle(Combobox::STYLE_NOTIFY_ON_CLICK); 499 SendKeyEvent(ui::VKEY_SPACE); 500 EXPECT_FALSE(listener.on_combobox_text_button_clicked_called()); 501 SendKeyEventWithType(ui::VKEY_SPACE, ui::ET_KEY_RELEASED); 502 EXPECT_TRUE(listener.on_combobox_text_button_clicked_called()); 503 } 504 505 TEST_F(ComboboxTest, NotifyOnClickWithMouse) { 506 InitCombobox(); 507 508 TestComboboxListener listener; 509 combobox_->set_listener(&listener); 510 511 combobox_->SetStyle(Combobox::STYLE_NOTIFY_ON_CLICK); 512 combobox_->Layout(); 513 514 // Click the right side (arrow button). The menu is shown. 515 TestMenuRunnerHandler* test_menu_runner_handler = new TestMenuRunnerHandler(); 516 scoped_ptr<MenuRunnerHandler> menu_runner_handler(test_menu_runner_handler); 517 scoped_ptr<test::MenuRunnerTestAPI> test_api( 518 new test::MenuRunnerTestAPI(combobox_->dropdown_list_menu_runner_.get())); 519 test_api->SetMenuRunnerHandler(menu_runner_handler.Pass()); 520 521 PerformClick(gfx::Point(combobox_->x() + combobox_->width() - 1, 522 combobox_->y() + combobox_->height() / 2)); 523 EXPECT_FALSE(listener.on_combobox_text_button_clicked_called()); 524 EXPECT_TRUE(test_menu_runner_handler->executed()); 525 526 // Click the left side (text button). The click event is notified. 527 test_menu_runner_handler = new TestMenuRunnerHandler(); 528 menu_runner_handler.reset(test_menu_runner_handler); 529 test_api.reset( 530 new test::MenuRunnerTestAPI(combobox_->dropdown_list_menu_runner_.get())); 531 test_api->SetMenuRunnerHandler(menu_runner_handler.Pass()); 532 PerformClick(gfx::Point(combobox_->x() + 1, 533 combobox_->y() + combobox_->height() / 2)); 534 EXPECT_TRUE(listener.on_combobox_text_button_clicked_called()); 535 EXPECT_FALSE(test_menu_runner_handler->executed()); 536 } 537 538 TEST_F(ComboboxTest, ConsumingPressKeyEvents) { 539 InitCombobox(); 540 541 EXPECT_FALSE(combobox_->OnKeyPressed( 542 ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, 0, false))); 543 EXPECT_FALSE(combobox_->OnKeyPressed( 544 ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_SPACE, 0, false))); 545 546 // When the combobox's style is STYLE_NOTIFY_ON_CLICK, pressing events of 547 // a space key or an enter key will be consumed. 548 combobox_->SetStyle(Combobox::STYLE_NOTIFY_ON_CLICK); 549 EXPECT_TRUE(combobox_->OnKeyPressed( 550 ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, 0, false))); 551 EXPECT_TRUE(combobox_->OnKeyPressed( 552 ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_SPACE, 0, false))); 553 } 554 555 } // namespace views 556