910e62b5创建于 1月15日历史提交
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/views/mahi/mahi_menu_view.h"

#include <memory>
#include <string>
#include <string_view>

#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/ash/mahi/web_contents/test_support/fake_mahi_web_contents_manager.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/ui/ash/editor_menu/utils/utils.h"
#include "chrome/browser/ui/views/mahi/mahi_menu_constants.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "chromeos/components/mahi/public/cpp/mahi_browser_util.h"
#include "chromeos/components/mahi/public/cpp/mahi_util.h"
#include "chromeos/components/mahi/public/cpp/mahi_web_contents_manager.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/application_locale_storage/application_locale_storage.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/screen.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_utils.h"

namespace chromeos::mahi {

using MahiMenuViewTest = ChromeViewsTestBase;

namespace {

using ::testing::Eq;

// This default_button_status shows summary button for the whole document and
// elucidation buttons.
const MahiMenuView::ButtonStatus default_button_status{
    // kEmpty means the summary button is for the whole document.
    .summary_of_selection_eligibility = SelectedTextState::kEmpty,
    .elucidation_eligiblity = SelectedTextState::kEligible};

class MockMahiWebContentsManager : public ::mahi::FakeMahiWebContentsManager {
 public:
  MOCK_METHOD(void,
              OnContextMenuClicked,
              (int64_t display_id,
               ::chromeos::mahi::ButtonType button_type,
               std::u16string_view question,
               const gfx::Rect& mahi_menu_bounds),
              (override));
};

// A widget that always claims to be active, regardless of its real activation
// status.
class ActiveWidget : public views::Widget {
 public:
  ActiveWidget() = default;

  ActiveWidget(const ActiveWidget&) = delete;
  ActiveWidget& operator=(const ActiveWidget&) = delete;

  ~ActiveWidget() override = default;

  bool IsActive() const override { return true; }
};

// Helper function to simulate typing "TEST".
void TypeTestResponse(ui::test::EventGenerator* event_generator) {
  ui::KeyboardCode keycodes[] = {ui::VKEY_T, ui::VKEY_E, ui::VKEY_S,
                                 ui::VKEY_T};
  for (ui::KeyboardCode keycode : keycodes) {
    event_generator->PressAndReleaseKey(keycode, ui::EF_NONE);
  }
}

const std::string& GetApplicationLocale() {
  return TestingBrowserProcess::GetGlobal()
      ->GetFeatures()
      ->application_locale_storage()
      ->Get();
}

std::unique_ptr<MahiMenuView> CreateMahiMenuView(
    MahiMenuView::ButtonStatus button_status) {
  return std::make_unique<MahiMenuView>(TestingBrowserProcess::GetGlobal()
                                            ->GetFeatures()
                                            ->application_locale_storage(),
                                        button_status);
}

}  // namespace

TEST_F(MahiMenuViewTest, Bounds) {
  const gfx::Rect anchor_view_bounds = gfx::Rect(50, 50, 25, 100);
  auto menu_widget =
      MahiMenuView::CreateWidget(TestingBrowserProcess::GetGlobal()
                                     ->GetFeatures()
                                     ->application_locale_storage(),
                                 anchor_view_bounds, default_button_status);

  // The bounds of the created widget should be similar to the value from the
  // utils function.
  EXPECT_EQ(editor_menu::GetEditorMenuBounds(
                anchor_view_bounds, menu_widget.get()->GetContentsView(),
                GetApplicationLocale()),
            menu_widget->GetRestoredBounds());
}

TEST_F(MahiMenuViewTest, SettingsButtonClicked) {
  base::HistogramTester histogram;
  MockMahiWebContentsManager mock_mahi_web_contents_manager;
  chromeos::ScopedMahiWebContentsManagerOverride
      scoped_mahi_web_contents_manager(&mock_mahi_web_contents_manager);

  std::unique_ptr<views::Widget> menu_widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  EXPECT_CALL(
      mock_mahi_web_contents_manager,
      OnContextMenuClicked(
          Eq(display::Screen::Get()
                 ->GetDisplayNearestWindow(menu_widget->GetNativeWindow())
                 .id()),
          Eq(::chromeos::mahi::ButtonType::kSettings),
          /*question=*/Eq(u""), Eq(menu_view->GetBoundsInScreen())))
      .Times(1);

  ui::test::EventGenerator event_generator(
      views::GetRootWindow(menu_widget.get()));
  event_generator.MoveMouseTo(menu_view->GetViewByID(ViewID::kSettingsButton)
                                  ->GetBoundsInScreen()
                                  .CenterPoint());
  event_generator.ClickLeftButton();

  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSettingsButton, 1);
}

