| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/controls/combobox/combobox.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/models/combobox_model.h" |
| #include "ui/base/models/combobox_model_observer.h" |
| #include "ui/base/models/menu_model.h" |
| #include "ui/base/models/simple_combobox_model.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/dom/dom_code.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/text_utils.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/style/platform_style.h" |
| #include "ui/views/style/typography_provider.h" |
| #include "ui/views/test/ax_event_counter.h" |
| #include "ui/views/test/combobox_test_api.h" |
| #include "ui/views/test/view_metadata_test_utils.h" |
| #include "ui/views/test/views_test_base.h" |
| #include "ui/views/test/views_test_utils.h" |
| #include "ui/views/widget/unique_widget_ptr.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_utils.h" |
| |
| using base::ASCIIToUTF16; |
| |
| namespace views { |
| |
| using test::ComboboxTestApi; |
| |
| namespace { |
| |
| using TestCombobox = Combobox; |
| |
| // A concrete class is needed to test the combobox. |
| class TestComboboxModel : public ui::ComboboxModel { |
| public: |
| TestComboboxModel() = default; |
| |
| TestComboboxModel(const TestComboboxModel&) = delete; |
| TestComboboxModel& operator=(const TestComboboxModel&) = delete; |
| |
| ~TestComboboxModel() override = default; |
| |
| static constexpr size_t kItemCount = 10; |
| |
| // ui::ComboboxModel: |
| size_t GetItemCount() const override { return item_count_; } |
| std::u16string GetItemAt(size_t index) const override { |
| DCHECK(!IsItemSeparatorAt(index)); |
| return ASCIIToUTF16(index % 2 == 0 ? "PEANUT BUTTER" : "JELLY"); |
| } |
| bool IsItemSeparatorAt(size_t index) const override { |
| return separators_.find(index) != separators_.end(); |
| } |
| |
| std::optional<size_t> GetDefaultIndex() const override { |
| // Return the first index that is not a separator. |
| for (size_t index = 0; index < kItemCount; ++index) { |
| if (separators_.find(index) == separators_.end()) { |
| return index; |
| } |
| } |
| NOTREACHED(); |
| } |
| |
| ComboboxModel::ItemCheckmarkConfig GetCheckmarkConfig() const override { |
| return menu_checkmark_override_; |
| } |
| |
| void SetSeparators(const std::set<size_t>& separators) { |
| separators_ = separators; |
| OnModelChanged(); |
| } |
| |
| void set_item_count(size_t item_count) { |
| item_count_ = item_count; |
| OnModelChanged(); |
| } |
| |
| void SetMenuCheckmarkOverride(ComboboxModel::ItemCheckmarkConfig config) { |
| menu_checkmark_override_ = config; |
| } |
| |
| private: |
| void OnModelChanged() { |
| for (auto& observer : observers()) { |
| observer.OnComboboxModelChanged(this); |
| } |
| } |
| |
| std::set<size_t> separators_; |
| size_t item_count_ = kItemCount; |
| ComboboxModel::ItemCheckmarkConfig menu_checkmark_override_ = |
| ComboboxModel::ItemCheckmarkConfig::kDefault; |
| }; |
| |
| // A combobox model which refers to a vector. |
| class VectorComboboxModel : public ui::ComboboxModel { |
| public: |
| explicit VectorComboboxModel(std::vector<std::string>* values) |
| : values_(values) {} |
| |
| VectorComboboxModel(const VectorComboboxModel&) = delete; |
| VectorComboboxModel& operator=(const VectorComboboxModel&) = delete; |
| |
| ~VectorComboboxModel() override = default; |
| |
| void set_default_index(size_t default_index) { |
| default_index_ = default_index; |
| } |
| |
| // ui::ComboboxModel: |
| size_t GetItemCount() const override { return values_->size(); } |
| std::u16string GetItemAt(size_t index) const override { |
| return ASCIIToUTF16((*values_)[index]); |
| } |
| bool IsItemSeparatorAt(size_t index) const override { return false; } |
| std::optional<size_t> GetDefaultIndex() const override { |
| return default_index_; |
| } |
| |
| void ValuesChanged() { |
| for (auto& observer : observers()) { |
| observer.OnComboboxModelChanged(this); |
| } |
| } |
| |
| private: |
| std::optional<size_t> default_index_ = std::nullopt; |
| const raw_ptr<std::vector<std::string>> values_; |
| }; |
| |
| class EvilListener { |
| public: |
| EvilListener() { |
| combobox()->SetCallback(base::BindRepeating(&EvilListener::OnPerformAction, |
| base::Unretained(this))); |
| } |
| |
| EvilListener(const EvilListener&) = delete; |
| EvilListener& operator=(const EvilListener&) = delete; |
| |
| ~EvilListener() = default; |
| |
| TestCombobox* combobox() { return combobox_.get(); } |
| |
| private: |
| void OnPerformAction() { combobox_.reset(); } |
| |
| TestComboboxModel model_; |
| std::unique_ptr<TestCombobox> combobox_ = |
| std::make_unique<TestCombobox>(&model_); |
| }; |
| |
| class TestComboboxListener { |
| public: |
| explicit TestComboboxListener(Combobox* combobox) : combobox_(combobox) {} |
| |
| TestComboboxListener(const TestComboboxListener&) = delete; |
| TestComboboxListener& operator=(const TestComboboxListener&) = delete; |
| |
| ~TestComboboxListener() = default; |
| |
| void OnPerformAction() { |
| perform_action_index_ = combobox_->GetSelectedIndex(); |
| actions_performed_++; |
| } |
| |
| std::optional<size_t> perform_action_index() const { |
| return perform_action_index_; |
| } |
| |
| bool on_perform_action_called() const { return actions_performed_ > 0; } |
| |
| int actions_performed() const { return actions_performed_; } |
| |
| private: |
| raw_ptr<Combobox> combobox_; |
| std::optional<size_t> perform_action_index_ = std::nullopt; |
| int actions_performed_ = 0; |
| }; |
| |
| } // namespace |
| |
| class ComboboxTest : public ViewsTestBase { |
| public: |
| ComboboxTest() = default; |
| |
| ComboboxTest(const ComboboxTest&) = delete; |
| ComboboxTest& operator=(const ComboboxTest&) = delete; |
| |
| void TearDown() override { |
| widget_.reset(); |
| ViewsTestBase::TearDown(); |
| } |
| |
| void InitCombobox(const std::set<size_t>* separators) { |
| model_ = std::make_unique<TestComboboxModel>(); |
| |
| if (separators) { |
| model_->SetSeparators(*separators); |
| } |
| |
| ASSERT_FALSE(combobox()); |
| auto box = std::make_unique<TestCombobox>(model_.get()); |
| ComboboxTestApi(box.get()).InstallTestMenuRunner(&menu_show_count_); |
| box->SetID(1); |
| |
| widget_ = std::make_unique<Widget>(); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.bounds = gfx::Rect(200, 200, 200, 200); |
| widget_->Init(std::move(params)); |
| View* container = widget_->SetContentsView(std::make_unique<View>()); |
| container->AddChildView(std::move(box)); |
| widget_->Show(); |
| |
| combobox()->RequestFocus(); |
| combobox()->SizeToPreferredSize(); |
| |
| event_generator_ = std::make_unique<ui::test::EventGenerator>( |
| GetRootWindow(widget_.get())); |
| event_generator_->set_target(ui::test::EventGenerator::Target::WINDOW); |
| } |
| |
| protected: |
| void PressKey(ui::KeyboardCode key_code, ui::EventFlags flags = ui::EF_NONE) { |
| event_generator_->PressKey(key_code, flags); |
| } |
| |
| void ReleaseKey(ui::KeyboardCode key_code, |
| ui::EventFlags flags = ui::EF_NONE) { |
| event_generator_->ReleaseKey(key_code, flags); |
| } |
| |
| View* GetFocusedView() { |
| return widget_->GetFocusManager()->GetFocusedView(); |
| } |
| |
| void PerformMousePress(const gfx::Point& point) { |
| ui::MouseEvent pressed_event = ui::MouseEvent( |
| ui::EventType::kMousePressed, point, point, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| widget_->OnMouseEvent(&pressed_event); |
| } |
| |
| void PerformMouseRelease(const gfx::Point& point) { |
| ui::MouseEvent released_event = ui::MouseEvent( |
| ui::EventType::kMouseReleased, point, point, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| widget_->OnMouseEvent(&released_event); |
| } |
| |
| void PerformClick(const gfx::Point& point) { |
| PerformMousePress(point); |
| PerformMouseRelease(point); |
| } |
| |
| TestCombobox* combobox() { |
| return widget_ ? static_cast<TestCombobox*>( |
| widget_->GetContentsView()->GetViewByID(1)) |
| : nullptr; |
| } |
| |
| // We need widget to populate wrapper class. |
| UniqueWidgetPtr widget_; |
| |
| // Combobox does not take ownership of the model, hence it needs to be scoped. |
| std::unique_ptr<TestComboboxModel> model_; |
| |
| // The current menu show count. |
| int menu_show_count_ = 0; |
| |
| std::unique_ptr<ui::test::EventGenerator> event_generator_; |
| }; |
| |
| #if BUILDFLAG(IS_MAC) |
| // Tests whether the various Mac specific keyboard shortcuts invoke the dropdown |
| // menu or not. |
| TEST_F(ComboboxTest, KeyTestMac) { |
| InitCombobox(nullptr); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(1, menu_show_count_); |
| |
| PressKey(ui::VKEY_HOME); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(2, menu_show_count_); |
| |
| PressKey(ui::VKEY_UP, ui::EF_COMMAND_DOWN); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(3, menu_show_count_); |
| |
| PressKey(ui::VKEY_DOWN, ui::EF_COMMAND_DOWN); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(4, menu_show_count_); |
| |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(5, menu_show_count_); |
| |
| PressKey(ui::VKEY_RIGHT); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(5, menu_show_count_); |
| |
| PressKey(ui::VKEY_LEFT); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(5, menu_show_count_); |
| |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(6, menu_show_count_); |
| |
| PressKey(ui::VKEY_PRIOR); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(6, menu_show_count_); |
| |
| PressKey(ui::VKEY_NEXT); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_EQ(6, menu_show_count_); |
| } |
| #endif |
| |
| // Iterate through all the metadata and test each property. |
| TEST_F(ComboboxTest, MetadataTest) { |
| InitCombobox(nullptr); |
| test::TestViewMetadata(combobox()); |
| } |
| |
| // Check that if a combobox is disabled before it has a native wrapper, then the |
| // native wrapper inherits the disabled state when it gets created. |
| TEST_F(ComboboxTest, DisabilityTest) { |
| model_ = std::make_unique<TestComboboxModel>(); |
| |
| ASSERT_FALSE(combobox()); |
| auto combobox = std::make_unique<TestCombobox>(model_.get()); |
| combobox->SetEnabled(false); |
| |
| widget_ = std::make_unique<Widget>(); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.bounds = gfx::Rect(100, 100, 100, 100); |
| widget_->Init(std::move(params)); |
| View* container = widget_->SetContentsView(std::make_unique<View>()); |
| Combobox* combobox_pointer = container->AddChildView(std::move(combobox)); |
| EXPECT_FALSE(combobox_pointer->GetEnabled()); |
| } |
| |
| // Ensure the border on the combobox is set correctly when Enabled state |
| // changes. |
| TEST_F(ComboboxTest, DisabledBorderTest) { |
| InitCombobox(nullptr); |
| ASSERT_TRUE(combobox()->GetEnabled()); |
| ASSERT_NE(combobox()->GetBorder(), nullptr); |
| combobox()->SetEnabled(false); |
| ASSERT_FALSE(combobox()->GetEnabled()); |
| ASSERT_EQ(combobox()->GetBorder(), nullptr); |
| combobox()->SetEnabled(true); |
| ASSERT_TRUE(combobox()->GetEnabled()); |
| ASSERT_NE(combobox()->GetBorder(), nullptr); |
| } |
| |
| // On Mac, key events can't change the currently selected index directly for a |
| // combobox. |
| #if !BUILDFLAG(IS_MAC) |
| |
| // Tests the behavior of various keyboard shortcuts on the currently selected |
| // index. |
| TEST_F(ComboboxTest, KeyTest) { |
| InitCombobox(nullptr); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(model_->GetItemCount() - 1, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_HOME); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_DOWN); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(2u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_RIGHT); |
| EXPECT_EQ(2u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_LEFT); |
| EXPECT_EQ(2u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_PRIOR); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_NEXT); |
| EXPECT_EQ(model_->GetItemCount() - 1, combobox()->GetSelectedIndex()); |
| } |
| |
| // Verifies that we don't select a separator line in combobox when navigating |
| // through keyboard. |
| TEST_F(ComboboxTest, SkipSeparatorSimple) { |
| std::set<size_t> separators; |
| separators.insert(2); |
| InitCombobox(&separators); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_HOME); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_PRIOR); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(9u, combobox()->GetSelectedIndex()); |
| } |
| |
| // Verifies that we never select the separator that is in the beginning of the |
| // combobox list when navigating through keyboard. |
| TEST_F(ComboboxTest, SkipSeparatorBeginning) { |
| std::set<size_t> separators; |
| separators.insert(0); |
| InitCombobox(&separators); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(2u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(2u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_HOME); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_PRIOR); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(9u, combobox()->GetSelectedIndex()); |
| } |
| |
| // Verifies that we never select the separator that is in the end of the |
| // combobox list when navigating through keyboard. |
| TEST_F(ComboboxTest, SkipSeparatorEnd) { |
| std::set<size_t> separators; |
| separators.insert(TestComboboxModel::kItemCount - 1); |
| InitCombobox(&separators); |
| combobox()->SetSelectedIndex(8); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(8u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(7u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(8u, combobox()->GetSelectedIndex()); |
| } |
| |
| // Verifies that we never select any of the adjacent separators (multiple |
| // consecutive) that appear in the beginning of the combobox list when |
| // navigating through keyboard. |
| TEST_F(ComboboxTest, SkipMultipleSeparatorsAtBeginning) { |
| std::set<size_t> separators; |
| separators.insert(0); |
| separators.insert(1); |
| separators.insert(2); |
| InitCombobox(&separators); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(4u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_NEXT); |
| EXPECT_EQ(9u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_HOME); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(9u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_PRIOR); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| } |
| |
| // Verifies that we never select any of the adjacent separators (multiple |
| // consecutive) that appear in the middle of the combobox list when navigating |
| // through keyboard. |
| TEST_F(ComboboxTest, SkipMultipleAdjacentSeparatorsAtMiddle) { |
| std::set<size_t> separators; |
| separators.insert(4); |
| separators.insert(5); |
| separators.insert(6); |
| InitCombobox(&separators); |
| combobox()->SetSelectedIndex(3); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(7u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(3u, combobox()->GetSelectedIndex()); |
| } |
| |
| // Verifies that we never select any of the adjacent separators (multiple |
| // consecutive) that appear in the end of the combobox list when navigating |
| // through keyboard. |
| TEST_F(ComboboxTest, SkipMultipleSeparatorsAtEnd) { |
| std::set<size_t> separators; |
| separators.insert(7); |
| separators.insert(8); |
| separators.insert(9); |
| InitCombobox(&separators); |
| combobox()->SetSelectedIndex(6); |
| PressKey(ui::VKEY_DOWN); |
| EXPECT_EQ(6u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_UP); |
| EXPECT_EQ(5u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_HOME); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_NEXT); |
| EXPECT_EQ(6u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_PRIOR); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| PressKey(ui::VKEY_END); |
| EXPECT_EQ(6u, combobox()->GetSelectedIndex()); |
| } |
| #endif // !BUILDFLAG(IS_MAC) |
| |
| TEST_F(ComboboxTest, GetTextForRowTest) { |
| std::set<size_t> separators; |
| separators.insert(0); |
| separators.insert(1); |
| separators.insert(9); |
| InitCombobox(&separators); |
| for (size_t i = 0; i < combobox()->GetRowCount(); ++i) { |
| if (separators.count(i) != 0) { |
| EXPECT_TRUE(combobox()->GetTextForRow(i).empty()) << i; |
| } else { |
| EXPECT_EQ(ASCIIToUTF16(i % 2 == 0 ? "PEANUT BUTTER" : "JELLY"), |
| combobox()->GetTextForRow(i)) |
| << i; |
| } |
| } |
| } |
| |
| // Verifies selecting the first matching value (and returning whether found). |
| TEST_F(ComboboxTest, SelectValue) { |
| InitCombobox(nullptr); |
| ASSERT_EQ(model_->GetDefaultIndex(), combobox()->GetSelectedIndex()); |
| EXPECT_TRUE(combobox()->SelectValue(u"PEANUT BUTTER")); |
| EXPECT_EQ(0u, combobox()->GetSelectedIndex()); |
| EXPECT_TRUE(combobox()->SelectValue(u"JELLY")); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| EXPECT_FALSE(combobox()->SelectValue(u"BANANAS")); |
| EXPECT_EQ(1u, combobox()->GetSelectedIndex()); |
| } |
| |
| TEST_F(ComboboxTest, ListenerHandlesDelete) { |
| auto evil_listener = std::make_unique<EvilListener>(); |
| ASSERT_TRUE(evil_listener->combobox()); |
| ASSERT_NO_FATAL_FAILURE({ |
| ui::MenuModel* model = |
| ComboboxTestApi(evil_listener->combobox()).menu_model(); |
| model->ActivatedAt(2); |
| }); |
| EXPECT_FALSE(evil_listener->combobox()); |
| } |
| |
| TEST_F(ComboboxTest, Click) { |
| InitCombobox(nullptr); |
| |
| TestComboboxListener listener(combobox()); |
| combobox()->SetCallback(base::BindRepeating( |
| &TestComboboxListener::OnPerformAction, base::Unretained(&listener))); |
| views::test::RunScheduledLayout(combobox()); |
| |
| // Click the left side. The menu is shown. |
| EXPECT_EQ(0, menu_show_count_); |
| PerformClick(gfx::Point(combobox()->x() + 1, |
| combobox()->y() + combobox()->height() / 2)); |
| EXPECT_FALSE(listener.on_perform_action_called()); |
| EXPECT_EQ(1, menu_show_count_); |
| } |
| |
| TEST_F(ComboboxTest, ClickButDisabled) { |
| InitCombobox(nullptr); |
| |
| TestComboboxListener listener(combobox()); |
| combobox()->SetCallback(base::BindRepeating( |
| &TestComboboxListener::OnPerformAction, base::Unretained(&listener))); |
| |
| views::test::RunScheduledLayout(combobox()); |
| combobox()->SetEnabled(false); |
| |
| // Click the left side, but nothing happens since the combobox is disabled. |
| PerformClick(gfx::Point(combobox()->x() + 1, |
| combobox()->y() + combobox()->height() / 2)); |
| EXPECT_FALSE(listener.on_perform_action_called()); |
| EXPECT_EQ(0, menu_show_count_); |
| } |
| |
| TEST_F(ComboboxTest, NotifyOnClickWithReturnKey) { |
| InitCombobox(nullptr); |
| |
| TestComboboxListener listener(combobox()); |
| combobox()->SetCallback(base::BindRepeating( |
| &TestComboboxListener::OnPerformAction, base::Unretained(&listener))); |
| |
| // The click event is ignored. Instead the menu is shown. |
| PressKey(ui::VKEY_RETURN); |
| EXPECT_EQ(PlatformStyle::kReturnClicksFocusedControl ? 1 : 0, |
| menu_show_count_); |
| EXPECT_FALSE(listener.on_perform_action_called()); |
| } |
| |
| TEST_F(ComboboxTest, NotifyOnClickWithSpaceKey) { |
| InitCombobox(nullptr); |
| |
| TestComboboxListener listener(combobox()); |
| combobox()->SetCallback(base::BindRepeating( |
| &TestComboboxListener::OnPerformAction, base::Unretained(&listener))); |
| |
| // The click event is ignored. Instead the menu is shwon. |
| PressKey(ui::VKEY_SPACE); |
| EXPECT_EQ(1, menu_show_count_); |
| EXPECT_FALSE(listener.on_perform_action_called()); |
| |
| ReleaseKey(ui::VKEY_SPACE); |
| EXPECT_EQ(1, menu_show_count_); |
| EXPECT_FALSE(listener.on_perform_action_called()); |
| } |
| |
| // Test that accessibility action events show the combobox dropdown. |
| TEST_F(ComboboxTest, ShowViaAccessibleAction) { |
| InitCombobox(nullptr); |
| |
| ui::AXActionData data; |
| data.action = ax::mojom::Action::kDoDefault; |
| |
| EXPECT_EQ(0, menu_show_count_); |
| combobox()->HandleAccessibleAction(data); |
| EXPECT_EQ(1, menu_show_count_); |
| |
| // ax::mojom::Action::kShowContextMenu is specifically for a context menu |
| // (e.g. right- click). Combobox should ignore it. |
| data.action = ax::mojom::Action::kShowContextMenu; |
| combobox()->HandleAccessibleAction(data); |
| EXPECT_EQ(1, menu_show_count_); // No change. |
| |
| data.action = ax::mojom::Action::kBlur; |
| combobox()->HandleAccessibleAction(data); |
| EXPECT_EQ(1, menu_show_count_); // No change. |
| |
| combobox()->SetEnabled(false); |
| combobox()->HandleAccessibleAction(data); |
| EXPECT_EQ(1, menu_show_count_); // No change. |
| |
| data.action = ax::mojom::Action::kShowContextMenu; |
| combobox()->HandleAccessibleAction(data); |
| EXPECT_EQ(1, menu_show_count_); // No change. |
| } |
| |
| TEST_F(ComboboxTest, ExpandedCollapsedAccessibleState) { |
| InitCombobox(nullptr); |
| |
| // Initially the combobox will be collapsed by default. |
| ui::AXNodeData node_data; |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_FALSE(node_data.HasState(ax::mojom::State::kExpanded)); |
| EXPECT_TRUE(node_data.HasState(ax::mojom::State::kCollapsed)); |
| |
| // Pressing space shows the menu, which sets the expanded state. |
| combobox()->OnKeyPressed( |
| ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_SPACE, ui::EF_NONE)); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_TRUE(node_data.HasState(ax::mojom::State::kExpanded)); |
| EXPECT_FALSE(node_data.HasState(ax::mojom::State::kCollapsed)); |
| |
| // Closing the menu with the test api sets the collapsed state. |
| ComboboxTestApi(combobox()).CloseMenu(); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_FALSE(node_data.HasState(ax::mojom::State::kExpanded)); |
| EXPECT_TRUE(node_data.HasState(ax::mojom::State::kCollapsed)); |
| |
| // Pressing space again reopens the menu and sets the expanded state. |
| combobox()->OnKeyPressed( |
| ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_SPACE, ui::EF_NONE)); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_TRUE(node_data.HasState(ax::mojom::State::kExpanded)); |
| EXPECT_FALSE(node_data.HasState(ax::mojom::State::kCollapsed)); |
| |
| // Changing the model closes the menu and sets the collapsed state. |
| model_->set_item_count(0); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_FALSE(node_data.HasState(ax::mojom::State::kExpanded)); |
| EXPECT_TRUE(node_data.HasState(ax::mojom::State::kCollapsed)); |
| } |
| |
| TEST_F(ComboboxTest, AccessibleDefaultActionVerb) { |
| InitCombobox(nullptr); |
| ui::AXNodeData node_data; |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(ax::mojom::DefaultActionVerb::kOpen, |
| node_data.GetDefaultActionVerb()); |
| |
| node_data = ui::AXNodeData(); |
| combobox()->SetEnabled(false); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_FALSE( |
| node_data.HasIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb)); |
| |
| node_data = ui::AXNodeData(); |
| combobox()->SetEnabled(true); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(ax::mojom::DefaultActionVerb::kOpen, |
| node_data.GetDefaultActionVerb()); |
| } |
| |
| TEST_F(ComboboxTest, SetSizePosInSetAccessibleProperties) { |
| InitCombobox(nullptr); |
| |
| // Test an empty model. |
| model_->set_item_count(0); |
| EXPECT_EQ(0u, combobox()->GetRowCount()); |
| EXPECT_EQ(0u, combobox()->GetSelectedRow()); |
| ui::AXNodeData node_data; |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(0, node_data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| EXPECT_EQ(0, node_data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| |
| // Update item count and selected index. |
| model_->set_item_count(5); |
| combobox()->SetSelectedIndex(4); |
| EXPECT_EQ(5u, combobox()->GetRowCount()); |
| EXPECT_EQ(4u, combobox()->GetSelectedRow()); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(5, node_data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| EXPECT_EQ(4, node_data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| |
| // Update item count. |
| model_->set_item_count(6); |
| EXPECT_EQ(6u, combobox()->GetRowCount()); |
| EXPECT_EQ(4u, combobox()->GetSelectedRow()); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(6, node_data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| EXPECT_EQ(4, node_data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| |
| // Update selected index. |
| combobox()->SetSelectedIndex(2); |
| EXPECT_EQ(6u, combobox()->GetRowCount()); |
| EXPECT_EQ(2u, combobox()->GetSelectedRow()); |
| node_data = ui::AXNodeData(); |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(6, node_data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| EXPECT_EQ(2, node_data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| } |
| |
| TEST_F(ComboboxTest, AccessibleValue) { |
| // Empty model kValue check |
| auto simple_model = std::make_unique<ui::SimpleComboboxModel>( |
| std::vector<ui::SimpleComboboxModel::Item>()); |
| auto combobox = std::make_unique<Combobox>(simple_model.get()); |
| |
| ui::AXNodeData node_data; |
| combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(0u, combobox->GetModel()->GetItemCount()); |
| EXPECT_EQ(std::nullopt, combobox->GetSelectedIndex()); |
| EXPECT_EQ("", |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue)); |
| |
| // Non-empty model. |
| simple_model->UpdateItemList({ui::SimpleComboboxModel::Item(u"Peanut Butter"), |
| ui::SimpleComboboxModel::Item(u"Yogurt")}); |
| node_data = ui::AXNodeData(); |
| combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(2u, combobox->GetModel()->GetItemCount()); |
| EXPECT_EQ(0u, combobox->GetSelectedIndex()); |
| EXPECT_EQ("Peanut Butter", |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue)); |
| |
| // set selected index to 1. |
| node_data = ui::AXNodeData(); |
| combobox->SetSelectedIndex(1); |
| EXPECT_EQ(1u, combobox->GetSelectedIndex()); |
| combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ("Yogurt", |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue)); |
| } |
| |
| TEST_F(ComboboxTest, NotifyOnClickWithMouse) { |
| InitCombobox(nullptr); |
| |
| TestComboboxListener listener(combobox()); |
| combobox()->SetCallback(base::BindRepeating( |
| &TestComboboxListener::OnPerformAction, base::Unretained(&listener))); |
| |
| views::test::RunScheduledLayout(combobox()); |
| |
| // Click the right side (arrow button). The menu is shown. |
| const gfx::Point right_point(combobox()->x() + combobox()->width() - 1, |
| combobox()->y() + combobox()->height() / 2); |
| |
| EXPECT_EQ(0, menu_show_count_); |
| |
| // Menu is shown on mouse down. |
| PerformMousePress(right_point); |
| EXPECT_EQ(1, menu_show_count_); |
| PerformMouseRelease(right_point); |
| EXPECT_EQ(1, menu_show_count_); |
| |
| // Click the left side (text button). The click event is notified. |
| const gfx::Point left_point(gfx::Point( |
| combobox()->x() + 1, combobox()->y() + combobox()->height() / 2)); |
| |
| PerformMousePress(left_point); |
| PerformMouseRelease(left_point); |
| |
| // Both the text and the arrow may toggle the menu. |
| EXPECT_EQ(2, menu_show_count_); |
| EXPECT_FALSE(listener.perform_action_index().has_value()); |
| } |
| |
| TEST_F(ComboboxTest, ConsumingPressKeyEvents) { |
| InitCombobox(nullptr); |
| |
| EXPECT_TRUE(combobox()->OnKeyPressed( |
| ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_SPACE, ui::EF_NONE))); |
| EXPECT_EQ(1, menu_show_count_); |
| |
| ui::KeyEvent return_press(ui::EventType::kKeyPressed, ui::VKEY_RETURN, |
| ui::EF_NONE); |
| if constexpr (PlatformStyle::kReturnClicksFocusedControl) { |
| EXPECT_TRUE(combobox()->OnKeyPressed(return_press)); |
| EXPECT_EQ(2, menu_show_count_); |
| } else { |
| EXPECT_FALSE(combobox()->OnKeyPressed(return_press)); |
| EXPECT_EQ(1, menu_show_count_); |
| } |
| } |
| |
| // Test that ensures that the combobox is resized correctly when selecting |
| // between indices of different label lengths. |
| TEST_F(ComboboxTest, ContentSizeUpdateOnSetSelectedIndex) { |
| const gfx::FontList& font_list = |
| TypographyProvider::Get().GetFont(Combobox::kContext, Combobox::kStyle); |
| InitCombobox(nullptr); |
| combobox()->SetSizeToLargestLabel(false); |
| ComboboxTestApi(combobox()).PerformActionAt(1); |
| EXPECT_EQ(gfx::GetStringWidth(model_->GetItemAt(1), font_list), |
| ComboboxTestApi(combobox()).content_size().width()); |
| combobox()->SetSelectedIndex(1); |
| EXPECT_EQ(gfx::GetStringWidth(model_->GetItemAt(1), font_list), |
| ComboboxTestApi(combobox()).content_size().width()); |
| |
| // Avoid selected_index_ == index optimization and start with index 1 selected |
| // to test resizing from a an index with a shorter label to an index with a |
| // longer label. |
| combobox()->SetSelectedIndex(0); |
| combobox()->SetSelectedIndex(1); |
| |
| ComboboxTestApi(combobox()).PerformActionAt(0); |
| EXPECT_EQ(gfx::GetStringWidth(model_->GetItemAt(0), font_list), |
| ComboboxTestApi(combobox()).content_size().width()); |
| combobox()->SetSelectedIndex(0); |
| EXPECT_EQ(gfx::GetStringWidth(model_->GetItemAt(0), font_list), |
| ComboboxTestApi(combobox()).content_size().width()); |
| } |
| |
| TEST_F(ComboboxTest, ContentWidth) { |
| std::vector<std::string> values; |
| VectorComboboxModel model(&values); |
| TestCombobox combobox(&model); |
| ComboboxTestApi test_api(&combobox); |
| |
| std::string long_item = "this is the long item"; |
| std::string short_item = "s"; |
| |
| values.resize(1); |
| values[0] = long_item; |
| model.ValuesChanged(); |
| |
| const int long_item_width = test_api.content_size().width(); |
| |
| values[0] = short_item; |
| model.ValuesChanged(); |
| |
| const int short_item_width = test_api.content_size().width(); |
| |
| values.resize(2); |
| values[0] = short_item; |
| values[1] = long_item; |
| model.ValuesChanged(); |
| |
| // The width will fit with the longest item. |
| EXPECT_EQ(long_item_width, test_api.content_size().width()); |
| EXPECT_LT(short_item_width, test_api.content_size().width()); |
| } |
| |
| // Test that model updates preserve the selected index, so long as it is in |
| // range. |
| TEST_F(ComboboxTest, ModelChanged) { |
| InitCombobox(nullptr); |
| |
| EXPECT_EQ(0u, combobox()->GetSelectedRow()); |
| EXPECT_EQ(10u, combobox()->GetRowCount()); |
| |
| combobox()->SetSelectedIndex(4); |
| EXPECT_EQ(4u, combobox()->GetSelectedRow()); |
| |
| model_->set_item_count(5); |
| EXPECT_EQ(5u, combobox()->GetRowCount()); |
| EXPECT_EQ(4u, combobox()->GetSelectedRow()); // Unchanged. |
| |
| model_->set_item_count(4); |
| EXPECT_EQ(4u, combobox()->GetRowCount()); |
| EXPECT_EQ(0u, combobox()->GetSelectedRow()); // Resets. |
| |
| // Restore a non-zero selection. |
| combobox()->SetSelectedIndex(2); |
| EXPECT_EQ(2u, combobox()->GetSelectedRow()); |
| |
| // Make the selected index a separator. |
| std::set<size_t> separators; |
| separators.insert(2); |
| model_->SetSeparators(separators); |
| EXPECT_EQ(4u, combobox()->GetRowCount()); |
| EXPECT_EQ(0u, combobox()->GetSelectedRow()); // Resets. |
| |
| // Restore a non-zero selection. |
| combobox()->SetSelectedIndex(1); |
| EXPECT_EQ(1u, combobox()->GetSelectedRow()); |
| |
| // Test an empty model. |
| model_->set_item_count(0); |
| EXPECT_EQ(0u, combobox()->GetRowCount()); |
| EXPECT_EQ(0u, combobox()->GetSelectedRow()); // Resets. |
| } |
| |
| TEST_F(ComboboxTest, TypingPrefixNotifiesListener) { |
| InitCombobox(nullptr); |
| |
| TestComboboxListener listener(combobox()); |
| combobox()->SetCallback(base::BindRepeating( |
| &TestComboboxListener::OnPerformAction, base::Unretained(&listener))); |
| ui::TextInputClient* input_client = |
| widget_->GetInputMethod()->GetTextInputClient(); |
| |
| // Type the first character of the second menu item ("JELLY"). |
| ui::KeyEvent key_event(ui::EventType::kKeyPressed, ui::VKEY_J, |
| ui::DomCode::US_J, 0, ui::DomKey::FromCharacter('J'), |
| ui::EventTimeForNow()); |
| |
| input_client->InsertChar(key_event); |
| EXPECT_EQ(1, listener.actions_performed()); |
| EXPECT_EQ(1u, listener.perform_action_index()); |
| |
| // Type the second character of "JELLY", item shouldn't change and |
| // OnPerformAction() shouldn't be re-called. |
| key_event = |
| ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_E, ui::DomCode::US_E, 0, |
| ui::DomKey::FromCharacter('E'), ui::EventTimeForNow()); |
| input_client->InsertChar(key_event); |
| EXPECT_EQ(1, listener.actions_performed()); |
| EXPECT_EQ(1u, listener.perform_action_index()); |
| |
| // Clears the typed text. |
| combobox()->OnBlur(); |
| combobox()->RequestFocus(); |
| |
| // Type the first character of "PEANUT BUTTER", which should change the |
| // selected index and perform an action. |
| key_event = |
| ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_E, ui::DomCode::US_P, 0, |
| ui::DomKey::FromCharacter('P'), ui::EventTimeForNow()); |
| input_client->InsertChar(key_event); |
| EXPECT_EQ(2, listener.actions_performed()); |
| EXPECT_EQ(2u, listener.perform_action_index()); |
| } |
| |
| // Test properties on the Combobox menu model. |
| TEST_F(ComboboxTest, MenuModel) { |
| const int kSeparatorIndex = 3; |
| std::set<size_t> separators; |
| separators.insert(kSeparatorIndex); |
| InitCombobox(&separators); |
| |
| ui::MenuModel* menu_model = ComboboxTestApi(combobox()).menu_model(); |
| |
| EXPECT_EQ(TestComboboxModel::kItemCount, menu_model->GetItemCount()); |
| EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, |
| menu_model->GetTypeAt(kSeparatorIndex)); |
| |
| #if BUILDFLAG(IS_MAC) |
| // Comboboxes on Mac should have checkmarks, with the selected item checked, |
| EXPECT_EQ(ui::MenuModel::TYPE_CHECK, menu_model->GetTypeAt(0)); |
| EXPECT_EQ(ui::MenuModel::TYPE_CHECK, menu_model->GetTypeAt(1)); |
| EXPECT_TRUE(menu_model->IsItemCheckedAt(0)); |
| EXPECT_FALSE(menu_model->IsItemCheckedAt(1)); |
| |
| combobox()->SetSelectedIndex(1); |
| EXPECT_FALSE(menu_model->IsItemCheckedAt(0)); |
| EXPECT_TRUE(menu_model->IsItemCheckedAt(1)); |
| #else |
| EXPECT_EQ(ui::MenuModel::TYPE_COMMAND, menu_model->GetTypeAt(0)); |
| EXPECT_EQ(ui::MenuModel::TYPE_COMMAND, menu_model->GetTypeAt(1)); |
| #endif |
| |
| // Override OS-specific checkmark setting. |
| model_->SetMenuCheckmarkOverride( |
| ui::ComboboxModel::ItemCheckmarkConfig::kEnabled); |
| EXPECT_EQ(ui::MenuModel::TYPE_CHECK, menu_model->GetTypeAt(0)); |
| EXPECT_EQ(ui::MenuModel::TYPE_CHECK, menu_model->GetTypeAt(1)); |
| model_->SetMenuCheckmarkOverride( |
| ui::ComboboxModel::ItemCheckmarkConfig::kDisabled); |
| EXPECT_EQ(ui::MenuModel::TYPE_COMMAND, menu_model->GetTypeAt(0)); |
| EXPECT_EQ(ui::MenuModel::TYPE_COMMAND, menu_model->GetTypeAt(1)); |
| |
| EXPECT_EQ(u"PEANUT BUTTER", menu_model->GetLabelAt(0)); |
| EXPECT_EQ(u"JELLY", menu_model->GetLabelAt(1)); |
| |
| EXPECT_TRUE(menu_model->IsVisibleAt(0)); |
| } |
| |
| // Verifies SetTooltipTextAndAccessibleName will call NotifyAccessibilityEvent. |
| TEST_F(ComboboxTest, SetTooltipTextNotifiesAccessibilityEvent) { |
| test::AXEventCounter counter(AXUpdateNotifier::Get()); |
| InitCombobox(nullptr); |
| std::u16string test_tooltip_text = u"Test Tooltip Text"; |
| EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged)); |
| |
| // `SetTooltipTextAndAccessibleName` does two things: |
| // 1. sets the tooltip text on the arrow button. `Button::SetTooltipText` |
| // fires a text-changed event. |
| // 2. if the accessible name is empty, calls |
| // `View::GetViewAccessibility().SetName` |
| // on the combobox. `GetViewAccessibility().SetName` fires a |
| // text-changed event. |
| combobox()->SetTooltipTextAndAccessibleName(test_tooltip_text); |
| EXPECT_EQ(test_tooltip_text, combobox()->GetTooltipTextAndAccessibleName()); |
| EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, |
| ax::mojom::Role::kButton)); |
| EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, |
| ax::mojom::Role::kComboBoxSelect)); |
| EXPECT_EQ(test_tooltip_text, |
| combobox()->GetViewAccessibility().GetCachedName()); |
| ui::AXNodeData data; |
| combobox()->GetViewAccessibility().GetAccessibleNodeData(&data); |
| const std::string& name = |
| data.GetStringAttribute(ax::mojom::StringAttribute::kName); |
| EXPECT_EQ(test_tooltip_text, ASCIIToUTF16(name)); |
| EXPECT_EQ(u"PEANUT BUTTER", |
| data.GetString16Attribute(ax::mojom::StringAttribute::kValue)); |
| } |
| |
| // Changing the value of the combobox should trigger a kTextChanged event. |
| TEST_F(ComboboxTest, SetValueAccessibilityEvents) { |
| InitCombobox(nullptr); |
| std::u16string value = u"hello world"; |
| test::AXEventCounter counter(views::AXUpdateNotifier::Get()); |
| EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged)); |
| combobox()->GetViewAccessibility().SetValue(value); |
| EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged)); |
| EXPECT_EQ(value, combobox()->GetViewAccessibility().GetValue()); |
| } |
| |
| // Regression test for crbug.com/1264288. |
| // Should fail in ASan build before the fix. |
| TEST_F(ComboboxTest, NoCrashWhenComboboxOutlivesModel) { |
| auto model = std::make_unique<TestComboboxModel>(); |
| auto combobox = std::make_unique<TestCombobox>(model.get()); |
| model.reset(); |
| combobox.reset(); |
| } |
| |
| namespace { |
| |
| std::string GetComboboxA11yValue(Combobox* combobox) { |
| const std::optional<size_t>& selected_index = combobox->GetSelectedIndex(); |
| return selected_index ? base::UTF16ToUTF8(combobox->GetModel()->GetItemAt( |
| selected_index.value())) |
| : std::string(); |
| } |
| |
| using ComboboxDefaultTest = 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: |
| size_t GetItemCount() const override { return item_count_; } |
| std::u16string GetItemAt(size_t index) const override { |
| DCHECK_LT(index, item_count_); |
| return base::NumberToString16(index); |
| } |
| std::optional<size_t> GetDefaultIndex() const override { |
| return default_index_; |
| } |
| |
| void SetItemCount(size_t item_count) { item_count_ = item_count; } |
| |
| void SetDefaultIndex(size_t default_index) { default_index_ = default_index; } |
| |
| private: |
| const raw_ptr<bool> destroyed_; |
| size_t item_count_ = 0; |
| std::optional<size_t> default_index_; |
| }; |
| |
| } // namespace |
| |
| TEST_F(ComboboxDefaultTest, Default) { |
| auto combobox = std::make_unique<Combobox>(); |
| EXPECT_EQ(0u, combobox->GetRowCount()); |
| EXPECT_FALSE(combobox->GetSelectedRow().has_value()); |
| } |
| |
| TEST_F(ComboboxDefaultTest, SetModel) { |
| bool destroyed = false; |
| std::unique_ptr<ConfigurableComboboxModel> model = |
| std::make_unique<ConfigurableComboboxModel>(&destroyed); |
| model->SetItemCount(42); |
| model->SetDefaultIndex(27); |
| { |
| auto combobox = std::make_unique<Combobox>(); |
| combobox->SetModel(model.get()); |
| EXPECT_EQ(42u, combobox->GetRowCount()); |
| EXPECT_EQ(27u, combobox->GetSelectedRow()); |
| } |
| EXPECT_FALSE(destroyed); |
| } |
| |
| TEST_F(ComboboxDefaultTest, SetOwnedModel) { |
| bool destroyed = false; |
| std::unique_ptr<ConfigurableComboboxModel> model = |
| std::make_unique<ConfigurableComboboxModel>(&destroyed); |
| model->SetItemCount(42); |
| model->SetDefaultIndex(27); |
| { |
| auto combobox = std::make_unique<Combobox>(); |
| combobox->SetOwnedModel(std::move(model)); |
| EXPECT_EQ(42u, combobox->GetRowCount()); |
| EXPECT_EQ(27u, combobox->GetSelectedRow()); |
| } |
| EXPECT_TRUE(destroyed); |
| } |
| |
| TEST_F(ComboboxDefaultTest, SetModelOverwriteOwned) { |
| bool destroyed = false; |
| std::unique_ptr<ConfigurableComboboxModel> model = |
| std::make_unique<ConfigurableComboboxModel>(&destroyed); |
| auto combobox = std::make_unique<Combobox>(); |
| combobox->SetModel(model.get()); |
| ASSERT_FALSE(destroyed); |
| combobox->SetOwnedModel(std::make_unique<ConfigurableComboboxModel>()); |
| EXPECT_FALSE(destroyed); |
| } |
| |
| TEST_F(ComboboxDefaultTest, SetOwnedModelOverwriteOwned) { |
| bool destroyed_first = false; |
| bool destroyed_second = false; |
| { |
| auto combobox = std::make_unique<Combobox>(); |
| combobox->SetOwnedModel( |
| std::make_unique<ConfigurableComboboxModel>(&destroyed_first)); |
| ASSERT_FALSE(destroyed_first); |
| combobox->SetOwnedModel( |
| std::make_unique<ConfigurableComboboxModel>(&destroyed_second)); |
| EXPECT_TRUE(destroyed_first); |
| ASSERT_FALSE(destroyed_second); |
| } |
| EXPECT_TRUE(destroyed_second); |
| } |
| |
| TEST_F(ComboboxDefaultTest, InteractionWithEmptyModel) { |
| ui::AXNodeData node_data; |
| |
| // Empty model. |
| // Verify `GetAccessibleNodeData()` doesn't crash when interacting with empty |
| // model. |
| auto simple_model = std::make_unique<ui::SimpleComboboxModel>( |
| std::vector<ui::SimpleComboboxModel::Item>()); |
| auto combobox = std::make_unique<Combobox>(simple_model.get()); |
| |
| IgnoreMissingWidgetForTestingScopedSetter ignore_missing_widget( |
| combobox->GetViewAccessibility()); |
| |
| combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(0u, combobox->GetModel()->GetItemCount()); |
| EXPECT_EQ(std::nullopt, combobox->GetSelectedIndex()); |
| EXPECT_EQ(GetComboboxA11yValue(combobox.get()), |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue)); |
| |
| // Non-empty model. |
| node_data = ui::AXNodeData(); |
| simple_model->UpdateItemList({ui::SimpleComboboxModel::Item(u"item")}); |
| combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(1u, combobox->GetModel()->GetItemCount()); |
| EXPECT_EQ(0u, combobox->GetSelectedIndex()); |
| EXPECT_EQ(GetComboboxA11yValue(combobox.get()), |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue)); |
| |
| // Empty model. |
| // Verify `OnComboboxModelChanged()` doesn't crash when interacting with empty |
| // model. |
| node_data = ui::AXNodeData(); |
| simple_model->UpdateItemList({}); |
| combobox->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(0u, combobox->GetModel()->GetItemCount()); |
| EXPECT_EQ(std::nullopt, combobox->GetSelectedIndex()); |
| EXPECT_EQ(GetComboboxA11yValue(combobox.get()), |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue)); |
| } |
| |
| } // namespace views |