blob: 6e49f076fc1ddaee3d7fd0a4b5040f48877ff026 [file] [log] [blame]
// Copyright 2019 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/editable_combobox/editable_combobox.h"
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/callback.h"
#include "base/macros.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/models/combobox_model.h"
#include "ui/base/models/simple_combobox_model.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/render_text.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/test/menu_test_utils.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_utils.h"
namespace views {
namespace {
using base::ASCIIToUTF16;
using views::test::WaitForMenuClosureAnimation;
// No-op test double of a ContextMenuController
class TestContextMenuController : public ContextMenuController {
public:
TestContextMenuController() = default;
~TestContextMenuController() override = default;
// ContextMenuController:
void ShowContextMenuForViewImpl(View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) override {
opened_menu_ = true;
}
bool opened_menu() const { return opened_menu_; }
private:
bool opened_menu_ = false;
DISALLOW_COPY_AND_ASSIGN(TestContextMenuController);
};
class EditableComboboxTest : public ViewsTestBase {
public:
EditableComboboxTest() { views::test::DisableMenuClosureAnimations(); }
void TearDown() override;
// Initializes the combobox with the given number of items.
void InitEditableCombobox(int item_count = 8,
bool filter_on_edit = false,
bool show_on_empty = true);
// Initializes the combobox with the given items.
void InitEditableCombobox(
const std::vector<std::u16string>& items,
bool filter_on_edit,
bool show_on_empty = true,
EditableCombobox::Type type = EditableCombobox::Type::kRegular);
// Initializes the widget where the combobox and the dummy control live.
void InitWidget();
protected:
void ClickArrow();
void ClickMenuItem(int index);
void ClickTextfield();
void DragMouseTo(const gfx::Point& location);
bool IsMenuOpen();
void PerformMouseEvent(Widget* widget,
const gfx::Point& point,
ui::EventType type);
void PerformClick(Widget* widget, const gfx::Point& point);
void SendKeyEvent(ui::KeyboardCode key_code,
bool alt = false,
bool shift = false,
bool ctrl_cmd = false);
int change_count() const { return change_count_; }
void OnContentChanged() { ++change_count_; }
// The widget where the control will appear.
Widget* widget_ = nullptr;
// |combobox_| and |dummy_focusable_view_| are allocated in
// |InitEditableCombobox| and then owned by |widget_|.
EditableCombobox* combobox_ = nullptr;
View* dummy_focusable_view_ = nullptr;
// We make |combobox_| a child of another View to test different removal
// scenarios.
View* parent_of_combobox_ = nullptr;
int change_count_ = 0;
std::unique_ptr<ui::test::EventGenerator> event_generator_;
private:
DISALLOW_COPY_AND_ASSIGN(EditableComboboxTest);
};
void EditableComboboxTest::TearDown() {
if (IsMenuOpen()) {
combobox_->GetMenuRunnerForTest()->Cancel();
WaitForMenuClosureAnimation();
}
if (widget_)
widget_->Close();
ViewsTestBase::TearDown();
}
// Initializes the combobox with the given number of items.
void EditableComboboxTest::InitEditableCombobox(const int item_count,
const bool filter_on_edit,
const bool show_on_empty) {
std::vector<std::u16string> items;
for (int i = 0; i < item_count; ++i)
items.push_back(ASCIIToUTF16(base::StringPrintf("item[%i]", i)));
InitEditableCombobox(items, filter_on_edit, show_on_empty);
}
// Initializes the combobox with the given items.
void EditableComboboxTest::InitEditableCombobox(
const std::vector<std::u16string>& items,
const bool filter_on_edit,
const bool show_on_empty,
const EditableCombobox::Type type) {
parent_of_combobox_ = new View();
parent_of_combobox_->SetID(1);
combobox_ =
new EditableCombobox(std::make_unique<ui::SimpleComboboxModel>(items),
filter_on_edit, show_on_empty, type);
combobox_->SetCallback(base::BindRepeating(
&EditableComboboxTest::OnContentChanged, base::Unretained(this)));
combobox_->SetID(2);
dummy_focusable_view_ = new View();
dummy_focusable_view_->SetFocusBehavior(View::FocusBehavior::ALWAYS);
dummy_focusable_view_->SetID(3);
InitWidget();
}
// Initializes the widget where the combobox and the dummy control live.
void EditableComboboxTest::InitWidget() {
widget_ = new Widget();
Widget::InitParams params =
CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.bounds = gfx::Rect(0, 0, 1000, 1000);
parent_of_combobox_->SetBoundsRect(gfx::Rect(0, 0, 500, 40));
combobox_->SetBoundsRect(gfx::Rect(0, 0, 500, 40));
widget_->Init(std::move(params));
View* container = widget_->SetContentsView(std::make_unique<View>());
container->AddChildView(parent_of_combobox_);
parent_of_combobox_->AddChildView(combobox_);
container->AddChildView(dummy_focusable_view_);
widget_->Show();
#if defined(OS_APPLE)
// The event loop needs to be flushed here, otherwise in various tests:
// 1. The actual showing of the native window backing the widget gets delayed
// until a spin of the event loop.
// 2. The combobox menu object is triggered, and it starts listening for the
// "window did become key" notification as a sign that it lost focus and
// should close.
// 3. The event loop is spun, and the actual showing of the native window
// triggers the close of the menu opened from within the window.
base::RunLoop().RunUntilIdle();
#endif
event_generator_ =
std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_));
event_generator_->set_target(ui::test::EventGenerator::Target::WINDOW);
}
void EditableComboboxTest::ClickArrow() {
const gfx::Point arrow_button(combobox_->x() + combobox_->width() - 1,
combobox_->y() + 1);
PerformClick(widget_, arrow_button);
}
void EditableComboboxTest::ClickMenuItem(const int index) {
DCHECK(combobox_->GetMenuRunnerForTest());
const gfx::Point middle_of_item(
combobox_->x() + combobox_->width() / 2,
combobox_->y() + combobox_->height() / 2 + combobox_->height() * index);
// For the menu, we send the click event to the child widget where the menu is
// shown. That child widget is the MenuHost object created inside
// EditableCombobox's MenuRunner to host the menu items.
std::set<Widget*> child_widgets;
Widget::GetAllOwnedWidgets(widget_->GetNativeView(), &child_widgets);
ASSERT_EQ(1UL, child_widgets.size());
PerformClick(*child_widgets.begin(), middle_of_item);
}
void EditableComboboxTest::ClickTextfield() {
const gfx::Point textfield(combobox_->x() + 1, combobox_->y() + 1);
PerformClick(widget_, textfield);
}
void EditableComboboxTest::DragMouseTo(const gfx::Point& location) {
ui::MouseEvent drag(ui::ET_MOUSE_DRAGGED, location, location,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0);
combobox_->GetTextfieldForTest()->OnMouseDragged(drag);
}
bool EditableComboboxTest::IsMenuOpen() {
return combobox_ && combobox_->GetMenuRunnerForTest() &&
combobox_->GetMenuRunnerForTest()->IsRunning();
}
void EditableComboboxTest::PerformMouseEvent(Widget* widget,
const gfx::Point& point,
const ui::EventType type) {
ui::MouseEvent event = ui::MouseEvent(
type, point, point, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON | ui::EF_NUM_LOCK_ON, ui::EF_LEFT_MOUSE_BUTTON);
widget->OnMouseEvent(&event);
}
void EditableComboboxTest::PerformClick(Widget* widget,
const gfx::Point& point) {
PerformMouseEvent(widget, point, ui::ET_MOUSE_PRESSED);
PerformMouseEvent(widget, point, ui::ET_MOUSE_RELEASED);
}
void EditableComboboxTest::SendKeyEvent(ui::KeyboardCode key_code,
const bool alt,
const bool shift,
const bool ctrl_cmd) {
#if defined(OS_APPLE)
bool command = ctrl_cmd;
bool control = false;
#else
bool command = false;
bool control = ctrl_cmd;
#endif
int flags = (shift ? ui::EF_SHIFT_DOWN : 0) |
(control ? ui::EF_CONTROL_DOWN : 0) |
(alt ? ui::EF_ALT_DOWN : 0) | (command ? ui::EF_COMMAND_DOWN : 0);
event_generator_->PressKey(key_code, flags);
}
TEST_F(EditableComboboxTest, FocusOnTextfieldDoesntOpenMenu) {
InitEditableCombobox();
EXPECT_FALSE(IsMenuOpen());
combobox_->GetTextfieldForTest()->RequestFocus();
EXPECT_FALSE(IsMenuOpen());
}
TEST_F(EditableComboboxTest, ArrowDownOpensMenu) {
InitEditableCombobox();
EXPECT_FALSE(IsMenuOpen());
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_DOWN);
EXPECT_TRUE(IsMenuOpen());
}
TEST_F(EditableComboboxTest, TabMovesToOtherViewAndClosesMenu) {
InitEditableCombobox();
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
EXPECT_TRUE(combobox_->GetTextfieldForTest()->HasFocus());
SendKeyEvent(ui::VKEY_TAB);
EXPECT_FALSE(combobox_->GetTextfieldForTest()->HasFocus());
EXPECT_TRUE(dummy_focusable_view_->HasFocus());
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
}
TEST_F(EditableComboboxTest,
ClickOutsideEditableComboboxWithoutLosingFocusClosesMenu) {
InitEditableCombobox();
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
EXPECT_TRUE(combobox_->GetTextfieldForTest()->HasFocus());
const gfx::Point outside_point(combobox_->x() + combobox_->width() + 1,
combobox_->y() + 1);
PerformClick(widget_, outside_point);
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
EXPECT_TRUE(combobox_->GetTextfieldForTest()->HasFocus());
}
TEST_F(EditableComboboxTest, ClickTextfieldDoesntCloseMenu) {
InitEditableCombobox();
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
MenuRunner* menu_runner1 = combobox_->GetMenuRunnerForTest();
ClickTextfield();
MenuRunner* menu_runner2 = combobox_->GetMenuRunnerForTest();
EXPECT_TRUE(IsMenuOpen());
// Making sure the menu didn't close and reopen (causing a flicker).
EXPECT_EQ(menu_runner1, menu_runner2);
}
TEST_F(EditableComboboxTest, RemovingControlWhileMenuOpenClosesMenu) {
InitEditableCombobox();
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
parent_of_combobox_->RemoveChildView(combobox_);
EXPECT_EQ(nullptr, combobox_->GetMenuRunnerForTest());
}
TEST_F(EditableComboboxTest, RemovingParentOfControlWhileMenuOpenClosesMenu) {
InitEditableCombobox();
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
widget_->GetContentsView()->RemoveChildView(parent_of_combobox_);
EXPECT_EQ(nullptr, combobox_->GetMenuRunnerForTest());
}
TEST_F(EditableComboboxTest, LeftOrRightKeysMoveInTextfield) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_E);
SendKeyEvent(ui::VKEY_LEFT);
SendKeyEvent(ui::VKEY_LEFT);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_RIGHT);
SendKeyEvent(ui::VKEY_D);
EXPECT_EQ(u"abcde", combobox_->GetText());
}
#if defined(OS_WIN)
// Flaky on Windows. https://crbug.com/965601
#define MAYBE_UpOrDownKeysMoveInMenu DISABLED_UpOrDownKeysMoveInMenu
#else
#define MAYBE_UpOrDownKeysMoveInMenu UpOrDownKeysMoveInMenu
#endif
TEST_F(EditableComboboxTest, MAYBE_UpOrDownKeysMoveInMenu) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_UP);
SendKeyEvent(ui::VKEY_RETURN);
WaitForMenuClosureAnimation();
EXPECT_EQ(u"item[1]", combobox_->GetText());
}
TEST_F(EditableComboboxTest, EndOrHomeMovesToBeginningOrEndOfText) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_HOME);
SendKeyEvent(ui::VKEY_X);
SendKeyEvent(ui::VKEY_END);
SendKeyEvent(ui::VKEY_Y);
EXPECT_EQ(u"xabcy", combobox_->GetText());
}
#if defined(OS_APPLE)
TEST_F(EditableComboboxTest, AltLeftOrRightMovesToNextWords) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
combobox_->SetText(u"foo bar foobar");
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/true, /*shift=*/false,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/true, /*shift=*/false,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_X);
SendKeyEvent(ui::VKEY_RIGHT, /*alt=*/true, /*shift=*/false,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_Y);
EXPECT_EQ(u"foo xbary foobar", combobox_->GetText());
}
TEST_F(EditableComboboxTest, CtrlLeftOrRightMovesToBeginningOrEndOfText) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/false, /*shift=*/false,
/*ctrl_cmd=*/true);
SendKeyEvent(ui::VKEY_X);
SendKeyEvent(ui::VKEY_RIGHT, /*alt=*/false, /*shift=*/false,
/*ctrl_cmd=*/true);
SendKeyEvent(ui::VKEY_Y);
EXPECT_EQ(u"xabcy", combobox_->GetText());
}
#else
TEST_F(EditableComboboxTest, AltLeftOrRightDoesNothing) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/true, /*shift=*/false,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_X);
SendKeyEvent(ui::VKEY_LEFT);
SendKeyEvent(ui::VKEY_RIGHT, /*alt=*/true, /*shift=*/false,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_Y);
EXPECT_EQ(u"abcyx", combobox_->GetText());
}
TEST_F(EditableComboboxTest, CtrlLeftOrRightMovesToNextWords) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
combobox_->SetText(u"foo bar foobar");
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/false, /*shift=*/false,
/*ctrl_cmd=*/true);
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/false, /*shift=*/false,
/*ctrl_cmd=*/true);
SendKeyEvent(ui::VKEY_X);
SendKeyEvent(ui::VKEY_RIGHT, /*alt=*/false, /*shift=*/false,
/*ctrl_cmd=*/true);
SendKeyEvent(ui::VKEY_Y);
#if defined(OS_WIN)
// Matches Windows-specific logic in
// RenderTextHarfBuzz::AdjacentWordSelectionModel.
EXPECT_EQ(u"foo xbar yfoobar", combobox_->GetText());
#else
EXPECT_EQ(u"foo xbary foobar", combobox_->GetText());
#endif
}
#endif
TEST_F(EditableComboboxTest, ShiftLeftOrRightSelectsCharInTextfield) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_LEFT, /*alt=*/false, /*shift=*/true,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_X);
SendKeyEvent(ui::VKEY_LEFT);
SendKeyEvent(ui::VKEY_LEFT);
SendKeyEvent(ui::VKEY_RIGHT, /*alt=*/false, /*shift=*/true,
/*ctrl_cmd=*/false);
SendKeyEvent(ui::VKEY_Y);
EXPECT_EQ(u"ayx", combobox_->GetText());
}
TEST_F(EditableComboboxTest, EnterClosesMenuWhileSelectingHighlightedMenuItem) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_DOWN);
EXPECT_TRUE(IsMenuOpen());
SendKeyEvent(ui::VKEY_RETURN);
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
EXPECT_EQ(u"item[0]", combobox_->GetText());
}
#if defined(OS_WIN)
// Flaky on Windows. https://crbug.com/965601
#define MAYBE_F4ClosesMenuWhileSelectingHighlightedMenuItem \
DISABLED_F4ClosesMenuWhileSelectingHighlightedMenuItem
#else
#define MAYBE_F4ClosesMenuWhileSelectingHighlightedMenuItem \
F4ClosesMenuWhileSelectingHighlightedMenuItem
#endif
TEST_F(EditableComboboxTest,
MAYBE_F4ClosesMenuWhileSelectingHighlightedMenuItem) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_DOWN);
EXPECT_TRUE(IsMenuOpen());
SendKeyEvent(ui::VKEY_F4);
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
EXPECT_EQ(u"item[0]", combobox_->GetText());
}
TEST_F(EditableComboboxTest, EscClosesMenuWithoutSelectingHighlightedMenuItem) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_DOWN);
EXPECT_TRUE(IsMenuOpen());
SendKeyEvent(ui::VKEY_ESCAPE);
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
EXPECT_EQ(u"a", combobox_->GetText());
}
TEST_F(EditableComboboxTest, TypingInTextfieldUnhighlightsMenuItem) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_B);
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_C);
SendKeyEvent(ui::VKEY_RETURN);
EXPECT_EQ(u"abc", combobox_->GetText());
}
TEST_F(EditableComboboxTest, ClickOnMenuItemSelectsItAndClosesMenu) {
InitEditableCombobox();
ClickArrow();
ASSERT_TRUE(IsMenuOpen());
ClickMenuItem(/*index=*/0);
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
EXPECT_EQ(u"item[0]", combobox_->GetText());
}
// This is different from the regular read-only Combobox, where SPACE
// opens/closes the menu.
TEST_F(EditableComboboxTest, SpaceIsReflectedInTextfield) {
InitEditableCombobox();
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_A);
SendKeyEvent(ui::VKEY_SPACE);
SendKeyEvent(ui::VKEY_SPACE);
SendKeyEvent(ui::VKEY_B);
EXPECT_EQ(u"a b", combobox_->GetText());
}
#if defined(OS_WIN)
// Flaky on Windows. https://crbug.com/965601
#define MAYBE_MenuCanAdaptToContentChange DISABLED_MenuCanAdaptToContentChange
#else
#define MAYBE_MenuCanAdaptToContentChange MenuCanAdaptToContentChange
#endif
// We test that the menu can adapt to content change by using an
// EditableCombobox with |filter_on_edit| set to true, which will change the
// menu's content as the user types.
TEST_F(EditableComboboxTest, MAYBE_MenuCanAdaptToContentChange) {
std::vector<std::u16string> items = {u"abc", u"abd", u"bac", u"bad"};
InitEditableCombobox(items, /*filter_on_edit=*/true);
ClickArrow();
ASSERT_TRUE(IsMenuOpen());
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_RETURN);
WaitForMenuClosureAnimation();
EXPECT_EQ(u"abc", combobox_->GetText());
SendKeyEvent(ui::VKEY_BACK);
SendKeyEvent(ui::VKEY_BACK);
SendKeyEvent(ui::VKEY_BACK);
MenuRunner* menu_runner1 = combobox_->GetMenuRunnerForTest();
SendKeyEvent(ui::VKEY_B);
MenuRunner* menu_runner2 = combobox_->GetMenuRunnerForTest();
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_RETURN);
WaitForMenuClosureAnimation();
EXPECT_EQ(u"bac", combobox_->GetText());
// Even though the items shown change, the menu runner shouldn't have been
// reset, otherwise there could be a flicker when the menu closes and reopens.
EXPECT_EQ(menu_runner1, menu_runner2);
}
TEST_F(EditableComboboxTest, RefocusingReopensMenuBasedOnLatestContent) {
std::vector<std::u16string> items = {u"abc", u"abd", u"bac", u"bad", u"bac2"};
InitEditableCombobox(items, /*filter_on_edit=*/true);
combobox_->GetTextfieldForTest()->RequestFocus();
SendKeyEvent(ui::VKEY_B);
ASSERT_EQ(3, combobox_->GetItemCountForTest());
SendKeyEvent(ui::VKEY_DOWN);
SendKeyEvent(ui::VKEY_RETURN);
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
EXPECT_EQ(u"bac", combobox_->GetText());
// Blur then focus to make the menu reopen. It should only show completions of
// "bac", the selected item, instead of showing completions of "b", what we
// had typed.
dummy_focusable_view_->RequestFocus();
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
ASSERT_EQ(2, combobox_->GetItemCountForTest());
}
TEST_F(EditableComboboxTest, GetItemsWithoutFiltering) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/false, /*show_on_empty=*/true);
combobox_->SetText(u"z");
ASSERT_EQ(2, combobox_->GetItemCountForTest());
ASSERT_EQ(u"item0", combobox_->GetItemForTest(0));
ASSERT_EQ(u"item1", combobox_->GetItemForTest(1));
}
TEST_F(EditableComboboxTest, FilteringEffectOnGetItems) {
std::vector<std::u16string> items = {u"abc", u"abd", u"bac", u"bad"};
InitEditableCombobox(items, /*filter_on_edit=*/true, /*show_on_empty=*/true);
ASSERT_EQ(4, combobox_->GetItemCountForTest());
ASSERT_EQ(u"abc", combobox_->GetItemForTest(0));
ASSERT_EQ(u"abd", combobox_->GetItemForTest(1));
ASSERT_EQ(u"bac", combobox_->GetItemForTest(2));
ASSERT_EQ(u"bad", combobox_->GetItemForTest(3));
combobox_->SetText(u"b");
ASSERT_EQ(2, combobox_->GetItemCountForTest());
ASSERT_EQ(u"bac", combobox_->GetItemForTest(0));
ASSERT_EQ(u"bad", combobox_->GetItemForTest(1));
combobox_->SetText(u"bc");
ASSERT_EQ(0, combobox_->GetItemCountForTest());
combobox_->SetText(std::u16string());
ASSERT_EQ(4, combobox_->GetItemCountForTest());
ASSERT_EQ(u"abc", combobox_->GetItemForTest(0));
ASSERT_EQ(u"abd", combobox_->GetItemForTest(1));
ASSERT_EQ(u"bac", combobox_->GetItemForTest(2));
ASSERT_EQ(u"bad", combobox_->GetItemForTest(3));
}
TEST_F(EditableComboboxTest, FilteringWithMismatchedCase) {
std::vector<std::u16string> items = {u"AbCd", u"aBcD", u"xyz"};
InitEditableCombobox(items, /*filter_on_edit=*/true, /*show_on_empty=*/true);
ASSERT_EQ(3, combobox_->GetItemCountForTest());
ASSERT_EQ(u"AbCd", combobox_->GetItemForTest(0));
ASSERT_EQ(u"aBcD", combobox_->GetItemForTest(1));
ASSERT_EQ(u"xyz", combobox_->GetItemForTest(2));
combobox_->SetText(u"abcd");
ASSERT_EQ(2, combobox_->GetItemCountForTest());
ASSERT_EQ(u"AbCd", combobox_->GetItemForTest(0));
ASSERT_EQ(u"aBcD", combobox_->GetItemForTest(1));
combobox_->SetText(u"ABCD");
ASSERT_EQ(2, combobox_->GetItemCountForTest());
ASSERT_EQ(u"AbCd", combobox_->GetItemForTest(0));
ASSERT_EQ(u"aBcD", combobox_->GetItemForTest(1));
}
TEST_F(EditableComboboxTest, DontShowOnEmpty) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/false,
/*show_on_empty=*/false);
ASSERT_EQ(0, combobox_->GetItemCountForTest());
combobox_->SetText(u"a");
ASSERT_EQ(2, combobox_->GetItemCountForTest());
ASSERT_EQ(u"item0", combobox_->GetItemForTest(0));
ASSERT_EQ(u"item1", combobox_->GetItemForTest(1));
}
TEST_F(EditableComboboxTest, NoFilteringNotifiesCallback) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/false, /*show_on_empty=*/true);
ASSERT_EQ(0, change_count());
combobox_->SetText(u"a");
ASSERT_EQ(1, change_count());
combobox_->SetText(u"ab");
ASSERT_EQ(2, change_count());
}
TEST_F(EditableComboboxTest, FilteringNotifiesCallback) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/true, /*show_on_empty=*/true);
ASSERT_EQ(0, change_count());
combobox_->SetText(u"i");
ASSERT_EQ(1, change_count());
combobox_->SetText(u"ix");
ASSERT_EQ(2, change_count());
combobox_->SetText(u"ixy");
ASSERT_EQ(3, change_count());
}
TEST_F(EditableComboboxTest, PasswordCanBeHiddenAndRevealed) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/false, /*show_on_empty=*/true,
EditableCombobox::Type::kPassword);
ASSERT_EQ(2, combobox_->GetItemCountForTest());
ASSERT_EQ(std::u16string(5, gfx::RenderText::kPasswordReplacementChar),
combobox_->GetItemForTest(0));
ASSERT_EQ(std::u16string(5, gfx::RenderText::kPasswordReplacementChar),
combobox_->GetItemForTest(1));
combobox_->RevealPasswords(/*revealed=*/true);
ASSERT_EQ(u"item0", combobox_->GetItemForTest(0));
ASSERT_EQ(u"item1", combobox_->GetItemForTest(1));
combobox_->RevealPasswords(/*revealed=*/false);
ASSERT_EQ(std::u16string(5, gfx::RenderText::kPasswordReplacementChar),
combobox_->GetItemForTest(0));
ASSERT_EQ(std::u16string(5, gfx::RenderText::kPasswordReplacementChar),
combobox_->GetItemForTest(1));
}
TEST_F(EditableComboboxTest, ArrowButtonOpensAndClosesMenu) {
InitEditableCombobox();
dummy_focusable_view_->RequestFocus();
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
ClickArrow();
EXPECT_TRUE(IsMenuOpen());
ClickArrow();
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
}
TEST_F(EditableComboboxTest, ShowContextMenuOnMouseRelease) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/false,
/*show_on_empty=*/true);
EXPECT_FALSE(IsMenuOpen());
TestContextMenuController context_menu_controller;
combobox_->GetTextfieldForTest()->set_context_menu_controller(
&context_menu_controller);
const gfx::Point textfield_point(combobox_->x() + 1, combobox_->y() + 1);
ui::MouseEvent click_mouse_event(ui::ET_MOUSE_PRESSED, textfield_point,
textfield_point, ui::EventTimeForNow(),
ui::EF_RIGHT_MOUSE_BUTTON,
ui::EF_RIGHT_MOUSE_BUTTON);
widget_->OnMouseEvent(&click_mouse_event);
EXPECT_FALSE(IsMenuOpen());
ui::MouseEvent release_mouse_event(ui::ET_MOUSE_RELEASED, textfield_point,
textfield_point, ui::EventTimeForNow(),
ui::EF_RIGHT_MOUSE_BUTTON,
ui::EF_RIGHT_MOUSE_BUTTON);
widget_->OnMouseEvent(&release_mouse_event);
// The context menu should appear, not the combobox dropdown.
EXPECT_FALSE(IsMenuOpen());
EXPECT_TRUE(context_menu_controller.opened_menu());
}
TEST_F(EditableComboboxTest, DragToSelectDoesntOpenTheMenu) {
std::vector<std::u16string> items = {u"item0", u"item1"};
InitEditableCombobox(items, /*filter_on_edit=*/false,
/*show_on_empty=*/true);
combobox_->SetText(u"abc");
dummy_focusable_view_->RequestFocus();
WaitForMenuClosureAnimation();
EXPECT_FALSE(IsMenuOpen());
const int kCursorXStart = 0;
const int kCursorXEnd = combobox_->x() + combobox_->width();
const int kCursorY = combobox_->y() + 1;
gfx::Point start_point(kCursorXStart, kCursorY);
gfx::Point end_point(kCursorXEnd, kCursorY);
PerformMouseEvent(widget_, start_point, ui::ET_MOUSE_PRESSED);
EXPECT_TRUE(combobox_->GetTextfieldForTest()->GetSelectedText().empty());
DragMouseTo(end_point);
ASSERT_EQ(u"abc", combobox_->GetTextfieldForTest()->GetSelectedText());
EXPECT_FALSE(IsMenuOpen());
PerformMouseEvent(widget_, end_point, ui::ET_MOUSE_RELEASED);
ASSERT_EQ(u"abc", combobox_->GetTextfieldForTest()->GetSelectedText());
EXPECT_FALSE(IsMenuOpen());
}
TEST_F(EditableComboboxTest, NoCrashWithoutWidget) {
std::vector<std::u16string> items = {u"item0", u"item1"};
auto combobox = std::make_unique<EditableCombobox>(
std::make_unique<ui::SimpleComboboxModel>(items),
/*filter_on_edit=*/false,
/*show_on_empty=*/true, EditableCombobox::Type::kPassword);
// Showing the dropdown should silently fail.
combobox->RevealPasswords(true);
}
using EditableComboboxDefaultTest = ViewsTestBase;
class ConfigurableComboboxModel final : public ui::ComboboxModel {
public:
explicit ConfigurableComboboxModel(bool* destroyed = nullptr)
: destroyed_(destroyed) {
if (destroyed_)
*destroyed_ = false;
}
ConfigurableComboboxModel(ConfigurableComboboxModel&) = delete;
ConfigurableComboboxModel& operator=(const ConfigurableComboboxModel&) =
delete;
~ConfigurableComboboxModel() override {
if (destroyed_)
*destroyed_ = true;
}
// ui::ComboboxModel:
int GetItemCount() const override { return item_count_; }
std::u16string GetItemAt(int index) const override {
DCHECK_LT(index, item_count_);
return base::NumberToString16(index);
}
void SetItemCount(int item_count) { item_count_ = item_count; }
private:
bool* const destroyed_;
int item_count_ = 0;
};
} // namespace
TEST_F(EditableComboboxDefaultTest, Default) {
auto combobox = std::make_unique<EditableCombobox>();
EXPECT_EQ(0, combobox->GetItemCountForTest());
}
TEST_F(EditableComboboxDefaultTest, SetModel) {
std::unique_ptr<ConfigurableComboboxModel> model =
std::make_unique<ConfigurableComboboxModel>();
model->SetItemCount(42);
auto combobox = std::make_unique<EditableCombobox>();
combobox->SetModel(std::move(model));
EXPECT_EQ(42, combobox->GetItemCountForTest());
}
TEST_F(EditableComboboxDefaultTest, SetModelOverwrite) {
bool destroyed_first = false;
bool destroyed_second = false;
{
auto combobox = std::make_unique<EditableCombobox>();
combobox->SetModel(
std::make_unique<ConfigurableComboboxModel>(&destroyed_first));
ASSERT_FALSE(destroyed_first);
combobox->SetModel(
std::make_unique<ConfigurableComboboxModel>(&destroyed_second));
EXPECT_TRUE(destroyed_first);
ASSERT_FALSE(destroyed_second);
}
EXPECT_TRUE(destroyed_second);
}
} // namespace views