TEST_F(MahiMenuViewTest, SummaryButtonClicked) {
  MockMahiWebContentsManager mock_mahi_web_contents_manager;
  auto scoped_mahi_web_contents_manager =
      std::make_unique<chromeos::ScopedMahiWebContentsManagerOverride>(
          &mock_mahi_web_contents_manager);

  auto menu_widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  auto event_generator = std::make_unique<ui::test::EventGenerator>(
      views::GetRootWindow(menu_widget.get()));
  event_generator->MoveMouseTo(menu_view->GetViewByID(ViewID::kSummaryButton)
                                   ->GetBoundsInScreen()
                                   .CenterPoint());

  base::HistogramTester histogram;
  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSummaryButton, 0);

  // Make sure that clicking the summary button would trigger the function in
  // `MahiWebContentsManager` with the correct parameters.
  base::RunLoop run_loop;
  EXPECT_CALL(mock_mahi_web_contents_manager, OnContextMenuClicked)
      .WillOnce([&run_loop, &menu_widget](
                    int64_t display_id,
                    ::chromeos::mahi::ButtonType button_type,
                    std::u16string_view question, gfx::Rect mahi_menu_bounds) {
        EXPECT_EQ(display::Screen::Get()
                      ->GetDisplayNearestWindow(menu_widget->GetNativeWindow())
                      .id(),
                  display_id);
        EXPECT_EQ(::chromeos::mahi::ButtonType::kSummary, button_type);
        EXPECT_EQ(std::u16string_view(), question);
        run_loop.Quit();
      });

  event_generator->ClickLeftButton();
  run_loop.Run();

  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSummaryButton, 1);
}

// Similar to `SummaryButtonClicked`, but initializes the summary button to do a
// summary for the selected text.
TEST_F(MahiMenuViewTest, SummaryOfSelectionButtonClicked) {
  MockMahiWebContentsManager mock_mahi_web_contents_manager;
  auto scoped_mahi_web_contents_manager =
      std::make_unique<chromeos::ScopedMahiWebContentsManagerOverride>(
          &mock_mahi_web_contents_manager);

  auto menu_widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);

  // The `button_status` makes the summary button for selected text.
  MahiMenuView::ButtonStatus button_status{.summary_of_selection_eligibility =
                                               SelectedTextState::kEligible};
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(button_status));

  auto event_generator = std::make_unique<ui::test::EventGenerator>(
      views::GetRootWindow(menu_widget.get()));
  event_generator->MoveMouseTo(menu_view->GetViewByID(ViewID::kSummaryButton)
                                   ->GetBoundsInScreen()
                                   .CenterPoint());

  base::HistogramTester histogram;
  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSummaryOfSelectionButton, 0);

  // Make sure that clicking the summary button would trigger the function in
  // `MahiWebContentsManager` with the correct parameters.
  base::RunLoop run_loop;
  EXPECT_CALL(mock_mahi_web_contents_manager, OnContextMenuClicked)
      .WillOnce([&run_loop, &menu_widget](
                    int64_t display_id,
                    ::chromeos::mahi::ButtonType button_type,
                    std::u16string_view question, gfx::Rect mahi_menu_bounds) {
        EXPECT_EQ(display::Screen::Get()
                      ->GetDisplayNearestWindow(menu_widget->GetNativeWindow())
                      .id(),
                  display_id);
        EXPECT_EQ(::chromeos::mahi::ButtonType::kSummaryOfSelection,
                  button_type);
        EXPECT_EQ(std::u16string_view(), question);
        run_loop.Quit();
      });

  event_generator->ClickLeftButton();
  run_loop.Run();

  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSummaryOfSelectionButton, 1);
}

