blob: e919dd28dd95e0608fc9ed4d065f5b09d4c6b68b [file] [log] [blame]
// 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