blob: a0bd8578ff72b1883b61114f9a2ad981c8fa78c1 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "ash/shell.h"
#include "ash/system/message_center/ash_message_popup_collection.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/run_loop.h"
#include "base/strings/string_piece_forward.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/quick_answers/quick_answers_browsertest_base.h"
#include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_view.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_view.h"
#include "chrome/browser/ui/quick_answers/ui/user_consent_view.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/components/quick_answers/public/cpp/quick_answers_prefs.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace quick_answers {
namespace {
constexpr char kTestQuery[] = "test";
constexpr int kCursorXToOverlapWithANotification = 600;
constexpr int kCursorYToOverlapWithANotification = 400;
constexpr char kTestNotificationId[] = "id";
constexpr char16_t kTestNotificationTitle[] = u"title";
constexpr char16_t kTestNotificationMessage[] = u"message";
constexpr char16_t kTestNotificationDisplaySource[] = u"display-source";
constexpr char kTestNotificationOriginUrl[] = "https://example.com/";
constexpr char16_t kSourceText[] = u"prodotto";
constexpr char16_t kTranslatedText[] = u"product";
constexpr int kFakeImageWidth = 300;
constexpr int kFakeImageHeight = 300;
constexpr int kAnimationCompletionPollingInterval = 50; // milliseconds
constexpr int kQuickAnswersResultTypeIconSizeDip = 12;
constexpr int kRichAnswersResultTypeIconSizeDip = 16;
gfx::Image CreateFakeImage() {
SkBitmap bitmap;
bitmap.allocN32Pixels(kFakeImageWidth, kFakeImageHeight);
return gfx::Image::CreateFrom1xBitmap(bitmap);
}
void CheckAnimationEnded(views::Widget* widget,
gfx::Rect* rect,
base::RunLoop* run_loop) {
gfx::Rect current_rect = widget->GetWindowBoundsInScreen();
if (current_rect == *rect) {
run_loop->Quit();
return;
}
*rect = current_rect;
}
void WaitAnimationCompletion(views::Widget* widget) {
base::RunLoop run_loop;
base::RepeatingTimer timer;
gfx::Rect rect;
timer.Start(
FROM_HERE, base::Milliseconds(kAnimationCompletionPollingInterval),
base::BindRepeating(&CheckAnimationEnded, widget, &rect, &run_loop));
run_loop.Run();
}
} // namespace
class QuickAnswersBrowserTest : public QuickAnswersBrowserTestBase {
protected:
// This simulates a behavior where a user enables QuickAnswers from Settings.
void SetQuickAnswersEnabled(bool enabled) {
chrome_test_utils::GetProfile(this)->GetPrefs()->SetBoolean(
prefs::kQuickAnswersEnabled, enabled);
}
void SendTestImageNotification() {
message_center::RichNotificationData rich_notification_data;
rich_notification_data.image = CreateFakeImage();
rich_notification_data.never_timeout = true;
message_center::Notification notification(
message_center::NotificationType::NOTIFICATION_TYPE_IMAGE,
kTestNotificationId, kTestNotificationTitle, kTestNotificationMessage,
/*icon=*/ui::ImageModel(), kTestNotificationDisplaySource,
GURL(kTestNotificationOriginUrl), message_center::NotifierId(),
rich_notification_data,
base::MakeRefCounted<message_center::NotificationDelegate>());
NotificationDisplayService::GetForProfile(
chrome_test_utils::GetProfile(this))
->Display(NotificationHandler::Type::TRANSIENT, notification,
/*metadata=*/nullptr);
}
};
IN_PROC_BROWSER_TEST_F(QuickAnswersBrowserTest,
QuickAnswersViewAboveNotification) {
SetQuickAnswersEnabled(true);
views::NamedWidgetShownWaiter quick_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), QuickAnswersView::kWidgetName);
ShowMenuParams params;
params.selected_text = kTestQuery;
params.x = kCursorXToOverlapWithANotification;
params.y = kCursorYToOverlapWithANotification;
ShowMenu(params);
views::Widget* quick_answers_view_widget =
quick_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget != nullptr);
views::NamedWidgetShownWaiter message_popup_widget_waiter(
views::test::AnyWidgetTestPasskey(),
ash::AshMessagePopupCollection::kMessagePopupWidgetName);
SendTestImageNotification();
views::Widget* message_popup_widget =
message_popup_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(message_popup_widget != nullptr);
// The notification is animating. Wait the animation completion before
// checking the bounds.
WaitAnimationCompletion(message_popup_widget);
// Make sure that `QuickAnswersView` overlaps with the notification.
ASSERT_FALSE(
gfx::IntersectRects(message_popup_widget->GetWindowBoundsInScreen(),
quick_answers_view_widget->GetWindowBoundsInScreen())
.IsEmpty());
// TODO(b/239716419): Quick answers UI should be above the notification.
EXPECT_TRUE(message_popup_widget->IsStackedAbove(
quick_answers_view_widget->GetNativeView()));
}
IN_PROC_BROWSER_TEST_F(QuickAnswersBrowserTest,
UserConsentViewAboveNotification) {
views::NamedWidgetShownWaiter user_consent_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), UserConsentView::kWidgetName);
ShowMenuParams params;
params.selected_text = kTestQuery;
params.x = kCursorXToOverlapWithANotification;
params.y = kCursorYToOverlapWithANotification;
ShowMenu(params);
views::Widget* user_consent_view_widget =
user_consent_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(user_consent_view_widget != nullptr);
views::NamedWidgetShownWaiter message_popup_widget_waiter(
views::test::AnyWidgetTestPasskey(),
ash::AshMessagePopupCollection::kMessagePopupWidgetName);
SendTestImageNotification();
views::Widget* message_popup_widget =
message_popup_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(message_popup_widget != nullptr);
// The notification is animating. Wait the animation completion before
// checking the bounds.
WaitAnimationCompletion(message_popup_widget);
// Make sure that `UserConsentView` overlaps with the notification.
ASSERT_FALSE(
gfx::IntersectRects(message_popup_widget->GetWindowBoundsInScreen(),
user_consent_view_widget->GetWindowBoundsInScreen())
.IsEmpty());
// TODO(b/239716419): Quick answers UI should be above the notification.
EXPECT_TRUE(message_popup_widget->IsStackedAbove(
user_consent_view_widget->GetNativeView()));
}
class RichAnswersBrowserTest : public QuickAnswersBrowserTest {
protected:
void SetUpOnMainThread() override {
QuickAnswersBrowserTestBase::SetUpOnMainThread();
SetQuickAnswersEnabled(true);
}
// Simulate a valid QuickAnswer translation response.
std::unique_ptr<QuickAnswersSession> CreateQuickAnswerTranslationResponse() {
std::unique_ptr<quick_answers::QuickAnswer> quick_answer =
std::make_unique<quick_answers::QuickAnswer>();
quick_answer->result_type = ResultType::kTranslationResult;
quick_answer->title.push_back(
std::make_unique<quick_answers::QuickAnswerText>(
l10n_util::GetStringFUTF8(IDS_QUICK_ANSWERS_TRANSLATION_TITLE_TEXT,
kSourceText, u"Italian")));
quick_answer->first_answer_row.push_back(
std::make_unique<quick_answers::QuickAnswerResultText>(
base::UTF16ToUTF8(kTranslatedText)));
std::unique_ptr<TranslationResult> translation_result =
std::make_unique<TranslationResult>();
translation_result->text_to_translate = kSourceText;
translation_result->translated_text = kTranslatedText;
translation_result->target_locale = "en";
translation_result->source_locale = "it";
std::unique_ptr<QuickAnswersSession> quick_answers_session =
std::make_unique<QuickAnswersSession>();
quick_answers_session->quick_answer = std::move(quick_answer);
quick_answers_session->structured_result =
std::make_unique<StructuredResult>();
quick_answers_session->structured_result->translation_result =
std::move(translation_result);
return quick_answers_session;
}
private:
base::test::ScopedFeatureList feature_list_{
chromeos::features::kQuickAnswersRichCard};
};
IN_PROC_BROWSER_TEST_F(RichAnswersBrowserTest,
RichAnswersDismissedOnOutOfBoundsClick) {
ui::test::EventGenerator event_generator(ash::Shell::GetPrimaryRootWindow());
views::NamedWidgetShownWaiter quick_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), QuickAnswersView::kWidgetName);
ShowMenuParams params;
params.selected_text = kTestQuery;
params.x = kCursorXToOverlapWithANotification;
params.y = kCursorYToOverlapWithANotification;
ShowMenu(params);
views::Widget* quick_answers_view_widget =
quick_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget != nullptr);
// Simulate having received a valid QuickAnswer response.
controller()->GetQuickAnswersDelegate()->OnQuickAnswerReceived(
CreateQuickAnswerTranslationResponse());
// Click on the quick answers view to trigger the rich answers view.
views::NamedWidgetShownWaiter rich_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), RichAnswersView::kWidgetName);
event_generator.MoveMouseTo(
quick_answers_view_widget->GetWindowBoundsInScreen().CenterPoint());
event_generator.ClickLeftButton();
// Check that the quick answers view closes when the rich answers view shows.
views::Widget* rich_answers_view_widget =
rich_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget->IsClosed());
ASSERT_TRUE(rich_answers_view_widget != nullptr);
ASSERT_TRUE(controller()->GetVisibilityForTesting() ==
QuickAnswersVisibility::kRichAnswersVisible);
// Click outside the rich answers view window bounds to dismiss it.
gfx::Rect rich_answers_bounds =
rich_answers_view_widget->GetWindowBoundsInScreen();
event_generator.MoveMouseTo(
gfx::Point(rich_answers_bounds.x() / 2, rich_answers_bounds.y() / 2));
event_generator.ClickLeftButton();
// Check that the rich answers view is dismissed.
ASSERT_TRUE(rich_answers_view_widget->IsClosed());
}
IN_PROC_BROWSER_TEST_F(RichAnswersBrowserTest,
RichAnswersNotTriggeredOnInvalidResult) {
ui::test::EventGenerator event_generator(ash::Shell::GetPrimaryRootWindow());
views::NamedWidgetShownWaiter quick_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), QuickAnswersView::kWidgetName);
ShowMenuParams params;
params.selected_text = kTestQuery;
params.x = kCursorXToOverlapWithANotification;
params.y = kCursorYToOverlapWithANotification;
ShowMenu(params);
views::Widget* quick_answers_view_widget =
quick_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget != nullptr);
// Click on the quick answers view. This should not trigger the
// rich answers view since no valid QuickAnswer result is provided.
views::NamedWidgetShownWaiter rich_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), RichAnswersView::kWidgetName);
event_generator.MoveMouseTo(
quick_answers_view_widget->GetWindowBoundsInScreen().CenterPoint());
event_generator.ClickLeftButton();
// Check that all quick answers views are closed.
ASSERT_TRUE(quick_answers_view_widget->IsClosed());
ASSERT_TRUE(controller()->GetVisibilityForTesting() ==
QuickAnswersVisibility::kClosed);
}
IN_PROC_BROWSER_TEST_F(RichAnswersBrowserTest,
CorrespondingResultTypeIconShown) {
ui::test::EventGenerator event_generator(ash::Shell::GetPrimaryRootWindow());
views::NamedWidgetShownWaiter quick_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), QuickAnswersView::kWidgetName);
ShowMenuParams params;
params.selected_text = kTestQuery;
params.x = kCursorXToOverlapWithANotification;
params.y = kCursorYToOverlapWithANotification;
ShowMenu(params);
views::Widget* quick_answers_view_widget =
quick_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(quick_answers_view_widget != nullptr);
// Simulate having received a valid QuickAnswer response.
controller()->GetQuickAnswersDelegate()->OnQuickAnswerReceived(
CreateQuickAnswerTranslationResponse());
// Check that the shown result type icon on the QuickAnswersView
// correctly corresponds to the Quick Answers result type.
QuickAnswersView* quick_answers_view = static_cast<QuickAnswersView*>(
quick_answers_view_widget->GetContentsView());
ui::ImageModel expected_image_model = ui::ImageModel::FromVectorIcon(
omnibox::kAnswerTranslationIcon, cros_tokens::kCrosSysSystemBaseElevated,
/*icon_size=*/kQuickAnswersResultTypeIconSizeDip);
ASSERT_TRUE(quick_answers_view->GetIconImageModelForTesting() ==
expected_image_model);
// Click on the quick answers view to trigger the rich answers view.
views::NamedWidgetShownWaiter rich_answers_view_widget_waiter(
views::test::AnyWidgetTestPasskey(), RichAnswersView::kWidgetName);
event_generator.MoveMouseTo(
quick_answers_view_widget->GetWindowBoundsInScreen().CenterPoint());
event_generator.ClickLeftButton();
views::Widget* rich_answers_view_widget =
rich_answers_view_widget_waiter.WaitIfNeededAndGet();
ASSERT_TRUE(rich_answers_view_widget != nullptr);
// Check that the shown result type icon on the RichAnswersView
// correctly corresponds to the Quick Answers result type.
RichAnswersView* rich_answers_view = static_cast<RichAnswersView*>(
rich_answers_view_widget->GetContentsView());
expected_image_model = ui::ImageModel::FromVectorIcon(
omnibox::kAnswerTranslationIcon, cros_tokens::kCrosSysSystemBaseElevated,
/*icon_size=*/kRichAnswersResultTypeIconSizeDip);
ASSERT_TRUE(rich_answers_view->GetIconImageModelForTesting() ==
expected_image_model);
}
} // namespace quick_answers