| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include "chrome/browser/ui/ash/input_method/candidate_view.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| |
| #include "base/check.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/aura/window.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/test/ax_event_counter.h" |
| #include "ui/views/test/views_test_base.h" |
| #include "ui/views/widget/widget_delegate.h" |
| |
| namespace ui { |
| namespace ime { |
| namespace { |
| |
| const char* const kDummyCandidates[] = { |
| "candidate1", |
| "candidate2", |
| "candidate3", |
| }; |
| |
| } // namespace |
| |
| class CandidateViewTest : public views::ViewsTestBase { |
| public: |
| CandidateViewTest() = default; |
| |
| CandidateViewTest(const CandidateViewTest&) = delete; |
| CandidateViewTest& operator=(const CandidateViewTest&) = delete; |
| |
| ~CandidateViewTest() override = default; |
| |
| void SetUp() override { |
| views::ViewsTestBase::SetUp(); |
| |
| views::Widget::InitParams init_params( |
| CreateParams(views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| views::Widget::InitParams::TYPE_WINDOW)); |
| |
| init_params.delegate = new views::WidgetDelegateView( |
| views::WidgetDelegateView::CreatePassKey()); |
| |
| container_ = init_params.delegate->GetContentsView(); |
| container_->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| for (size_t i = 0; i < std::size(kDummyCandidates); ++i) { |
| CandidateView* candidate = new CandidateView( |
| views::Button::PressedCallback(), ui::CandidateWindow::VERTICAL); |
| ui::CandidateWindow::Entry entry; |
| entry.value = base::UTF8ToUTF16(kDummyCandidates[i]); |
| candidate->SetEntry(entry); |
| container_->AddChildViewRaw(candidate); |
| } |
| |
| widget_ = new views::Widget(); |
| widget_->Init(std::move(init_params)); |
| widget_->Show(); |
| |
| aura::Window* native_window = widget_->GetNativeWindow(); |
| event_generator_ = std::make_unique<ui::test::EventGenerator>( |
| native_window->GetRootWindow(), native_window); |
| } |
| |
| void TearDown() override { |
| widget_->Close(); |
| |
| views::ViewsTestBase::TearDown(); |
| } |
| |
| protected: |
| CandidateView* GetCandidateAt(size_t index) { |
| return static_cast<CandidateView*>(container_->children()[index]); |
| } |
| |
| size_t GetHighlightedCount() const { |
| const auto& children = container_->children(); |
| return std::ranges::count_if( |
| children, [](const views::View* v) { return !!v->background(); }); |
| } |
| |
| int GetHighlightedIndex() const { |
| const auto& children = container_->children(); |
| const auto it = std::ranges::find_if( |
| children, [](const views::View* v) { return !!v->background(); }); |
| return (it == children.cend()) ? -1 : std::distance(children.cbegin(), it); |
| } |
| |
| ui::test::EventGenerator* event_generator() { return event_generator_.get(); } |
| |
| private: |
| raw_ptr<views::Widget, DanglingUntriaged> widget_ = nullptr; |
| raw_ptr<views::View, DanglingUntriaged> container_ = nullptr; |
| std::unique_ptr<ui::test::EventGenerator> event_generator_; |
| }; |
| |
| TEST_F(CandidateViewTest, MouseHovers) { |
| GetCandidateAt(0)->SetHighlighted(true); |
| |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(0, GetHighlightedIndex()); |
| |
| // Mouse hover shouldn't change the background. |
| event_generator()->MoveMouseTo( |
| GetCandidateAt(0)->GetBoundsInScreen().CenterPoint()); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(0, GetHighlightedIndex()); |
| |
| // Mouse hover shouldn't change the background. |
| event_generator()->MoveMouseTo( |
| GetCandidateAt(1)->GetBoundsInScreen().CenterPoint()); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(0, GetHighlightedIndex()); |
| |
| // Mouse hover shouldn't change the background. |
| event_generator()->MoveMouseTo( |
| GetCandidateAt(2)->GetBoundsInScreen().CenterPoint()); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(0, GetHighlightedIndex()); |
| } |
| |
| TEST_F(CandidateViewTest, MouseClick) { |
| bool clicked = false; |
| CandidateView* view = GetCandidateAt(1); |
| view->SetCallback( |
| base::BindRepeating([](bool* clicked) { *clicked = true; }, &clicked)); |
| event_generator()->MoveMouseTo(view->GetBoundsInScreen().CenterPoint()); |
| event_generator()->ClickLeftButton(); |
| EXPECT_TRUE(clicked); |
| } |
| |
| TEST_F(CandidateViewTest, ClickAndMove) { |
| GetCandidateAt(0)->SetHighlighted(true); |
| |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(0, GetHighlightedIndex()); |
| |
| bool clicked = false; |
| CandidateView* view = GetCandidateAt(1); |
| view->SetCallback( |
| base::BindRepeating([](bool* clicked) { *clicked = true; }, &clicked)); |
| event_generator()->MoveMouseTo( |
| GetCandidateAt(2)->GetBoundsInScreen().CenterPoint()); |
| event_generator()->PressLeftButton(); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(2, GetHighlightedIndex()); |
| |
| // Highlight follows the drag. |
| event_generator()->MoveMouseTo(view->GetBoundsInScreen().CenterPoint()); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(1, GetHighlightedIndex()); |
| |
| event_generator()->MoveMouseTo( |
| GetCandidateAt(0)->GetBoundsInScreen().CenterPoint()); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(0, GetHighlightedIndex()); |
| |
| event_generator()->MoveMouseTo(view->GetBoundsInScreen().CenterPoint()); |
| EXPECT_EQ(1u, GetHighlightedCount()); |
| EXPECT_EQ(1, GetHighlightedIndex()); |
| |
| EXPECT_FALSE(clicked); |
| event_generator()->ReleaseLeftButton(); |
| EXPECT_TRUE(clicked); |
| } |
| |
| TEST_F(CandidateViewTest, SetEntryChangesAccessibleName) { |
| CandidateView* view = GetCandidateAt(1); |
| |
| ui::CandidateWindow::Entry entry; |
| entry.value = u"Candidate"; |
| view->SetEntry(entry); |
| EXPECT_EQ(u"Candidate", view->GetViewAccessibility().GetCachedName()); |
| } |
| |
| TEST_F(CandidateViewTest, SetEntryNotifiesAccessibilityEvent) { |
| views::test::AXEventCounter counter(views::AXUpdateNotifier::Get()); |
| CandidateView* view = GetCandidateAt(1); |
| |
| // Calling SetEntry affects the accessible name, so it should notify twice: |
| // once for CandidateView's child label and once for CandidateView itself. |
| EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged)); |
| ui::CandidateWindow::Entry entry; |
| entry.value = u"Candidate"; |
| view->SetEntry(entry); |
| EXPECT_EQ(2, counter.GetCount(ax::mojom::Event::kTextChanged)); |
| } |
| |
| TEST_F(CandidateViewTest, AccessibilityAttributes) { |
| CandidateView* view = GetCandidateAt(1); |
| |
| ui::CandidateWindow::Entry entry; |
| entry.value = u"Candidate"; |
| view->SetEntry(entry); |
| |
| ui::AXNodeData data; |
| static_cast<views::View*>(view)->GetViewAccessibility().GetAccessibleNodeData( |
| &data); |
| |
| EXPECT_EQ(ax::mojom::Role::kImeCandidate, data.role); |
| EXPECT_EQ("Candidate", |
| data.GetStringAttribute(ax::mojom::StringAttribute::kName)); |
| EXPECT_EQ(static_cast<int>(ax::mojom::DefaultActionVerb::kPress), |
| data.GetIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb)); |
| EXPECT_EQ(0, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| EXPECT_EQ(0, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| |
| view->SetPositionData(1, 3); |
| data = ui::AXNodeData(); |
| static_cast<views::View*>(view)->GetViewAccessibility().GetAccessibleNodeData( |
| &data); |
| EXPECT_EQ(2, data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); |
| EXPECT_EQ(3, data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| } |
| |
| } // namespace ime |
| } // namespace ui |