// 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 <optional>
#include <string>
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "base/i18n/base_i18n_switches.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/chromeos/read_write_cards/read_write_cards_ui_controller.h"
#include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h"
#include "chrome/browser/ui/quick_answers/quick_answers_ui_controller.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_view.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/components/quick_answers/public/cpp/constants.h"
#include "chromeos/components/quick_answers/public/cpp/controller/quick_answers_controller.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/test/view_skia_gold_pixel_diff.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace quick_answers {
namespace {
constexpr char kScreenshotPrefix[] = "quick_answers";
constexpr char kTestTitle[] = "TestTitle. A selected text.";
constexpr char kTestQuery[] = "TestQuery";
constexpr char kTestPhoneticsUrl[] = "https://example.com/";
constexpr char kTestDefinition[] =
"TestDefinition. A test definition for TestTitle.";
constexpr char kTextToTranslateLong[] =
"Text to translate. This text is long enough as this can be elided.";
constexpr char kSourceLocaleJaJp[] = "ja-JP";
constexpr char kTranslatedText[] = "Translated text";
constexpr gfx::Rect kContextMenuRectNarrow = {100, 100, 100, 200};
constexpr gfx::Rect kContextMenuRectWide = {100, 100, 300, 200};
using PixelTestParam = std::tuple<bool, bool, bool, Design, bool>;
bool IsDarkMode(const PixelTestParam& pixel_test_param) {
return std::get<0>(pixel_test_param);
}
bool IsRtl(const PixelTestParam& pixel_test_param) {
return std::get<1>(pixel_test_param);
}
bool IsNarrowLayout(const PixelTestParam& pixel_test_param) {
return std::get<2>(pixel_test_param);
}
Design GetDesign(const PixelTestParam& pixel_test_param) {
return std::get<3>(pixel_test_param);
}
bool IsInternal(const PixelTestParam& pixel_test_param) {
return std::get<4>(pixel_test_param);
}
std::string GetDarkModeParamValue(const PixelTestParam& pixel_test_param) {
return IsDarkMode(pixel_test_param) ? "Dark" : "Light";
}
std::string GetRtlParamValue(const PixelTestParam& pixel_test_param) {
return IsRtl(pixel_test_param) ? "Rtl" : "Ltr";
}
std::string GetNarrowLayoutParamValue(const PixelTestParam& pixel_test_param) {
return IsNarrowLayout(pixel_test_param) ? "Narrow" : "Wide";
}
std::optional<std::string> MaybeGetDesignParamValue(
const PixelTestParam& pixel_test_param) {
switch (GetDesign(pixel_test_param)) {
case Design::kCurrent:
return std::nullopt;
case Design::kRefresh:
return "Refresh";
case Design::kMagicBoost:
return "MagicBoost";
}
CHECK(false) << "Invalid design enum class value specified";
}
std::optional<std::string> MaybeInternalParamValue(
const PixelTestParam& pixel_test_param) {
if (IsInternal(pixel_test_param)) {
return "Internal";
}
return std::nullopt;
}
std::string GetParamName(const PixelTestParam& param,
std::string_view separator) {
std::vector<std::string> param_names;
param_names.push_back(GetDarkModeParamValue(param));
param_names.push_back(GetRtlParamValue(param));
param_names.push_back(GetNarrowLayoutParamValue(param));
std::optional<std::string> design_param_value =
MaybeGetDesignParamValue(param);
if (design_param_value) {
param_names.push_back(*design_param_value);
}
std::optional<std::string> internal_param_value =
MaybeInternalParamValue(param);
if (internal_param_value) {
param_names.push_back(*internal_param_value);
}
return base::JoinString(param_names, separator);
}
std::string GenerateParamName(
const testing::TestParamInfo<PixelTestParam>& test_param_info) {
return GetParamName(test_param_info.param, /*separator=*/"");
}
std::string GetScreenshotName(const std::string& test_name,
const PixelTestParam& param) {
return test_name + "." + GetParamName(param, /*separator=*/".");
}
// To run a pixel test locally:
//
// e.g., for only running light mode variants of Loading case:
// browser_tests --gtest_filter=*QuickAnswersPixelTest.Loading/Light*
// --enable-pixel-output-in-tests
// --browser-ui-tests-verify-pixels
// --skia-gold-local-png-write-directory=/tmp/qa_pixel_test
//
// Tip: you can use a screenshot test result for a screenshot of string
// translations.
//
// e.g.,
// MESSAGE_ID=IDS_QUICK_ANSWERS_USER_CONSENT_VIEW_TRY_IT_BUTTON && \
// TEST_NAME=QuickAnswersPixelTestUserConsentView.Dictionary && \
// SCREENSHOT_NAME=UserConsentIntentDictionary && \
// VARIANT=Light.Ltr.Wide.Refresh.ash && \
// autoninja -C out/Default browser_tests && \
// testing/xvfb.py out/Default/browser_tests --gtest_filter=*${TEST_NAME}* \
// --enable-pixel-output-in-tests \
// --browser-ui-tests-verify-pixels \
// --skia-gold-local-png-write-directory=/tmp/qa_pixel_test ; \
// cp /tmp/qa_pixel_test/quick_answers.${SCREENSHOT_NAME}.${VARIANT}.png \
// ./chromeos/chromeos_strings_grd/${MESSAGE_ID}.png
//
// See //docs/translation_screenshots.md on how to upload it.
class QuickAnswersPixelTestBase
: public InProcessBrowserTest,
public testing::WithParamInterface<PixelTestParam> {
public:
void SetUp() override {
// Make sure kQuickAnswersRichCard is disabled. It might be enabled via
// fieldtrial_testing_config.
scoped_feature_list_.InitAndDisableFeature(
chromeos::features::kQuickAnswersRichCard);
InProcessBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
if (IsRtl(GetParam())) {
command_line->AppendSwitchASCII(switches::kForceUIDirection,
switches::kForceDirectionRTL);
}
InProcessBrowserTest::SetUpCommandLine(command_line);
if (!command_line->HasSwitch(switches::kVerifyPixels)) {
GTEST_SKIP() << "A pixel test requires kVerifyPixels flag.";
}
pixel_diff_.emplace(kScreenshotPrefix);
}
void SetUpOnMainThread() override {
ash::DarkLightModeController::Get()->SetDarkModeEnabledForTest(
IsDarkMode(GetParam()));
InProcessBrowserTest::SetUpOnMainThread();
}
protected:
void CreateAndShowQuickAnswersViewForLoading(std::optional<Intent> intent) {
QuickAnswersUiController* quick_answers_ui_controller =
GetQuickAnswersUiController();
ASSERT_TRUE(quick_answers_ui_controller);
chromeos::ReadWriteCardsUiController& read_write_cards_ui_controller =
quick_answers_ui_controller->GetReadWriteCardsUiController();
quick_answers_ui_controller->CreateQuickAnswersViewForPixelTest(
browser()->profile(), kTestQuery, intent,
{
.title = kTestTitle,
.design = GetDesign(GetParam()),
.is_internal = IsInternal(GetParam()),
});
read_write_cards_ui_controller.SetContextMenuBounds(GetContextMenuRect());
ASSERT_TRUE(read_write_cards_ui_controller.widget_for_test())
<< "A widget must be created to show a UI.";
QuickAnswersController* quick_answers_controller =
QuickAnswersController::Get();
ASSERT_TRUE(quick_answers_controller);
quick_answers_controller->SetVisibility(
QuickAnswersVisibility::kQuickAnswersVisible);
}
void CreateAndShowUserConsentView(IntentType intent_type,
const std::u16string& intent_text,
bool use_refreshed_design) {
QuickAnswersUiController* quick_answers_ui_controller =
GetQuickAnswersUiController();
ASSERT_TRUE(quick_answers_ui_controller);
chromeos::ReadWriteCardsUiController& read_write_cards_ui_controller =
quick_answers_ui_controller->GetReadWriteCardsUiController();
QuickAnswersController* quick_answers_controller =
QuickAnswersController::Get();
ASSERT_TRUE(quick_answers_controller);
quick_answers_controller->SetVisibility(QuickAnswersVisibility::kPending);
quick_answers_ui_controller->CreateUserConsentViewForPixelTest(
GetContextMenuRect(), intent_type, intent_text, use_refreshed_design);
read_write_cards_ui_controller.SetContextMenuBounds(GetContextMenuRect());
ASSERT_TRUE(read_write_cards_ui_controller.widget_for_test())
<< "A widget must be created to show a UI.";
}
gfx::Rect GetContextMenuRect() {
return IsNarrowLayout(GetParam()) ? kContextMenuRectNarrow
: kContextMenuRectWide;
}
QuickAnswersUiController* GetQuickAnswersUiController() {
QuickAnswersController* controller = QuickAnswersController::Get();
if (!controller) {
return nullptr;
}
return static_cast<QuickAnswersControllerImpl*>(controller)
->quick_answers_ui_controller();
}
views::Widget* GetWidget() {
return GetQuickAnswersUiController()
->GetReadWriteCardsUiController()
.widget_for_test();
}
base::test::ScopedFeatureList scoped_feature_list_;
std::optional<views::ViewSkiaGoldPixelDiff> pixel_diff_;
};
using QuickAnswersPixelTest = QuickAnswersPixelTestBase;
using QuickAnswersPixelTestInternal = QuickAnswersPixelTestBase;
using QuickAnswersPixelTestLoading = QuickAnswersPixelTestBase;
using QuickAnswersPixelTestResultView = QuickAnswersPixelTestBase;
using QuickAnswersPixelTestUserConsentView = QuickAnswersPixelTestBase;
INSTANTIATE_TEST_SUITE_P(
PixelTest,
QuickAnswersPixelTest,
testing::Combine(testing::Bool(),
testing::Bool(),
testing::Bool(),
testing::Values(Design::kCurrent,
Design::kRefresh,
Design::kMagicBoost),
/*is_internal=*/testing::Values(false)),
&GenerateParamName);
// Separate parameterized test suite for an internal UI to avoid having large
// number of screenshots.
INSTANTIATE_TEST_SUITE_P(
PixelTest,
QuickAnswersPixelTestInternal,
testing::Combine(/*is_dark_mode=*/testing::Values(false),
/*is_rtl=*/testing::Values(false),
/*is_narrow=*/testing::Values(false),
testing::Values(Design::kCurrent,
Design::kRefresh,
Design::kMagicBoost),
/*is_internal=*/testing::Values(true)),
&GenerateParamName);
// `QuickAnswersPixelTestLoading` is for testing loading UI with `kUnknown`
// intent. This is applicable only for `Design::kRefresh`.
INSTANTIATE_TEST_SUITE_P(
PixelTest,
QuickAnswersPixelTestLoading,
testing::Combine(/*is_dark_mode=*/testing::Values(false),
/*is_rtl=*/testing::Values(false),
/*is_narrow=*/testing::Bool(),
testing::Values(Design::kRefresh),
/*is_internal=*/testing::Values(false)),
&GenerateParamName);
// `QuickAnswersPixelTestResultView` is for testing sub text in the result view.
// Use `Design::kRefresh` as a sub text is not used in `Design::kCurrent`.
INSTANTIATE_TEST_SUITE_P(
PixelTest,
QuickAnswersPixelTestResultView,
testing::Combine(/*is_dark_mode=*/testing::Values(false),
/*is_rtl=*/testing::Values(false),
/*is_narrow=*/testing::Bool(),
testing::Values(Design::kRefresh),
/*is_internal=*/testing::Values(false)),
&GenerateParamName);
// `QuickAnswersPixelTestUserConsentView` is for testing a ui text variant of
// `UserConsentView`. More specifically, this is for testing
// `IntentType::kUnknown`, which is used for Linux-ChromeOS.
INSTANTIATE_TEST_SUITE_P(
PixelTest,
QuickAnswersPixelTestUserConsentView,
testing::Combine(/*is_dark_mode=*/testing::Values(false),
/*is_rtl=*/testing::Values(false),
/*is_narrow=*/testing::Values(false),
testing::Values(Design::kCurrent, Design::kRefresh),
/*is_internal=*/testing::Values(false)),
&GenerateParamName);
} // namespace
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTest, Loading) {
// Spread intent types between tests to have better UI test coverage.
CreateAndShowQuickAnswersViewForLoading(Intent::kTranslation);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("Loading", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTest, Result) {
// Spread intent types between tests to have better UI test coverage.
CreateAndShowQuickAnswersViewForLoading(Intent::kDefinition);
StructuredResult structured_result;
structured_result.definition_result = std::make_unique<DefinitionResult>();
structured_result.definition_result->word = kTestTitle;
structured_result.definition_result->sense.definition = kTestDefinition;
structured_result.definition_result->phonetics_info.query_text = kTestQuery;
structured_result.definition_result->phonetics_info.phonetics_audio =
GURL(kTestPhoneticsUrl);
structured_result.definition_result->phonetics_info.tts_audio_enabled = true;
GetQuickAnswersUiController()->RenderQuickAnswersViewWithResult(
structured_result);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("Result", GetParam()), GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTest, Retry) {
// Spread intent types between tests to have better UI test coverage.
CreateAndShowQuickAnswersViewForLoading(Intent::kUnitConversion);
GetQuickAnswersUiController()->ShowRetry();
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("Retry", GetParam()), GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTest, UserConsent) {
Design design = GetDesign(GetParam());
if (design == Design::kMagicBoost) {
GTEST_SKIP()
<< "User consent is handled by MagicBoost UI if MagicBoost is on";
}
CreateAndShowUserConsentView(
IntentType::kDictionary, u"Test",
/*use_refreshed_design=*/design == Design::kRefresh);
// For Narrow layout, we intentionally let it overflow in x-axis. See comments
// in user_consent_view.cc.
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("UserConsent", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestUserConsentView, Unknown) {
Design design = GetDesign(GetParam());
CreateAndShowUserConsentView(
IntentType::kUnknown, u"IntentText",
/*use_refreshed_design=*/design == Design::kRefresh);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("UserConsentIntentUnknown", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestUserConsentView, Dictionary) {
Design design = GetDesign(GetParam());
if (design != Design::kRefresh) {
GTEST_SKIP() << "This test is for testing refreshed UI";
}
CreateAndShowUserConsentView(IntentType::kDictionary, u"unfathomable",
/*use_refreshed_design=*/true);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("UserConsentIntentDictionary", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestUserConsentView, Translation) {
Design design = GetDesign(GetParam());
if (design != Design::kRefresh) {
GTEST_SKIP() << "This test is for testing refreshed UI";
}
CreateAndShowUserConsentView(IntentType::kTranslation, u"信息",
/*use_refreshed_design=*/true);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("UserConsentIntentTranslation", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestUserConsentView, Unit) {
Design design = GetDesign(GetParam());
if (design != Design::kRefresh) {
GTEST_SKIP() << "This test is for testing refreshed UI";
}
CreateAndShowUserConsentView(IntentType::kUnit, u"1kg",
/*use_refreshed_design=*/true);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("UserConsentIntentUnit", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestInternal, InternalUi) {
CreateAndShowQuickAnswersViewForLoading(Intent::kDefinition);
StructuredResult structured_result;
structured_result.definition_result = std::make_unique<DefinitionResult>();
structured_result.definition_result->word = kTestTitle;
structured_result.definition_result->sense.definition = kTestDefinition;
structured_result.definition_result->phonetics_info.query_text = kTestQuery;
structured_result.definition_result->phonetics_info.phonetics_audio =
GURL(kTestPhoneticsUrl);
structured_result.definition_result->phonetics_info.tts_audio_enabled = true;
GetQuickAnswersUiController()->RenderQuickAnswersViewWithResult(
structured_result);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("InternalUi", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestResultView, ElidePrimaryText) {
CreateAndShowQuickAnswersViewForLoading(Intent::kTranslation);
// Translation result uses sub text.
StructuredResult structured_result;
structured_result.translation_result = std::make_unique<TranslationResult>();
structured_result.translation_result->text_to_translate =
kTextToTranslateLong;
structured_result.translation_result->source_locale = kSourceLocaleJaJp;
structured_result.translation_result->translated_text = kTranslatedText;
GetQuickAnswersUiController()->RenderQuickAnswersViewWithResult(
structured_result);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("ElidePrimaryText", GetParam()),
GetWidget()->GetContentsView()));
}
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestResultView, NoSubText) {
CreateAndShowQuickAnswersViewForLoading(Intent::kDefinition);
// No-result case has no sub text.
StructuredResult structured_result;
GetQuickAnswersUiController()->RenderQuickAnswersViewWithResult(
structured_result);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("NoSubText", GetParam()),
GetWidget()->GetContentsView()));
}
// On Linux-ChromeOS, text annotator is not used. It means that loading UI is
// shown with `kUnknown` intent. Note that we are currently using an empty text
// as a placeholder text for `Design::Refresh` on Linux-ChromeOS. Loading UI
// should not be shown with `kUnknown` on prod.
IN_PROC_BROWSER_TEST_P(QuickAnswersPixelTestLoading, Unknown) {
CreateAndShowQuickAnswersViewForLoading(std::nullopt);
EXPECT_TRUE(pixel_diff_->CompareViewScreenshot(
GetScreenshotName("LoadingUnknown", GetParam()),
GetWidget()->GetContentsView()));
}
} // namespace quick_answers