| // 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 |