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
22namespace views {
23
24namespace {
25
26// An dummy implementation of MenuRunnerHandler to check if the dropdown menu is
27// shown or not.
28class 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.
53class 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.
88class 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
118class 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
137class 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
171class 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
244TEST_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.
267TEST_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.
286TEST_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.
308TEST_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.
330TEST_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.
347TEST_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.
372TEST_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.
389TEST_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
411TEST_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).
429TEST_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
440TEST_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
449TEST_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
469TEST_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
485TEST_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
505TEST_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
538TEST_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