// Tests that the availability of summary button respects the button status.
TEST_F(MahiMenuViewTest, SummaryButtonAvailability) {
  auto menu_widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);

  // kEmpty enables the button for whole document.
  MahiMenuView::ButtonStatus button_status{.summary_of_selection_eligibility =
                                               SelectedTextState::kEmpty};
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_TRUE(menu_view->GetViewByID(ViewID::kSummaryButton)->GetEnabled());

  // kTooShort shows the button but disabled.
  button_status.summary_of_selection_eligibility = SelectedTextState::kTooShort;
  menu_view = menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_FALSE(menu_view->GetViewByID(ViewID::kSummaryButton)->GetEnabled());
  EXPECT_EQ(menu_view->GetViewByID(ViewID::kSummaryButton)->GetTooltipText(),
            l10n_util::GetStringUTF16(
                IDS_MAHI_SUMMARIZE_BUTTON_TOOL_TIP_FOR_SELECTION_TOO_SHORT));

  // kEligible enables the button for summary of selection.
  button_status.summary_of_selection_eligibility = SelectedTextState::kEligible;
  menu_view = menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_TRUE(menu_view->GetViewByID(ViewID::kSummaryButton)->GetEnabled());
}

TEST_F(MahiMenuViewTest, ElucidationButtonVisibilityAvailability) {
  auto menu_widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);

  // kUnknown hides the elucidation button.
  MahiMenuView::ButtonStatus button_status{
      .summary_of_selection_eligibility = SelectedTextState::kEmpty,
      .elucidation_eligiblity = SelectedTextState::kUnknown};
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_FALSE(
      menu_view->GetViewByID(ViewID::kElucidationButton)->GetVisible());

  // kTooLong and kTooShort shows the button but disabled.
  button_status.elucidation_eligiblity = SelectedTextState::kTooLong;
  menu_view = menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_TRUE(menu_view->GetViewByID(ViewID::kElucidationButton)->GetVisible());
  EXPECT_FALSE(
      menu_view->GetViewByID(ViewID::kElucidationButton)->GetEnabled());

  button_status.elucidation_eligiblity = SelectedTextState::kTooShort;
  menu_view = menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_TRUE(menu_view->GetViewByID(ViewID::kElucidationButton)->GetVisible());
  EXPECT_FALSE(
      menu_view->GetViewByID(ViewID::kElucidationButton)->GetEnabled());

  // kEligible enables the button.
  button_status.elucidation_eligiblity = SelectedTextState::kEligible;
  menu_view = menu_widget->SetContentsView(CreateMahiMenuView(button_status));
  EXPECT_TRUE(menu_view->GetViewByID(ViewID::kElucidationButton)->GetVisible());
  EXPECT_TRUE(menu_view->GetViewByID(ViewID::kElucidationButton)->GetEnabled());
}

TEST_F(MahiMenuViewTest, ElucidationButtonClicked) {
  MockMahiWebContentsManager mock_mahi_web_contents_manager;
  auto scoped_mahi_web_contents_manager =
      std::make_unique<chromeos::ScopedMahiWebContentsManagerOverride>(
          &mock_mahi_web_contents_manager);

  auto menu_widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  views::test::RunScheduledLayout(menu_view);

  auto event_generator = std::make_unique<ui::test::EventGenerator>(
      views::GetRootWindow(menu_widget.get()));
  event_generator->MoveMouseTo(
      menu_view->GetViewByID(ViewID::kElucidationButton)
          ->GetBoundsInScreen()
          .CenterPoint());

  base::HistogramTester histogram;
  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kElucidationButton, 0);

  // Make sure that clicking the summary button would trigger the function in
  // `MahiWebContentsManager` with the correct parameters.
  base::RunLoop run_loop;
  EXPECT_CALL(mock_mahi_web_contents_manager, OnContextMenuClicked)
      .WillOnce([&run_loop, &menu_widget](
                    int64_t display_id,
                    ::chromeos::mahi::ButtonType button_type,
                    std::u16string_view question, gfx::Rect mahi_menu_bounds) {
        EXPECT_EQ(display::Screen::Get()
                      ->GetDisplayNearestWindow(menu_widget->GetNativeWindow())
                      .id(),
                  display_id);
        EXPECT_EQ(::chromeos::mahi::ButtonType::kElucidation, button_type);
        EXPECT_EQ(std::u16string_view(), question);
        run_loop.Quit();
      });

  event_generator->ClickLeftButton();
  run_loop.Run();

  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kElucidationButton, 1);
}

