chromium/chrome/browser/ui/ash/input_method/candidate_view_unittest.cc

// 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 "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.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();

    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_->AddChildView(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 base::ranges::count_if(
        children, [](const views::View* v) { return !!v->background(); });
  }

  int GetHighlightedIndex() const {
    const auto& children = container_->children();
    const auto it = base::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::AXEventManager::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