// 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 "ash/system/mahi/mahi_panel_view.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/url_constants.h"
#include "ash/public/cpp/image_util.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/test/test_new_window_delegate.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/system_textfield.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/system/mahi/mahi_content_source_button.h"
#include "ash/system/mahi/mahi_ui_controller.h"
#include "ash/system/mahi/mahi_utils.h"
#include "ash/system/mahi/test/mahi_test_util.h"
#include "ash/system/mahi/test/mock_mahi_manager.h"
#include "ash/test/ash_test_base.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chromeos/components/mahi/public/cpp/mahi_manager.h"
#include "chromeos/constants/chromeos_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/scroll_bar.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace ash {
namespace {
// Aliases ---------------------------------------------------------------------
using chromeos::MahiResponseStatus;
using ::testing::_;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Return;
// MockNewWindowDelegate -------------------------------------------------------
class MockNewWindowDelegate : public NiceMock<TestNewWindowDelegate> {
public:
// TestNewWindowDelegate:
MOCK_METHOD(void,
OpenUrl,
(const GURL& url, OpenUrlFrom from, Disposition disposition),
(override));
};
// Helpers ---------------------------------------------------------------------
// Returns a comprehensive list of potential errors from Mahi backend.
// NOTE: `MahiResponseStatus::kLowQuota` is a warning instead of an error.
std::vector<MahiResponseStatus> GetMahiErrors() {
std::vector<MahiResponseStatus> errors;
for (size_t status_value = 0;
status_value <= static_cast<size_t>(MahiResponseStatus::kMaxValue);
++status_value) {
MahiResponseStatus status = static_cast<MahiResponseStatus>(status_value);
if (status != MahiResponseStatus::kSuccess &&
status != MahiResponseStatus::kLowQuota) {
errors.push_back(status);
}
}
return errors;
}
void PressEnter() {
ui::test::EventGenerator(Shell::GetPrimaryRootWindow())
.PressKey(ui::KeyboardCode::VKEY_RETURN, ui::EF_NONE);
}
// Returns an answer asyncly with the specified `status`. Use `waiter` to wait
// for the response.
void ReturnDefaultAnswerAsyncly(
base::test::TestFuture<void>& waiter,
MahiResponseStatus status,
chromeos::MahiManager::MahiAnswerQuestionCallback callback,
base::TimeDelta delay = base::TimeDelta()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](base::OnceClosure unblock_closure, MahiResponseStatus status,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(u"fake answer", status);
std::move(unblock_closure).Run();
},
waiter.GetCallback(), status, std::move(callback)),
delay);
}
// Returns `kFakeOutlines` asyncly with the specified `status`. Use `waiter` to
// wait for the response.
void ReturnDefaultOutlinesAsyncly(
base::test::TestFuture<void>& waiter,
MahiResponseStatus status,
chromeos::MahiManager::MahiOutlinesCallback callback) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::OnceClosure unblock_closure, MahiResponseStatus status,
chromeos::MahiManager::MahiOutlinesCallback callback) {
std::move(callback).Run(mahi_test_util::GetDefaultFakeOutlines(),
status);
std::move(unblock_closure).Run();
},
waiter.GetCallback(), status, std::move(callback)));
}
// Returns a fake summary asyncly with the specified `status`. Use `waiter` to
// wait for the response.
void ReturnDefaultSummaryAsyncly(
base::test::TestFuture<void>& waiter,
MahiResponseStatus status,
chromeos::MahiManager::MahiSummaryCallback callback,
base::TimeDelta delay = base::TimeDelta()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](base::OnceClosure unblock_closure, MahiResponseStatus status,
chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(u"fake summary", status);
std::move(unblock_closure).Run();
},
waiter.GetCallback(), status, std::move(callback)),
delay);
}
// Returns a long summary.
void ReturnLongSummary(chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(
base::StrCat(std::vector<std::u16string>(100, u"Long Summary\n")),
MahiResponseStatus::kSuccess);
}
const std::u16string& GetContentSourceTitle(views::View* mahi_view) {
return views::AsViewClass<MahiContentSourceButton>(
mahi_view->GetViewByID(
mahi_constants::ViewId::kContentSourceButton))
->GetText();
}
gfx::ImageSkia GetContentSourceIcon(views::View* mahi_view) {
return views::AsViewClass<MahiContentSourceButton>(
mahi_view->GetViewByID(
mahi_constants::ViewId::kContentSourceButton))
->GetImage(views::Button::STATE_NORMAL);
}
views::Label* GetSummaryLabel(views::View* mahi_view) {
return views::AsViewClass<views::Label>(
mahi_view->GetViewByID(mahi_constants::ViewId::kSummaryLabel));
}
// Generates a random string, given the maximum amount of words the string can
// have.
std::u16string GetRandomString(int max_words_count) {
int string_length = base::RandInt(1, max_words_count);
std::vector<char> random_chars;
for (int string_index = 0; string_index < string_length; string_index++) {
int word_length = base::RandInt(1, 10);
for (int word_index = 0; word_index < word_length; word_index++) {
// Add a random character from 'a' to 'z' to the string.
random_chars.push_back(base::RandInt('a', 'z'));
}
// Add a space between each word.
random_chars.push_back(0x20);
}
return std::u16string(random_chars.begin(), random_chars.end());
}
} // namespace
class MahiPanelViewTest : public AshTestBase {
public:
MahiPanelViewTest()
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
MockMahiManager& mock_mahi_manager() { return mock_mahi_manager_; }
MahiUiController* ui_controller() { return &ui_controller_; }
MockNewWindowDelegate& new_window_delegate() { return *new_window_delegate_; }
MahiPanelView* panel_view() { return panel_view_; }
views::Widget* widget() { return widget_.get(); }
protected:
// AshTestBase:
void SetUp() override {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{chromeos::features::kMahi,
chromeos::features::kFeatureManagementMahi},
/*disabled_features=*/{});
auto delegate = std::make_unique<MockNewWindowDelegate>();
new_window_delegate_ = delegate.get();
delegate_provider_ =
std::make_unique<TestNewWindowDelegateProvider>(std::move(delegate));
AshTestBase::SetUp();
scoped_setter_ = std::make_unique<chromeos::ScopedMahiManagerSetter>(
&mock_mahi_manager_);
CreatePanelWidget();
}
void TearDown() override {
panel_view_ = nullptr;
widget_.reset();
scoped_setter_.reset();
AshTestBase::TearDown();
new_window_delegate_ = nullptr;
}
// Creates a widget hosting `MahiPanelView`. Recreates if there is one.
void CreatePanelWidget() {
ResetPanelWidget();
widget_ = CreateFramelessTestWidget();
widget_->SetBounds(
gfx::Rect(/*x=*/0, /*y=*/0,
/*width=*/mahi_constants::kPanelDefaultWidth,
/*height=*/mahi_constants::kPanelDefaultHeight));
panel_view_ = widget_->SetContentsView(
std::make_unique<MahiPanelView>(&ui_controller_));
}
void ResetPanelWidget() {
// Avoid creating a dangling pointer.
panel_view_ = nullptr;
widget_.reset();
}
// Submit a test question by setting the text in the textfield and click send
// button.
void SubmitTestQuestion(const std::u16string& question = u"fake question") {
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
ASSERT_TRUE(question_textfield);
question_textfield->SetText(question);
auto* const send_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kAskQuestionSendButton);
ASSERT_TRUE(send_button);
LeftClickOn(send_button);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
NiceMock<MockMahiManager> mock_mahi_manager_;
std::unique_ptr<chromeos::ScopedMahiManagerSetter> scoped_setter_;
MahiUiController ui_controller_;
raw_ptr<MahiPanelView> panel_view_ = nullptr;
std::unique_ptr<views::Widget> widget_;
raw_ptr<MockNewWindowDelegate> new_window_delegate_;
std::unique_ptr<TestNewWindowDelegateProvider> delegate_provider_;
};
// Checks that the summary text is set correctly in ctor with different texts.
TEST_F(MahiPanelViewTest, SummaryText) {
const std::u16string summary_text1(u"test summary text 1");
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault([&summary_text1](
chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(summary_text1, MahiResponseStatus::kSuccess);
});
MahiPanelView mahi_view1(ui_controller());
const auto* const summary_label1 = views::AsViewClass<views::Label>(
mahi_view1.GetViewByID(mahi_constants::ViewId::kSummaryLabel));
EXPECT_EQ(summary_text1, summary_label1->GetText());
const std::u16string summary_text2(u"test summary text 2");
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault([&summary_text2](
chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(summary_text2, MahiResponseStatus::kSuccess);
});
MahiPanelView mahi_view2(ui_controller());
const auto* const summary_label2 = views::AsViewClass<views::Label>(
mahi_view2.GetViewByID(mahi_constants::ViewId::kSummaryLabel));
EXPECT_EQ(summary_text2, summary_label2->GetText());
// Make sure the text is multiline and aligned correctly.
EXPECT_TRUE(summary_label2->GetMultiLine());
EXPECT_EQ(summary_label2->GetHorizontalAlignment(),
gfx::HorizontalAlignment::ALIGN_LEFT);
}
TEST_F(MahiPanelViewTest, ThumbsUpFeedbackButton) {
base::HistogramTester histogram_tester;
// Pressing thumbs up should toggle the button on and update the feedback
// histogram.
EXPECT_CALL(mock_mahi_manager(), OpenFeedbackDialog).Times(0);
IconButton* thumbs_up_button = views::AsViewClass<IconButton>(
panel_view()->GetViewByID(mahi_constants::ViewId::kThumbsUpButton));
LeftClickOn(thumbs_up_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
EXPECT_TRUE(thumbs_up_button->toggled());
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
true, 1);
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
false, 0);
// Pressing thumbs up again should just toggle the button off.
EXPECT_CALL(mock_mahi_manager(), OpenFeedbackDialog).Times(0);
LeftClickOn(thumbs_up_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
EXPECT_FALSE(thumbs_up_button->toggled());
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
true, 1);
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
false, 0);
// Pressing thumbs up should toggle the button on and update the histogram
// again.
EXPECT_CALL(mock_mahi_manager(), OpenFeedbackDialog).Times(0);
LeftClickOn(thumbs_up_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
EXPECT_TRUE(thumbs_up_button->toggled());
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
true, 2);
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
false, 0);
}
TEST_F(MahiPanelViewTest, ThumbsDownFeedbackButton) {
base::HistogramTester histogram_tester;
// Pressing thumbs down the first time should open the feedback dialog, toggle
// the button off and update the feedback histogram.
EXPECT_CALL(mock_mahi_manager(), OpenFeedbackDialog).Times(1);
IconButton* thumbs_down_button = views::AsViewClass<IconButton>(
panel_view()->GetViewByID(mahi_constants::ViewId::kThumbsDownButton));
LeftClickOn(thumbs_down_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
EXPECT_TRUE(thumbs_down_button->toggled());
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
true, 0);
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
false, 1);
// Pressing thumbs down again should just toggle the button off.
EXPECT_CALL(mock_mahi_manager(), OpenFeedbackDialog).Times(0);
LeftClickOn(thumbs_down_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
EXPECT_FALSE(thumbs_down_button->toggled());
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
true, 0);
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
false, 1);
// Pressing thumbs down should toggle the button on and update the histogram
// again.
EXPECT_CALL(mock_mahi_manager(), OpenFeedbackDialog).Times(0);
LeftClickOn(thumbs_down_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
EXPECT_TRUE(thumbs_down_button->toggled());
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
true, 0);
histogram_tester.ExpectBucketCount(mahi_constants::kMahiFeedbackHistogramName,
false, 2);
}
TEST_F(MahiPanelViewTest, CloseButton) {
EXPECT_FALSE(widget()->IsClosed());
LeftClickOn(panel_view()->GetViewByID(mahi_constants::ViewId::kCloseButton));
EXPECT_TRUE(widget()->IsClosed());
}
TEST_F(MahiPanelViewTest, LearnMoreLink) {
auto* learn_more_link =
panel_view()->GetViewByID(mahi_constants::ViewId::kLearnMoreLink);
// Run layout so the link updates its size and becomes clickable.
views::test::RunScheduledLayout(widget());
EXPECT_CALL(new_window_delegate(),
OpenUrl(GURL(chrome::kHelpMeReadWriteLearnMoreURL),
NewWindowDelegate::OpenUrlFrom::kUserInteraction,
NewWindowDelegate::Disposition::kNewForegroundTab));
LeftClickOn(learn_more_link);
Mock::VerifyAndClearExpectations(&new_window_delegate());
}
// TODO(b/330643995): Remove this test after outlines can be shown by default.
TEST_F(MahiPanelViewTest, OutlinesHiddenByDefault) {
EXPECT_FALSE(
panel_view()
->GetViewByID(mahi_constants::ViewId::kOutlinesSectionContainer)
->GetVisible());
}
// Make sure the `PanelContentsContainer` is larger than its contents when the
// contents are short.
TEST_F(MahiPanelViewTest, PanelContentsViewBoundsWithShortSummary) {
ON_CALL(mock_mahi_manager(), GetContentTitle)
.WillByDefault(Return(u"fake content title"));
ON_CALL(mock_mahi_manager(), GetOutlines)
.WillByDefault(mahi_test_util::ReturnDefaultOutlines);
// Configure the mock manager to return a short summary.
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault([](chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(/*summary=*/u"Short summary",
MahiResponseStatus::kSuccess);
});
MahiPanelView mahi_view(ui_controller());
// TODO(b/330643995): After outlines are shown by default, remove this since
// we won't need to explicitly show the outlines section anymore.
mahi_view.GetViewByID(mahi_constants::ViewId::kOutlinesSectionContainer)
->SetVisible(true);
mahi_view.SetPreferredSize(gfx::Size(300, 400));
mahi_view.SizeToPreferredSize();
const int short_content_height =
mahi_view.GetViewByID(mahi_constants::kSummaryOutlinesSection)
->bounds()
.height();
const int short_contents_container_height =
mahi_view.GetViewByID(mahi_constants::kPanelContentsContainer)
->bounds()
.height();
// The container should be larger than the contents when the summary is short.
EXPECT_GT(short_contents_container_height, short_content_height);
}
// Make sure the `PanelContentsContainer` is smaller than its contents when the
// contents are long.
TEST_F(MahiPanelViewTest, PanelContentsViewBoundsWithLongSummary) {
ON_CALL(mock_mahi_manager(), GetContentTitle)
.WillByDefault(Return(u"fake content title"));
ON_CALL(mock_mahi_manager(), GetOutlines)
.WillByDefault(mahi_test_util::ReturnDefaultOutlines);
// Configure the mock manager to return a long summary.
ON_CALL(mock_mahi_manager(), GetSummary).WillByDefault(ReturnLongSummary);
MahiPanelView mahi_view(ui_controller());
// TODO(b/330643995): After outlines are shown by default, remove this since
// we won't need to explicitly show the outlines section anymore.
mahi_view.GetViewByID(mahi_constants::ViewId::kOutlinesSectionContainer)
->SetVisible(true);
mahi_view.SetPreferredSize(gfx::Size(300, 400));
mahi_view.SizeToPreferredSize();
const int long_content_height =
mahi_view.GetViewByID(mahi_constants::kSummaryOutlinesSection)
->bounds()
.height();
const int long_contents_container_height =
mahi_view.GetViewByID(mahi_constants::kPanelContentsContainer)
->bounds()
.height();
// The container should be smaller than the contents when the summary is long.
EXPECT_LT(long_contents_container_height, long_content_height);
}
// Make sure the `PanelContentsContainer` is always sized to occupy the same
// amount of space in the `MahiPanelView` irrespective of its contents size.
TEST_F(MahiPanelViewTest, PanelContentsViewBoundsStayConstant) {
// Create a panel with a short summary.
constexpr gfx::Size kPanelBounds(/*width=*/300, /*height=*/400);
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault([](chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(u"Short summary", MahiResponseStatus::kSuccess);
});
MahiPanelView mahi_view1(ui_controller());
mahi_view1.SetPreferredSize(kPanelBounds);
mahi_view1.SizeToPreferredSize();
// Create another panel with a long summary.
ON_CALL(mock_mahi_manager(), GetSummary).WillByDefault(ReturnLongSummary);
MahiPanelView mahi_view2(ui_controller());
mahi_view2.SetPreferredSize(kPanelBounds);
mahi_view2.SizeToPreferredSize();
const int short_contents_container_height =
mahi_view1.GetViewByID(mahi_constants::kPanelContentsContainer)
->bounds()
.height();
const int long_contents_container_height =
mahi_view2.GetViewByID(mahi_constants::kPanelContentsContainer)
->bounds()
.height();
// The container size should stay constant irrespective of summary length.
EXPECT_EQ(short_contents_container_height, long_contents_container_height);
}
TEST_F(MahiPanelViewTest, LoadingAnimations) {
ResetPanelWidget();
// Config the mock mahi manager to return a summary asyncly.
base::test::TestFuture<void> summary_waiter;
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault([&summary_waiter](
chromeos::MahiManager::MahiSummaryCallback callback) {
ReturnDefaultSummaryAsyncly(
summary_waiter, MahiResponseStatus::kSuccess, std::move(callback));
});
// Config the mock mahi manager to return outlines asyncly.
base::test::TestFuture<void> outlines_waiter;
ON_CALL(mock_mahi_manager(), GetOutlines)
.WillByDefault([&outlines_waiter](
chromeos::MahiManager::MahiOutlinesCallback callback) {
ReturnDefaultOutlinesAsyncly(
outlines_waiter, MahiResponseStatus::kSuccess, std::move(callback));
});
MahiPanelView mahi_view(ui_controller());
// TODO(b/330643995): After outlines are shown by default, remove this since
// we won't need to explicitly show the outlines section anymore.
mahi_view.GetViewByID(mahi_constants::ViewId::kOutlinesSectionContainer)
->SetVisible(true);
const auto* const summary_loading_animated_image = mahi_view.GetViewByID(
mahi_constants::ViewId::kSummaryLoadingAnimatedImage);
const auto* const outlines_loading_animated_image = mahi_view.GetViewByID(
mahi_constants::ViewId::kOutlinesLoadingAnimatedImage);
const auto* const summary_label =
mahi_view.GetViewByID(mahi_constants::ViewId::kSummaryLabel);
const auto* const outlines_container =
mahi_view.GetViewByID(mahi_constants::ViewId::kOutlinesContainer);
// Since the APIs that return summary & outlines are blocked, loading
// animations should play.
EXPECT_TRUE(summary_loading_animated_image->GetVisible());
EXPECT_TRUE(outlines_loading_animated_image->GetVisible());
EXPECT_FALSE(summary_label->GetVisible());
EXPECT_FALSE(outlines_container->GetVisible());
// Wait until summary is returned. Then check:
// 1. The outlines loading animation is still playing.
// 2. `summary_label` is visible.
ASSERT_TRUE(summary_waiter.Wait());
EXPECT_FALSE(summary_loading_animated_image->GetVisible());
EXPECT_TRUE(outlines_loading_animated_image->GetVisible());
EXPECT_TRUE(summary_label->GetVisible());
EXPECT_FALSE(outlines_container->GetVisible());
// Wait until outlines are returned. Both labels should be visible.
ASSERT_TRUE(outlines_waiter.Wait());
EXPECT_FALSE(summary_loading_animated_image->GetVisible());
EXPECT_FALSE(outlines_loading_animated_image->GetVisible());
EXPECT_TRUE(summary_label->GetVisible());
// TODO(b/330643995): Expect TRUE after outlines are shown by default.
EXPECT_FALSE(outlines_container->GetVisible());
}
TEST_F(MahiPanelViewTest, SummaryLoadingAnimationsMetricsRecord) {
// Reset the default panel to avoid unnecessary histogram record.
ResetPanelWidget();
base::HistogramTester histogram_tester;
auto delay_time = base::Milliseconds(100);
// Config the mock mahi manager to return a summary asyncly.
base::test::TestFuture<void> summary_waiter;
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault([&summary_waiter, delay_time](
chromeos::MahiManager::MahiSummaryCallback callback) {
ReturnDefaultSummaryAsyncly(
summary_waiter, MahiResponseStatus::kSuccess, std::move(callback),
/*delay=*/delay_time);
});
MahiPanelView mahi_view(ui_controller());
histogram_tester.ExpectTimeBucketCount(
mahi_constants::kSummaryLoadingTimeHistogramName, delay_time, 0);
// Test that loading time metrics is recorded when summary is loaded.
ASSERT_TRUE(summary_waiter.WaitAndClear());
histogram_tester.ExpectTimeBucketCount(
mahi_constants::kSummaryLoadingTimeHistogramName, delay_time, 1);
// Test that loading time metrics is recorded when the content is refreshed.
ui_controller()->RefreshContents();
ASSERT_TRUE(summary_waiter.Wait());
histogram_tester.ExpectTimeBucketCount(
mahi_constants::kSummaryLoadingTimeHistogramName, delay_time, 2);
}
TEST_F(MahiPanelViewTest, AnswerLoadingAnimationsMetricsRecord) {
base::HistogramTester histogram_tester;
auto delay_time = base::Milliseconds(100);
// Config the mock mahi manager to return an answer asyncly.
base::test::TestFuture<void> answer_waiter;
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
.WillOnce(
[&answer_waiter, delay_time](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(
answer_waiter, MahiResponseStatus::kSuccess,
std::move(callback), /*delay=*/delay_time);
});
SubmitTestQuestion();
histogram_tester.ExpectTimeBucketCount(
mahi_constants::kAnswerLoadingTimeHistogramName, delay_time, 0);
// Test that loading time metrics is recorded when a question is answered.
ASSERT_TRUE(answer_waiter.Wait());
histogram_tester.ExpectTimeBucketCount(
mahi_constants::kAnswerLoadingTimeHistogramName, delay_time, 1);
}
// Tests that pressing on the send button with a valid textfield takes the user
// to the Q&A View and the back to summary outlines button that appears on top
// takes the user back to the main view.
TEST_F(MahiPanelViewTest, TransitionToQuestionAnswerView) {
// Initially the Summary Outlines section is visible.
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
ASSERT_TRUE(summary_outlines_section);
EXPECT_TRUE(summary_outlines_section->GetVisible());
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
ASSERT_TRUE(question_answer_view);
EXPECT_FALSE(question_answer_view->GetVisible());
const auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
ASSERT_TRUE(send_button);
EXPECT_TRUE(send_button->GetVisible());
const auto* const go_to_summary_outlines_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kGoToSummaryOutlinesButton);
ASSERT_TRUE(go_to_summary_outlines_button);
EXPECT_FALSE(go_to_summary_outlines_button->GetVisible());
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
ASSERT_TRUE(question_textfield);
EXPECT_TRUE(question_textfield->GetVisible());
const auto* const go_to_question_answer_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kGoToQuestionAndAnswerButton);
ASSERT_TRUE(go_to_question_answer_button);
EXPECT_FALSE(go_to_question_answer_button->GetVisible());
// Provide a valid input in the textfield so it can be sent as a question.
question_textfield->SetText(u"input");
// Pressing the send button with a valid input in the textfield should take
// the user to the Q&A view.
LeftClickOn(send_button);
EXPECT_FALSE(summary_outlines_section->GetVisible());
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_TRUE(go_to_summary_outlines_button->GetVisible());
EXPECT_TRUE(send_button->GetVisible());
// Run layout so the back to summary outlines button updates its size and
// becomes clickable.
views::test::RunScheduledLayout(widget());
// Pressing the back to summary outlines button should take the user back to
// the main view.
LeftClickOn(go_to_summary_outlines_button);
EXPECT_TRUE(summary_outlines_section->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_FALSE(go_to_summary_outlines_button->GetVisible());
EXPECT_TRUE(send_button->GetVisible());
views::test::RunScheduledLayout(widget());
// "Back to QA" button should now be visible and clickable.
EXPECT_TRUE(go_to_question_answer_button->GetVisible());
LeftClickOn(go_to_question_answer_button);
EXPECT_FALSE(summary_outlines_section->GetVisible());
EXPECT_TRUE(question_answer_view->GetVisible());
// Refreshing the summary contents should clear the Q&A view and make the
// "Back to QA button" invisible.
ui_controller()->RefreshContents();
EXPECT_TRUE(summary_outlines_section->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_FALSE(go_to_question_answer_button->GetVisible());
}
TEST_F(MahiPanelViewTest, ScrollViewContentsDynamicSize) {
auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
auto* const scroll_view_contents =
panel_view()->GetViewByID(mahi_constants::ViewId::kScrollViewContents);
// Make sure the views have different size and their heights exceed the
// visible rect's height of the scroll view.
summary_outlines_section->SetPreferredSize(gfx::Size(80, 300));
question_answer_view->SetPreferredSize(gfx::Size(80, 600));
views::test::RunScheduledLayout(widget());
EXPECT_TRUE(summary_outlines_section->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_EQ(summary_outlines_section->height() +
mahi_constants::kScrollContentsViewBottomPadding,
scroll_view_contents->GetPreferredSize().height());
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
question_textfield->SetText(u"input");
// Transition to Q&A view. Scroll view should change its preferred height (the
// height that the view will take when it is not constrained by `ScrollView`).
LeftClickOn(panel_view()->GetViewByID(
mahi_constants::ViewId::kAskQuestionSendButton));
// Run layout so the views update their size.
views::test::RunScheduledLayout(widget());
EXPECT_FALSE(summary_outlines_section->GetVisible());
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_EQ(question_answer_view->height() +
mahi_constants::kScrollContentsViewBottomPadding,
scroll_view_contents->GetPreferredSize().height());
// Transition back to summary outlines view. Scroll view should change its
// preferred height.(the height that the view will take when it is not
// constrained by `ScrollView`).
LeftClickOn(panel_view()->GetViewByID(
mahi_constants::ViewId::kGoToSummaryOutlinesButton));
views::test::RunScheduledLayout(widget());
EXPECT_TRUE(summary_outlines_section->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_EQ(summary_outlines_section->height() +
mahi_constants::kScrollContentsViewBottomPadding,
scroll_view_contents->GetPreferredSize().height());
}
// Tests that the question textfield accepts user input and creates a text
// bubble with the provided text by pressing the send button or enter.
TEST_F(MahiPanelViewTest, QuestionTextfield_CreateQuestion) {
auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
const std::u16string answer1(u"test answer1");
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[&answer1](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(answer1, MahiResponseStatus::kSuccess);
});
// Set a valid text in the question textfield.
const std::u16string question1(u"question 1");
question_textfield->SetText(question1);
// Pressing the send button should create a question and answer text bubble.
LeftClickOn(send_button);
ASSERT_EQ(2u, question_answer_view->children().size());
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[0]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
question1);
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[1]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
answer1);
// Textfield contents should be cleared after processing input.
EXPECT_TRUE(question_textfield->GetText().empty());
const std::u16string answer2(u"test answer2");
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[&answer2](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(answer2, MahiResponseStatus::kSuccess);
});
// Set another valid text in the question textfield.
const std::u16string question2(u"question 2");
question_textfield->SetText(question2);
// Pressing the "Enter" key while the textfield is focused should create a
// question and answer text bubble.
question_textfield->RequestFocus();
PressEnter();
ASSERT_EQ(question_answer_view->children().size(), 4u);
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[2]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
question2);
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[3]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
answer2);
// Textfield contents should be cleared after processing input.
EXPECT_TRUE(question_textfield->GetText().empty());
}
// Tests that the question textfield does not send requests to the manager while
// it is waiting to load an answer.
TEST_F(MahiPanelViewTest, QuestionTextfield_InputDisabledWhileLoadingAnswer) {
// Send button is initially enabled.
const auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
ASSERT_TRUE(send_button);
EXPECT_TRUE(send_button->GetEnabled());
// Config the mock mahi manager to return an answer asyncly.
base::test::TestFuture<void> answer_waiter;
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
.WillOnce(
[&answer_waiter](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(answer_waiter,
MahiResponseStatus::kSuccess,
std::move(callback));
});
// Set up the textfield to have a valid input.
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
ASSERT_TRUE(question_textfield);
const std::u16string question(u"fake question");
question_textfield->SetText(question);
// After a question is posted and before an answer is loaded, the send button
// should be disabled.
LeftClickOn(send_button);
EXPECT_FALSE(send_button->GetEnabled());
// Set up the textfield to have a valid input again.
question_textfield->SetText(question);
// Attempt sending a question while loading. It should not be processed either
// by attempting to press the send button or by pressing `Enter`.
LeftClickOn(send_button);
question_textfield->RequestFocus();
PressEnter();
// Wait until an answer is loaded. Send button should be enabled again.
ASSERT_TRUE(answer_waiter.Wait());
EXPECT_TRUE(send_button->GetEnabled());
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
// Attempting to send now should process the new input.
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion);
PressEnter();
EXPECT_FALSE(send_button->GetEnabled());
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
}
// Tests that the question textfield does not process empty or blank inputs.
TEST_F(MahiPanelViewTest, QuestionTextfield_EmptyInput) {
// Question textfield is initially empty.
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
ASSERT_TRUE(question_textfield);
EXPECT_TRUE(question_textfield->GetText().empty());
// Attempting to send an empty input should not process the text.
auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
ASSERT_TRUE(send_button);
LeftClickOn(send_button);
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
ASSERT_TRUE(question_answer_view);
EXPECT_TRUE(question_answer_view->children().empty());
// Set a value of whitespace for the textfield.
question_textfield->SetText(u" ");
EXPECT_FALSE(question_textfield->GetText().empty());
// Attempting to send only whitespace should not process the text.
LeftClickOn(send_button);
EXPECT_TRUE(question_answer_view->children().empty());
}
// Tests that the question textfield trims whitespace from the front and back of
// the provided text.
TEST_F(MahiPanelViewTest, QuestionTextfield_TrimWhitespace) {
// Set a text in the textfield with leading and trailing whitespace.
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
ASSERT_TRUE(question_textfield);
question_textfield->SetText(u" leading and trailing ");
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[](const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(/*answer=*/u"fake answer",
MahiResponseStatus::kSuccess);
});
// Sending the text should create a question and answer text bubble.
// The whitespace should be trimmed from the sides.
auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
ASSERT_TRUE(send_button);
LeftClickOn(send_button);
auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
ASSERT_TRUE(question_answer_view);
// TODO(b/334117521): Create a test API and use it instead of using
// `children()`.
ASSERT_EQ(question_answer_view->children().size(), 2u);
EXPECT_EQ(u"leading and trailing",
views::AsViewClass<views::Label>(
question_answer_view->children()[0]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText());
// Set a text in the textfield with whitespace between the string.
const std::u16string question_text(u"whitespace between");
question_textfield->SetText(question_text);
// Sending the text should create a question and answer text bubble.
// The whitespace should not be trimmed if it's not on the sides.
LeftClickOn(send_button);
ASSERT_EQ(question_answer_view->children().size(), 4u);
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[2]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
question_text);
}
// Tests the animated image showing when answer is loading.
TEST_F(MahiPanelViewTest, AnswerLoadingAnimation) {
// Config the mock mahi manager to answer a question asyncly.
base::test::TestFuture<void> answer_waiter;
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[&answer_waiter](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(answer_waiter,
MahiResponseStatus::kSuccess,
std::move(callback));
});
SubmitTestQuestion();
// After a question is posted and before an answer is loaded, the Q&A view
// should show.
auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
ASSERT_TRUE(question_answer_view);
// When the answer is loading, the view should show the question and the
// loading image.
EXPECT_EQ(question_answer_view->children().size(), 2u);
EXPECT_TRUE(question_answer_view->children()[0]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_TRUE(question_answer_view->children()[1]->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
// After the answer is loaded, the view should show the question and answer
// labels, without the loading image.
ASSERT_TRUE(answer_waiter.WaitAndClear());
EXPECT_EQ(question_answer_view->children().size(), 2u);
EXPECT_TRUE(question_answer_view->children()[0]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_TRUE(question_answer_view->children()[1]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_FALSE(panel_view()->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
// Test submitting another question.
SubmitTestQuestion();
EXPECT_EQ(question_answer_view->children().size(), 4u);
EXPECT_TRUE(question_answer_view->children()[2]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_TRUE(question_answer_view->children()[3]->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
// After 2nd answer is loaded.
ASSERT_TRUE(answer_waiter.WaitAndClear());
EXPECT_EQ(question_answer_view->children().size(), 4u);
EXPECT_TRUE(question_answer_view->children()[2]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_TRUE(question_answer_view->children()[3]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_FALSE(panel_view()->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
}
// Tests that the content scroll view scrolls to the bottom or top when being
// laid out based on the following:
// 1. Scroll to bottom when switching to the Q&A view, or when a new
// question/answer is added.
// 2. Scroll to top when switching to the summary & outlines section, or
// when refreshing the summary contents.
TEST_F(MahiPanelViewTest, ScrollViewScrollsAfterLayout) {
ON_CALL(mock_mahi_manager(), GetSummary).WillByDefault(ReturnLongSummary);
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[](const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(/*answer=*/u"answer",
MahiResponseStatus::kSuccess);
});
// Recreate panel widget so it can return a long summary.
CreatePanelWidget();
// Check that the Summary view with a long summary has a scroll bar.
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
ASSERT_TRUE(summary_outlines_section);
EXPECT_TRUE(summary_outlines_section->GetVisible());
auto* const scroll_view = views::AsViewClass<views::ScrollView>(
panel_view()->GetViewByID(mahi_constants::ViewId::kScrollView));
ASSERT_TRUE(scroll_view);
auto* const scroll_bar = scroll_view->vertical_scroll_bar();
ASSERT_TRUE(scroll_bar);
EXPECT_TRUE(scroll_bar->GetVisible());
// Switch to the Q&A view, which should initially not be scrollable.
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
ASSERT_TRUE(question_textfield);
question_textfield->SetText(u"question");
const auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
ASSERT_TRUE(send_button);
LeftClickOn(send_button);
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
ASSERT_TRUE(question_answer_view);
ASSERT_TRUE(question_answer_view->GetVisible());
views::test::RunScheduledLayout(widget());
EXPECT_FALSE(scroll_bar->GetVisible());
// Add enough questions to make the view scrollable.
while (!scroll_bar->GetVisible()) {
question_textfield->SetText(u"question");
LeftClickOn(send_button);
views::test::RunScheduledLayout(widget());
}
// Ensure that the view scrolls down after a new question is added.
int previous_scroll_bar_position = scroll_bar->GetPosition();
question_textfield->SetText(u"question");
LeftClickOn(send_button);
views::test::RunScheduledLayout(widget());
EXPECT_GT(scroll_bar->GetPosition(), previous_scroll_bar_position);
// Ensure that the scroll bar is at the end position.
previous_scroll_bar_position = scroll_bar->GetPosition();
scroll_bar->ScrollByAmount(views::ScrollBar::ScrollAmount::kEnd);
EXPECT_EQ(scroll_bar->GetPosition(), previous_scroll_bar_position);
// The view scrolls up after switching to the summary section.
const auto* const go_to_summary_outlines_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kGoToSummaryOutlinesButton);
ASSERT_TRUE(go_to_summary_outlines_button);
LeftClickOn(go_to_summary_outlines_button);
views::test::RunScheduledLayout(widget());
EXPECT_TRUE(summary_outlines_section->GetVisible());
// Ensure that the scroll bar is visible.
ASSERT_TRUE(scroll_bar->GetVisible());
EXPECT_LT(scroll_bar->GetPosition(), previous_scroll_bar_position);
// Ensure that the scroll bar is at the start position.
previous_scroll_bar_position = scroll_bar->GetPosition();
scroll_bar->ScrollByAmount(views::ScrollBar::ScrollAmount::kStart);
EXPECT_EQ(scroll_bar->GetPosition(), previous_scroll_bar_position);
// Scroll down to the end position to test that refreshing the summary
// contents scrolls to the start position.
scroll_bar->ScrollByAmount(views::ScrollBar::ScrollAmount::kEnd);
EXPECT_GT(scroll_bar->GetPosition(), previous_scroll_bar_position);
previous_scroll_bar_position = scroll_bar->GetPosition();
ui_controller()->RefreshContents();
views::test::RunScheduledLayout(widget());
EXPECT_LT(scroll_bar->GetPosition(), previous_scroll_bar_position);
// Ensure again that the scroll bar is at the start position.
previous_scroll_bar_position = scroll_bar->GetPosition();
scroll_bar->ScrollByAmount(views::ScrollBar::ScrollAmount::kStart);
EXPECT_EQ(scroll_bar->GetPosition(), previous_scroll_bar_position);
}
// Verifies the mahi panel view when loading an answer with an error by
// iterating all possible errors.
TEST_F(MahiPanelViewTest, FailToGetAnswer) {
for (MahiResponseStatus error : GetMahiErrors()) {
// Configs the mock mahi manager to return answer with an `error` asyncly.
base::test::TestFuture<void> answer_waiter;
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
.WillOnce(
[&answer_waiter, error](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(answer_waiter, error,
std::move(callback));
});
base::HistogramTester histogram_tester;
const std::u16string question(u"A question that brings errors");
SubmitTestQuestion(question);
histogram_tester.ExpectBucketCount(
mahi_constants::kMahiQuestionSourceHistogramName,
MahiUiController::QuestionSource::kPanel, 1);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
// After a question is posted and before an answer is loaded, the Q&A view
// should show.
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
CHECK(question_answer_view);
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_TRUE(question_answer_view->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
CHECK(summary_outlines_section);
EXPECT_FALSE(summary_outlines_section->GetVisible());
auto* error_label_view =
views::AsViewClass<views::Label>(panel_view()->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorLabel));
EXPECT_EQ(nullptr, error_label_view);
// Waits until an answer is loaded with an error.
ASSERT_TRUE(answer_waiter.WaitAndClear());
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_FALSE(summary_outlines_section->GetVisible());
// Checks the contents of `error_status_label`. The error should show
// inline.
error_label_view =
views::AsViewClass<views::Label>(panel_view()->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorLabel));
EXPECT_TRUE(error_label_view->GetVisible());
EXPECT_EQ(
error_label_view->GetText(),
l10n_util::GetStringUTF16(mahi_utils::GetErrorStatusViewTextId(error)));
auto* const send_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kAskQuestionSendButton);
EXPECT_TRUE(send_button->GetEnabled());
EXPECT_FALSE(question_answer_view->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
// Configs the mock mahi manager to return an answer in success.
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
.WillOnce(
[&answer_waiter](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(answer_waiter,
MahiResponseStatus::kSuccess,
std::move(callback));
});
// Asks another question.
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
question_textfield->SetText(u"A new question");
LeftClickOn(send_button);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
// Loading animated image should show again.
EXPECT_TRUE(question_answer_view->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
// The error image view and the error label view should still exist.
EXPECT_TRUE(panel_view()->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorImage));
EXPECT_TRUE(panel_view()->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorLabel));
// Waits for the answer to load. Both the error image view and the error
// label view should still exist.
ASSERT_TRUE(answer_waiter.WaitAndClear());
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_TRUE(panel_view()->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorImage));
EXPECT_TRUE(panel_view()->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorLabel));
EXPECT_EQ(question_answer_view->children().size(), 4u);
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[3]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
u"fake answer");
// Configs the mock mahi manager to return answer with an `error` again.
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
.WillOnce(
[&answer_waiter, error](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(answer_waiter, error,
std::move(callback));
});
const std::u16string question2(u"A new question that brings errors");
SubmitTestQuestion(question2);
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
// Shows the new error message inline.
ASSERT_TRUE(answer_waiter.WaitAndClear());
EXPECT_EQ(question_answer_view->children().size(), 6u);
EXPECT_EQ(question_answer_view->children()[5]->children()[0]->GetID(),
mahi_constants::ViewId::kQuestionAnswerErrorImage);
EXPECT_EQ(question_answer_view->children()[5]->children()[1]->GetID(),
mahi_constants::ViewId::kQuestionAnswerErrorLabel);
EXPECT_EQ(
views::AsViewClass<views::Label>(
question_answer_view->children()[5]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerErrorLabel))
->GetText(),
l10n_util::GetStringUTF16(mahi_utils::GetErrorStatusViewTextId(error)));
CreatePanelWidget();
}
}
// Verifies the mahi panel view when loading an answer with a low quota warning.
TEST_F(MahiPanelViewTest, GetAnswerWithLowQuotaWarning) {
// Config the mock mahi manager to return an answer with a low quota warning.
base::test::TestFuture<void> answer_waiter;
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion)
.WillOnce(
[&answer_waiter](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
ReturnDefaultAnswerAsyncly(answer_waiter,
MahiResponseStatus::kLowQuota,
std::move(callback));
});
SubmitTestQuestion();
Mock::VerifyAndClearExpectations(&mock_mahi_manager());
// After a question is posted and before an answer is loaded, the Q&A view
// should show.
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
CHECK(question_answer_view);
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_TRUE(question_answer_view->GetViewByID(
mahi_constants::ViewId::kAnswerLoadingAnimatedImage));
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
CHECK(summary_outlines_section);
EXPECT_FALSE(summary_outlines_section->GetVisible());
const auto* const error_status_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusView);
CHECK(error_status_view);
EXPECT_FALSE(error_status_view->GetVisible());
// Wait until an answer is loaded.
ASSERT_TRUE(answer_waiter.Wait());
// `question_answer_view` should still be visible because
// `MahiResponseStatus::kLowQuota` should not block the answer.
EXPECT_FALSE(error_status_view->GetVisible());
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_FALSE(summary_outlines_section->GetVisible());
// Check the answer bubble.
// TODO(http://b/334117521): Add a test API instead of using `children()`.
ASSERT_EQ(question_answer_view->children().size(), 2u);
EXPECT_EQ(views::AsViewClass<views::Label>(
question_answer_view->children()[1]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel))
->GetText(),
u"fake answer");
}
// Verifies the mahi panel view when loading outlines with an error by
// iterating all possible errors.
TEST_F(MahiPanelViewTest, FailToGetOutlines) {
for (MahiResponseStatus error : GetMahiErrors()) {
// Config the mock mahi manager to return outlines with an `error` asyncly.
base::test::TestFuture<void> outlines_waiter;
EXPECT_CALL(mock_mahi_manager(), GetOutlines)
.WillOnce([&outlines_waiter, error](
chromeos::MahiManager::MahiOutlinesCallback callback) {
ReturnDefaultOutlinesAsyncly(outlines_waiter, error,
std::move(callback));
});
CreatePanelWidget();
Mock::VerifyAndClear(&mock_mahi_manager());
// Before outlines are loaded with an error, the summary & outlines section
// should show.
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
CHECK(question_answer_view);
EXPECT_FALSE(question_answer_view->GetVisible());
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
CHECK(summary_outlines_section);
EXPECT_TRUE(summary_outlines_section->GetVisible());
const auto* const error_status_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusView);
CHECK(error_status_view);
EXPECT_FALSE(error_status_view->GetVisible());
const auto* const error_status_label = views::AsViewClass<views::Label>(
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusLabel));
CHECK(error_status_label);
EXPECT_TRUE(error_status_label->GetText().empty());
// Wait until outlines are loaded with an error.
ASSERT_TRUE(outlines_waiter.Wait());
EXPECT_TRUE(error_status_view->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_FALSE(summary_outlines_section->GetVisible());
// Check the contents of `error_status_label`.
EXPECT_EQ(
error_status_label->GetText(),
l10n_util::GetStringUTF16(mahi_utils::GetErrorStatusViewTextId(error)));
const auto* const retry_link =
panel_view()->GetViewByID(mahi_constants::kErrorStatusRetryLink);
ASSERT_TRUE(retry_link);
EXPECT_EQ(retry_link->GetVisible(),
mahi_utils::CalculateRetryLinkVisible(error));
if (retry_link->GetVisible()) {
// Click the `retry_link`. The mock mahi manager should be requested for a
// summary and outlines again.
views::test::RunScheduledLayout(widget());
GetEventGenerator()->MoveMouseTo(
retry_link->GetBoundsInScreen().CenterPoint());
EXPECT_CALL(mock_mahi_manager(), GetSummary);
EXPECT_CALL(mock_mahi_manager(), GetOutlines);
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion).Times(0);
GetEventGenerator()->ClickLeftButton();
Mock::VerifyAndClear(&mock_mahi_manager());
}
}
}
// Verifies the mahi panel view when loading outlines with a low quota warning.
TEST_F(MahiPanelViewTest, GetOutlinesWithLowQuotaWarning) {
// Config the mock mahi manager to return outlines with a low quota warning.
base::test::TestFuture<void> outlines_waiter;
EXPECT_CALL(mock_mahi_manager(), GetOutlines)
.WillOnce([&outlines_waiter](
chromeos::MahiManager::MahiOutlinesCallback callback) {
ReturnDefaultOutlinesAsyncly(outlines_waiter,
MahiResponseStatus::kLowQuota,
std::move(callback));
});
CreatePanelWidget();
Mock::VerifyAndClear(&mock_mahi_manager());
// Before outlines are loaded, the summary & outlines section should show.
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
CHECK(question_answer_view);
EXPECT_FALSE(question_answer_view->GetVisible());
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
CHECK(summary_outlines_section);
EXPECT_TRUE(summary_outlines_section->GetVisible());
const auto* const error_status_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusView);
CHECK(error_status_view);
EXPECT_FALSE(error_status_view->GetVisible());
// Wait until outlines are loaded.
ASSERT_TRUE(outlines_waiter.Wait());
// `summary_outlines_section` should still be visible because
// `MahiResponseStatus::kLowQuota` should not block the outlines.
// TODO(http://b/330643995): Check the outlines container is visible.
EXPECT_FALSE(error_status_view->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_TRUE(summary_outlines_section->GetVisible());
}
// Verifies the mahi panel view when loading summary with an error by iterating
// all possible errors.
TEST_F(MahiPanelViewTest, FailToGetSummary) {
for (MahiResponseStatus error : GetMahiErrors()) {
// Config the mock mahi manager to return a summary with an `error` asyncly.
base::test::TestFuture<void> summary_waiter;
EXPECT_CALL(mock_mahi_manager(), GetSummary)
.WillOnce([&summary_waiter,
error](chromeos::MahiManager::MahiSummaryCallback callback) {
ReturnDefaultSummaryAsyncly(summary_waiter, error,
std::move(callback));
});
CreatePanelWidget();
Mock::VerifyAndClear(&mock_mahi_manager());
// Before the summary is loaded with an error, the summary & outlines
// section should show.
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
CHECK(question_answer_view);
EXPECT_FALSE(question_answer_view->GetVisible());
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
CHECK(summary_outlines_section);
EXPECT_TRUE(summary_outlines_section->GetVisible());
const auto* const error_status_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusView);
CHECK(error_status_view);
EXPECT_FALSE(error_status_view->GetVisible());
const auto* const error_status_label = views::AsViewClass<views::Label>(
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusLabel));
CHECK(error_status_label);
EXPECT_TRUE(error_status_label->GetText().empty());
// Wait until the summary is loaded with an error.
ASSERT_TRUE(summary_waiter.Wait());
EXPECT_TRUE(error_status_view->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_FALSE(summary_outlines_section->GetVisible());
// Check the contents of `error_status_label`.
EXPECT_EQ(
error_status_label->GetText(),
l10n_util::GetStringUTF16(mahi_utils::GetErrorStatusViewTextId(error)));
const auto* const retry_link =
panel_view()->GetViewByID(mahi_constants::kErrorStatusRetryLink);
ASSERT_TRUE(retry_link);
EXPECT_EQ(retry_link->GetVisible(),
mahi_utils::CalculateRetryLinkVisible(error));
if (retry_link->GetVisible()) {
// Click the `retry_link`. The mock mahi manager should be requested for a
// summary and outlines again.
views::test::RunScheduledLayout(widget());
GetEventGenerator()->MoveMouseTo(
retry_link->GetBoundsInScreen().CenterPoint());
EXPECT_CALL(mock_mahi_manager(), GetSummary);
EXPECT_CALL(mock_mahi_manager(), GetOutlines);
EXPECT_CALL(mock_mahi_manager(), AnswerQuestion).Times(0);
GetEventGenerator()->ClickLeftButton();
Mock::VerifyAndClear(&mock_mahi_manager());
}
}
}
// Verifies the mahi panel view when loading a summary with a low quota warning.
TEST_F(MahiPanelViewTest, GetSummaryWithLowQuotaWarning) {
// Config the mock mahi manager to return a summary with a low quota warning.
base::test::TestFuture<void> summary_waiter;
EXPECT_CALL(mock_mahi_manager(), GetSummary)
.WillOnce([&summary_waiter](
chromeos::MahiManager::MahiSummaryCallback callback) {
ReturnDefaultSummaryAsyncly(
summary_waiter, MahiResponseStatus::kLowQuota, std::move(callback));
});
CreatePanelWidget();
Mock::VerifyAndClear(&mock_mahi_manager());
// Before the summary is loaded, the summary & outlines section should show.
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
CHECK(question_answer_view);
EXPECT_FALSE(question_answer_view->GetVisible());
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
CHECK(summary_outlines_section);
EXPECT_TRUE(summary_outlines_section->GetVisible());
const auto* const error_status_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kErrorStatusView);
CHECK(error_status_view);
EXPECT_FALSE(error_status_view->GetVisible());
// Wait until the summary is loaded.
ASSERT_TRUE(summary_waiter.Wait());
// `summary_outlines_section` should still be visible because
// `MahiResponseStatus::kLowQuota` should not block the summary.
EXPECT_FALSE(error_status_view->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_TRUE(summary_outlines_section->GetVisible());
const auto* const summary_label = GetSummaryLabel(panel_view());
ASSERT_TRUE(summary_label);
EXPECT_TRUE(summary_label->GetVisible());
}
// Tests that calling `RefreshSummaryContents` will update the panel's contents
// with the new data from the manager.
TEST_F(MahiPanelViewTest, RefreshSummaryContents) {
const std::u16string title1(u"Test content title");
const std::u16string summary1(u"Short summary");
const auto icon1(gfx::test::CreateImageSkia(/*size=*/128, SK_ColorBLUE));
ON_CALL(mock_mahi_manager(), GetContentTitle).WillByDefault(Return(title1));
ON_CALL(mock_mahi_manager(), GetContentIcon).WillByDefault(Return(icon1));
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault(
[&summary1](chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(summary1,
chromeos::MahiResponseStatus::kSuccess);
});
MahiPanelView mahi_view(ui_controller());
EXPECT_EQ(GetContentSourceTitle(&mahi_view), title1);
EXPECT_TRUE(gfx::test::AreBitmapsEqual(
*GetContentSourceIcon(&mahi_view).bitmap(),
*image_util::ResizeAndCropImage(icon1, mahi_constants::kContentIconSize)
.bitmap()));
EXPECT_EQ(GetSummaryLabel(&mahi_view)->GetText(), summary1);
const std::u16string title2(u"Test content title 2");
const std::u16string summary2(u"Short summary 2");
const auto icon2(gfx::test::CreateImageSkia(/*size=*/128, SK_ColorRED));
ON_CALL(mock_mahi_manager(), GetContentTitle).WillByDefault(Return(title2));
ON_CALL(mock_mahi_manager(), GetContentIcon).WillByDefault(Return(icon2));
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault(
[&summary2](chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(summary2,
chromeos::MahiResponseStatus::kSuccess);
});
ui_controller()->RefreshContents();
EXPECT_EQ(GetContentSourceTitle(&mahi_view), title2);
EXPECT_TRUE(gfx::test::AreBitmapsEqual(
*GetContentSourceIcon(&mahi_view).bitmap(),
*image_util::ResizeAndCropImage(icon2, mahi_constants::kContentIconSize)
.bitmap()));
EXPECT_EQ(GetSummaryLabel(&mahi_view)->GetText(), summary2);
}
// Tests that clicking the content source button opens the source url
// corresponding to the refreshed content shown on the Mahi panel.
TEST_F(MahiPanelViewTest, ContentSourceButtonUrlAfterRefresh) {
const GURL test_url1("https://www.google.com");
ON_CALL(mock_mahi_manager(), GetContentUrl).WillByDefault(Return(test_url1));
CreatePanelWidget();
EXPECT_CALL(
new_window_delegate(),
OpenUrl(test_url1, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
NewWindowDelegate::Disposition::kSwitchToTab));
LeftClickOn(
panel_view()->GetViewByID(mahi_constants::ViewId::kContentSourceButton));
Mock::VerifyAndClearExpectations(&new_window_delegate());
const GURL test_url2("https://en.wikipedia.org");
ON_CALL(mock_mahi_manager(), GetContentUrl).WillByDefault(Return(test_url2));
ui_controller()->RefreshContents();
EXPECT_CALL(
new_window_delegate(),
OpenUrl(test_url2, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
NewWindowDelegate::Disposition::kSwitchToTab));
LeftClickOn(
panel_view()->GetViewByID(mahi_constants::ViewId::kContentSourceButton));
Mock::VerifyAndClearExpectations(&new_window_delegate());
}
// Tests that refreshing Summary contents will bring the user to the Summary
// View and clear all previously added Q&A text bubbles.
TEST_F(MahiPanelViewTest, RefreshSummaryContents_TransitionToSummaryView) {
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[](const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(u"answer",
chromeos::MahiResponseStatus::kSuccess);
});
const auto* const summary_outlines_section = panel_view()->GetViewByID(
mahi_constants::ViewId::kSummaryOutlinesSection);
const auto* const question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
// Transition to Q&A view by asking a question.
SubmitTestQuestion();
EXPECT_FALSE(summary_outlines_section->GetVisible());
EXPECT_TRUE(question_answer_view->GetVisible());
EXPECT_EQ(question_answer_view->children().size(), 2u);
// Refreshing summary contents should clear all Q&A contents and transition to
// the summary view.
ui_controller()->RefreshContents();
EXPECT_TRUE(summary_outlines_section->GetVisible());
EXPECT_FALSE(question_answer_view->GetVisible());
EXPECT_TRUE(question_answer_view->children().empty());
}
// TODO(crbug.com/333800096): Re-enable this test
TEST_F(MahiPanelViewTest, DISABLED_ClickMetrics) {
base::HistogramTester histogram;
// Learn more button.
histogram.ExpectBucketCount(mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kLearnMoreLink, 0);
LeftClickOn(
panel_view()->GetViewByID(mahi_constants::ViewId::kLearnMoreLink));
histogram.ExpectBucketCount(mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kLearnMoreLink, 1);
histogram.ExpectTotalCount(mahi_constants::kMahiButtonClickHistogramName, 1);
auto* const send_button =
panel_view()->GetViewByID(mahi_constants::ViewId::kAskQuestionSendButton);
auto* const back_to_summary_outlines_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kGoToSummaryOutlinesButton);
auto* const back_to_question_answer_button = panel_view()->GetViewByID(
mahi_constants::ViewId::kGoToQuestionAndAnswerButton);
auto* const question_textfield = views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield));
// Send question button.
// Should not send question when the question text is empty.
views::test::RunScheduledLayout(widget());
LeftClickOn(send_button);
histogram.ExpectBucketCount(
mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kAskQuestionSendButton, 0);
histogram.ExpectTotalCount(mahi_constants::kMahiButtonClickHistogramName, 1);
// Should send question when the question text is not empty.
EXPECT_FALSE(back_to_summary_outlines_button->GetVisible());
const std::u16string question(u"question text");
question_textfield->SetText(question);
LeftClickOn(send_button);
histogram.ExpectBucketCount(
mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kAskQuestionSendButton, 1);
histogram.ExpectTotalCount(mahi_constants::kMahiButtonClickHistogramName, 2);
// Now the back to summary outlines button is visible.
EXPECT_TRUE(back_to_summary_outlines_button->GetVisible());
// Back to summary outlines button.
views::test::RunScheduledLayout(widget());
histogram.ExpectBucketCount(
mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kGoToSummaryOutlinesButton, 0);
LeftClickOn(back_to_summary_outlines_button);
histogram.ExpectBucketCount(
mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kGoToSummaryOutlinesButton, 1);
histogram.ExpectTotalCount(mahi_constants::kMahiButtonClickHistogramName, 3);
// Back to Q&A button.
views::test::RunScheduledLayout(widget());
EXPECT_TRUE(back_to_question_answer_button->GetVisible());
histogram.ExpectBucketCount(
mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kGoToQuestionAndAnswerButton, 0);
LeftClickOn(back_to_question_answer_button);
histogram.ExpectBucketCount(
mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kGoToQuestionAndAnswerButton, 1);
// Close button.
views::test::RunScheduledLayout(widget());
histogram.ExpectBucketCount(mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kCloseButton, 0);
LeftClickOn(panel_view()->GetViewByID(mahi_constants::ViewId::kCloseButton));
histogram.ExpectBucketCount(mahi_constants::kMahiButtonClickHistogramName,
mahi_constants::PanelButton::kCloseButton, 1);
histogram.ExpectTotalCount(mahi_constants::kMahiButtonClickHistogramName, 5);
}
TEST_F(MahiPanelViewTest, UserJourneyTimeMetrics) {
base::HistogramTester histogram;
histogram.ExpectTimeBucketCount(
mahi_constants::kMahiUserJourneyTimeHistogramName, base::Seconds(3),
/*expected_count=*/0);
task_environment()->AdvanceClock(base::Seconds(3));
CreatePanelWidget();
histogram.ExpectTimeBucketCount(
mahi_constants::kMahiUserJourneyTimeHistogramName, base::Seconds(3),
/*expected_count=*/1);
task_environment()->AdvanceClock(base::Minutes(3));
CreatePanelWidget();
histogram.ExpectTimeBucketCount(
mahi_constants::kMahiUserJourneyTimeHistogramName, base::Minutes(3),
/*expected_count=*/1);
task_environment()->AdvanceClock(base::Minutes(10));
CreatePanelWidget();
histogram.ExpectTimeBucketCount(
mahi_constants::kMahiUserJourneyTimeHistogramName, base::Minutes(10),
/*expected_count=*/1);
}
TEST_F(MahiPanelViewTest, ReportQuestionCountWhenRefresh) {
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[](const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(u"answer",
chromeos::MahiResponseStatus::kSuccess);
});
// Ask one question then refresh. Verify that the recorded question count
// in this Mahi session should be one.
base::HistogramTester histogram_tester;
SubmitTestQuestion();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/1,
/*expected_count=*/0);
ui_controller()->RefreshContents();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/1,
/*expected_count=*/1);
// Ask two questions then refresh. Verify that the recorded question count
// in this Mahi session should be two.
SubmitTestQuestion();
SubmitTestQuestion();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/1,
/*expected_count=*/1);
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/2,
/*expected_count=*/0);
ui_controller()->RefreshContents();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/2,
/*expected_count=*/1);
}
TEST_F(MahiPanelViewTest, ReportQuestionCountWhenMahiPanelDestroyed) {
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[](const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(u"answer",
chromeos::MahiResponseStatus::kSuccess);
});
// Ask one question then destroy the Mahi panel. Verify that the recorded
// question count in this Mahi session should be one.
base::HistogramTester histogram_tester;
SubmitTestQuestion();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/1,
/*expected_count=*/0);
ResetPanelWidget();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName, /*sample=*/1,
/*expected_count=*/1);
// Ask two questions then destroy the Mahi panel. Verify that the recorded
// question count in this Mahi session should be two.
CreatePanelWidget();
SubmitTestQuestion();
SubmitTestQuestion();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName,
/*sample=*/1,
/*expected_count=*/1);
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName,
/*sample=*/2,
/*expected_count=*/0);
ResetPanelWidget();
histogram_tester.ExpectBucketCount(
mahi_constants::kQuestionCountPerMahiSessionHistogramName,
/*sample=*/2,
/*expected_count=*/1);
}
// Make sure that summary label is displayed correctly given any kind of text.
TEST_F(MahiPanelViewTest, RandomizedTextSummaryLabel) {
auto random_string = GetRandomString(/*max_words_count=*/500);
ON_CALL(mock_mahi_manager(), GetSummary)
.WillByDefault(
[random_string](chromeos::MahiManager::MahiSummaryCallback callback) {
std::move(callback).Run(random_string,
chromeos::MahiResponseStatus::kSuccess);
});
ui_controller()->RefreshContents();
views::test::RunScheduledLayout(widget());
auto* summary_label = views::AsViewClass<views::Label>(
panel_view()->GetViewByID(mahi_constants::ViewId::kSummaryLabel));
// Make sure the summary label is not clipped.
EXPECT_FALSE(summary_label->IsDisplayTextClipped())
<< "Summary label is clipped with the text: " << random_string;
// Make sure the label is within the bounds of its parent view.
auto* scroll_view = views::AsViewClass<views::ScrollView>(
panel_view()->GetViewByID(mahi_constants::ViewId::kScrollView));
EXPECT_LE(summary_label->width(), scroll_view->GetVisibleRect().width())
<< "Summary label width surpasses scroll view visible width: "
<< random_string;
}
// Make sure the question and answer labels are displayed correctly given any
// kind of texts.
TEST_F(MahiPanelViewTest, RandomizedTextQuestionAnswerLabels) {
auto random_answer = GetRandomString(/*max_words_count=*/100);
ON_CALL(mock_mahi_manager(), AnswerQuestion)
.WillByDefault(
[&random_answer](
const std::u16string& question, bool current_panel_content,
chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
std::move(callback).Run(random_answer,
chromeos::MahiResponseStatus::kSuccess);
});
auto random_question = GetRandomString(/*max_words_count=*/100);
views::AsViewClass<views::Textfield>(
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionTextfield))
->SetText(random_question);
// Pressing the send button should create a question and answer text bubble.
LeftClickOn(panel_view()->GetViewByID(
mahi_constants::ViewId::kAskQuestionSendButton));
views::test::RunScheduledLayout(widget());
auto* question_answer_view =
panel_view()->GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
auto* question_label = views::AsViewClass<views::Label>(
question_answer_view->children()[0]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_FALSE(question_label->IsDisplayTextClipped())
<< "Question label is clipped with the text: " << random_question;
auto* scroll_view = views::AsViewClass<views::ScrollView>(
panel_view()->GetViewByID(mahi_constants::ViewId::kScrollView));
EXPECT_LE(question_label->width(), scroll_view->GetVisibleRect().width())
<< "Question label width surpasses scroll view visible width: "
<< random_answer;
auto* answer_label = views::AsViewClass<views::Label>(
question_answer_view->children()[1]->GetViewByID(
mahi_constants::ViewId::kQuestionAnswerTextBubbleLabel));
EXPECT_FALSE(answer_label->IsDisplayTextClipped())
<< "Answer label is clipped with the text: " << random_answer;
EXPECT_LE(answer_label->width(), scroll_view->GetVisibleRect().width())
<< "Answer label width surpasses scroll view visible width: "
<< random_answer;
}
TEST_F(MahiPanelViewTest, OnlyOneFeedbackButtonCanKeepToggled) {
IconButton* thumbs_up_button = views::AsViewClass<IconButton>(
panel_view()->GetViewByID(mahi_constants::ViewId::kThumbsUpButton));
IconButton* thumbs_down_button = views::AsViewClass<IconButton>(
panel_view()->GetViewByID(mahi_constants::ViewId::kThumbsDownButton));
EXPECT_FALSE(thumbs_up_button->toggled());
EXPECT_FALSE(thumbs_down_button->toggled());
// Pressing thumbs up should toggle the button.
LeftClickOn(thumbs_up_button);
EXPECT_TRUE(thumbs_up_button->toggled());
EXPECT_FALSE(thumbs_down_button->toggled());
// Pressing thumbs down should just toggle down button on and up button off.
LeftClickOn(thumbs_down_button);
EXPECT_TRUE(thumbs_down_button->toggled());
EXPECT_FALSE(thumbs_up_button->toggled());
// Pressing thumbs up should just toggle up button on and down button off.
LeftClickOn(thumbs_up_button);
EXPECT_TRUE(thumbs_up_button->toggled());
EXPECT_FALSE(thumbs_down_button->toggled());
}
TEST_F(MahiPanelViewTest, FeedbackButtonsAllowed) {
PrefService* prefs =
Shell::Get()->session_controller()->GetActivePrefService();
prefs->SetBoolean(prefs::kHmrFeedbackAllowed, false);
CreatePanelWidget();
EXPECT_FALSE(
panel_view()
->GetViewByID(mahi_constants::ViewId::kFeedbackButtonsContainer)
->GetVisible());
EXPECT_EQ(
l10n_util::GetStringFUTF16(
IDS_ASH_MAHI_PANEL_DISCLAIMER_FEEDBACK_DISABLED,
l10n_util::GetStringUTF16(IDS_ASH_MAHI_LEARN_MORE_LINK_LABEL_TEXT)),
static_cast<views::StyledLabel*>(
panel_view()->GetViewByID(mahi_constants::ViewId::kFooterLabel))
->GetText());
prefs->SetBoolean(prefs::kHmrFeedbackAllowed, true);
CreatePanelWidget();
EXPECT_TRUE(
panel_view()
->GetViewByID(mahi_constants::ViewId::kFeedbackButtonsContainer)
->GetVisible());
EXPECT_EQ(l10n_util::GetStringUTF16(IDS_ASH_MAHI_PANEL_DISCLAIMER),
static_cast<views::Label*>(
panel_view()->GetViewByID(mahi_constants::ViewId::kFooterLabel))
->GetText());
}
} // namespace ash