TEST_F(MahiMenuViewTest, SubmitQuestionButtonEnabledAfterTextInput) {
  auto menu_widget = std::make_unique<ActiveWidget>();
  menu_widget->Init(CreateParamsForTestWidget());

  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  auto event_generator = std::make_unique<ui::test::EventGenerator>(
      views::GetRootWindow(menu_widget.get()));

  auto* submit_question_button =
      menu_view->GetViewByID(ViewID::kSubmitQuestionButton);
  auto* textfield = menu_view->GetViewByID(ViewID::kTextfield);

  EXPECT_FALSE(submit_question_button->GetEnabled());

  event_generator->MoveMouseTo(textfield->GetBoundsInScreen().CenterPoint());
  event_generator->ClickLeftButton();

  TypeTestResponse(event_generator.get());

  EXPECT_TRUE(submit_question_button->GetEnabled());
}

TEST_F(MahiMenuViewTest, QuestionSubmitted) {
  MockMahiWebContentsManager mock_mahi_web_contents_manager;
  auto scoped_mahi_web_contents_manager =
      std::make_unique<chromeos::ScopedMahiWebContentsManagerOverride>(
          &mock_mahi_web_contents_manager);

  auto menu_widget = std::make_unique<ActiveWidget>();
  menu_widget->Init(CreateParamsForTestWidget());
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  auto event_generator = std::make_unique<ui::test::EventGenerator>(
      views::GetRootWindow(menu_widget.get()));
  event_generator->MoveMouseTo(menu_view->GetViewByID(ViewID::kTextfield)
                                   ->GetBoundsInScreen()
                                   .CenterPoint());
  event_generator->ClickLeftButton();
  TypeTestResponse(event_generator.get());

  event_generator->MoveMouseTo(
      menu_view->GetViewByID(ViewID::kSubmitQuestionButton)
          ->GetBoundsInScreen()
          .CenterPoint());

  base::HistogramTester histogram;
  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSubmitQuestionButton, 0);

  // Make sure that clicking the summary button would trigger the function in
  // `MahiWebContentsManager` with the correct parameters.
  base::RunLoop run_loop;
  EXPECT_CALL(mock_mahi_web_contents_manager, OnContextMenuClicked)
      .WillOnce([&run_loop, &menu_widget](
                    int64_t display_id,
                    ::chromeos::mahi::ButtonType button_type,
                    std::u16string_view question,
                    const gfx::Rect& mahi_menu_bounds) {
        EXPECT_EQ(display::Screen::Get()
                      ->GetDisplayNearestWindow(menu_widget->GetNativeWindow())
                      .id(),
                  display_id);
        EXPECT_EQ(::chromeos::mahi::ButtonType::kQA, button_type);
        EXPECT_EQ(u"test", question);
        run_loop.Quit();
      });

  event_generator->ClickLeftButton();
  run_loop.Run();

  histogram.ExpectBucketCount(kMahiContextMenuButtonClickHistogram,
                              MahiMenuButton::kSubmitQuestionButton, 1);
}

TEST_F(MahiMenuViewTest, EmptyQuestionNotSubmitted) {
  MockMahiWebContentsManager mock_mahi_web_contents_manager;
  auto scoped_mahi_web_contents_manager =
      std::make_unique<chromeos::ScopedMahiWebContentsManagerOverride>(
          &mock_mahi_web_contents_manager);

  auto menu_widget = std::make_unique<ActiveWidget>();
  menu_widget->Init(CreateParamsForTestWidget());
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  auto event_generator = std::make_unique<ui::test::EventGenerator>(
      views::GetRootWindow(menu_widget.get()));
  event_generator->MoveMouseTo(menu_view->GetViewByID(ViewID::kTextfield)
                                   ->GetBoundsInScreen()
                                   .CenterPoint());
  event_generator->ClickLeftButton();

  // Make sure that hitting enter with an empty textfield doesn't result in a
  // call to OnContextMenuClicked.
  base::RunLoop run_loop;
  EXPECT_CALL(mock_mahi_web_contents_manager, OnContextMenuClicked).Times(0);

  event_generator->PressAndReleaseKey(ui::VKEY_RETURN);
}

TEST_F(MahiMenuViewTest, AccessibleProperties) {
  auto menu_widget = std::make_unique<ActiveWidget>();
  menu_widget->Init(CreateParamsForTestWidget());
  auto* menu_view =
      menu_widget->SetContentsView(CreateMahiMenuView(default_button_status));

  ui::AXNodeData data;
  menu_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.role, ax::mojom::Role::kDialog);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            l10n_util::GetStringUTF16(IDS_ASH_MAHI_MENU_TITLE));
}

}  // namespace chromeos::mahi