// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_theme_provider_impl.h"
#include <memory>
#include "ash/constants/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/color_palette_controller.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/style/mojom/color_scheme.mojom-shared.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_metrics.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_utils.h"
#include "chrome/test/base/chrome_ash_test_base.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/user_manager/scoped_user_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_web_ui.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
namespace ash::personalization_app {
namespace {
constexpr char kFakeTestEmail[] = "fakeemail@personalization";
constexpr char kTestGaiaId[] = "1234567890";
AccountId kAccountId =
AccountId::FromUserEmailGaiaId(kFakeTestEmail, kTestGaiaId);
void AddAndLoginUser() {
ash::FakeChromeUserManager* user_manager =
static_cast<ash::FakeChromeUserManager*>(
user_manager::UserManager::Get());
user_manager->AddUser(kAccountId);
user_manager->LoginUser(kAccountId);
user_manager->SwitchActiveUser(kAccountId);
}
class TestThemeObserver
: public ash::personalization_app::mojom::ThemeObserver {
public:
void OnColorModeChanged(bool dark_mode_enabled) override {
dark_mode_enabled_ = dark_mode_enabled;
}
void OnColorModeAutoScheduleChanged(bool enabled) override {
color_mode_auto_schedule_enabled_ = enabled;
}
void OnColorSchemeChanged(
ash::style::mojom::ColorScheme color_scheme) override {
color_scheme_ = color_scheme;
}
void OnSampleColorSchemesChanged(const std::vector<ash::SampleColorScheme>&
sample_color_schemes) override {
sample_color_schemes_ = sample_color_schemes;
}
void OnStaticColorChanged(std::optional<::SkColor> static_color) override {
static_color_ = static_color;
}
void OnGeolocationPermissionForSystemServicesChanged(bool enabled) override {
geolocation_for_system_enabled_ = enabled;
}
void OnDaylightTimeChanged(const std::u16string& sunrise_time,
const std::u16string& sunset_time) override {
// no-op
}
mojo::PendingRemote<ash::personalization_app::mojom::ThemeObserver>
pending_remote() {
if (theme_observer_receiver_.is_bound()) {
theme_observer_receiver_.reset();
}
return theme_observer_receiver_.BindNewPipeAndPassRemote();
}
std::optional<bool> is_dark_mode_enabled() {
if (!theme_observer_receiver_.is_bound()) {
return std::nullopt;
}
theme_observer_receiver_.FlushForTesting();
return dark_mode_enabled_;
}
bool is_color_mode_auto_schedule_enabled() {
if (theme_observer_receiver_.is_bound()) {
theme_observer_receiver_.FlushForTesting();
}
return color_mode_auto_schedule_enabled_;
}
bool is_geolocation_enabled_for_system_services() {
if (theme_observer_receiver_.is_bound()) {
theme_observer_receiver_.FlushForTesting();
}
return geolocation_for_system_enabled_;
}
ash::style::mojom::ColorScheme GetColorScheme() {
if (theme_observer_receiver_.is_bound()) {
theme_observer_receiver_.FlushForTesting();
}
return color_scheme_;
}
std::optional<SkColor> GetStaticColor() {
if (!theme_observer_receiver_.is_bound()) {
return std::nullopt;
}
theme_observer_receiver_.FlushForTesting();
return static_color_;
}
private:
mojo::Receiver<ash::personalization_app::mojom::ThemeObserver>
theme_observer_receiver_{this};
bool dark_mode_enabled_ = false;
bool color_mode_auto_schedule_enabled_ = false;
bool geolocation_for_system_enabled_ = false;
ash::style::mojom::ColorScheme color_scheme_ =
ash::style::mojom::ColorScheme::kTonalSpot;
std::optional<::SkColor> static_color_ = std::nullopt;
std::vector<ash::SampleColorScheme> sample_color_schemes_;
};
} // namespace
class PersonalizationAppThemeProviderImplTest : public ChromeAshTestBase {
public:
PersonalizationAppThemeProviderImplTest()
: scoped_user_manager_(std::make_unique<ash::FakeChromeUserManager>()),
profile_manager_(TestingBrowserProcess::GetGlobal()) {}
PersonalizationAppThemeProviderImplTest(
const PersonalizationAppThemeProviderImplTest&) = delete;
PersonalizationAppThemeProviderImplTest& operator=(
const PersonalizationAppThemeProviderImplTest&) = delete;
~PersonalizationAppThemeProviderImplTest() override = default;
protected:
// testing::Test:
void SetUp() override {
ChromeAshTestBase::SetUp();
ash::DarkLightModeControllerImpl::Get()->OnActiveUserPrefServiceChanged(
ash::Shell::Get()->session_controller()->GetActivePrefService());
ASSERT_TRUE(profile_manager_.SetUp());
profile_ = profile_manager_.CreateTestingProfile(kFakeTestEmail);
web_contents_ = content::WebContents::Create(
content::WebContents::CreateParams(profile_));
web_ui_.set_web_contents(web_contents_.get());
theme_provider_ =
std::make_unique<PersonalizationAppThemeProviderImpl>(&web_ui_);
theme_provider_->BindInterface(
theme_provider_remote_.BindNewPipeAndPassReceiver());
}
void TearDown() override {
theme_provider_.reset();
ChromeAshTestBase::TearDown();
}
TestingProfile* profile() { return profile_; }
mojo::Remote<ash::personalization_app::mojom::ThemeProvider>*
theme_provider_remote() {
return &theme_provider_remote_;
}
PersonalizationAppThemeProviderImpl* theme_provider() {
return theme_provider_.get();
}
void SetThemeObserver() {
theme_provider_remote_->SetThemeObserver(
test_theme_observer_.pending_remote());
}
std::optional<bool> is_dark_mode_enabled() {
if (theme_provider_remote_.is_bound()) {
theme_provider_remote_.FlushForTesting();
}
return test_theme_observer_.is_dark_mode_enabled();
}
bool is_color_mode_auto_schedule_enabled() {
if (theme_provider_remote_.is_bound()) {
theme_provider_remote_.FlushForTesting();
}
return test_theme_observer_.is_color_mode_auto_schedule_enabled();
}
bool is_geolocation_enabled_for_system_services() {
if (theme_provider_remote_.is_bound()) {
theme_provider_remote_.FlushForTesting();
}
return test_theme_observer_.is_geolocation_enabled_for_system_services();
}
ash::style::mojom::ColorScheme GetColorScheme() {
if (theme_provider_remote_.is_bound()) {
theme_provider_remote_.FlushForTesting();
}
return test_theme_observer_.GetColorScheme();
}
std::optional<SkColor> GetStaticColor() {
if (theme_provider_remote_.is_bound()) {
theme_provider_remote_.FlushForTesting();
}
return test_theme_observer_.GetStaticColor();
}
const base::HistogramTester& histogram_tester() { return histogram_tester_; }
private:
user_manager::ScopedUserManager scoped_user_manager_;
TestingProfileManager profile_manager_;
content::TestWebUI web_ui_;
std::unique_ptr<content::WebContents> web_contents_;
raw_ptr<TestingProfile> profile_;
mojo::Remote<ash::personalization_app::mojom::ThemeProvider>
theme_provider_remote_;
TestThemeObserver test_theme_observer_;
std::unique_ptr<PersonalizationAppThemeProviderImpl> theme_provider_;
base::HistogramTester histogram_tester_;
};
TEST_F(PersonalizationAppThemeProviderImplTest, SetColorModePref) {
SetThemeObserver();
theme_provider()->SetColorModePref(/*dark_mode_enabled=*/false);
EXPECT_FALSE(is_dark_mode_enabled().value());
theme_provider()->SetColorModePref(/*dark_mode_enabled=*/true);
EXPECT_TRUE(is_dark_mode_enabled().value());
histogram_tester().ExpectBucketCount(
kPersonalizationThemeColorModeHistogramName, ColorMode::kDark, 1);
}
TEST_F(PersonalizationAppThemeProviderImplTest, OnColorModeChanged) {
SetThemeObserver();
auto* dark_light_mode_controller = ash::DarkLightModeControllerImpl::Get();
bool dark_mode_enabled = dark_light_mode_controller->IsDarkModeEnabled();
dark_light_mode_controller->ToggleColorMode();
EXPECT_NE(is_dark_mode_enabled().value(), dark_mode_enabled);
dark_light_mode_controller->ToggleColorMode();
EXPECT_EQ(is_dark_mode_enabled().value(), dark_mode_enabled);
}
TEST_F(PersonalizationAppThemeProviderImplTest,
SetColorModeAutoScheduleEnabled) {
SetThemeObserver();
theme_provider_remote()->FlushForTesting();
theme_provider()->SetColorModeAutoScheduleEnabled(/*enabled=*/false);
EXPECT_FALSE(is_color_mode_auto_schedule_enabled());
histogram_tester().ExpectBucketCount(
kPersonalizationThemeColorModeHistogramName, ColorMode::kAuto, 0);
theme_provider()->SetColorModeAutoScheduleEnabled(/*enabled=*/true);
EXPECT_TRUE(is_color_mode_auto_schedule_enabled());
histogram_tester().ExpectBucketCount(
kPersonalizationThemeColorModeHistogramName, ColorMode::kAuto, 1);
}
TEST_F(PersonalizationAppThemeProviderImplTest,
EnableGeolocationForSystemServices) {
SetThemeObserver();
theme_provider()->EnableGeolocationForSystemServices();
EXPECT_TRUE(is_geolocation_enabled_for_system_services());
}
class PersonalizationAppThemeProviderImplJellyTest
: public PersonalizationAppThemeProviderImplTest {
public:
PersonalizationAppThemeProviderImplJellyTest() {
scoped_feature_list_.InitAndEnableFeature(chromeos::features::kJelly);
}
PersonalizationAppThemeProviderImplJellyTest(
const PersonalizationAppThemeProviderImplJellyTest&) = delete;
PersonalizationAppThemeProviderImplJellyTest& operator=(
const PersonalizationAppThemeProviderImplJellyTest&) = delete;
void SetUp() override {
PersonalizationAppThemeProviderImplTest::SetUp();
AddAndLoginUser();
GetSessionControllerClient()->AddUserSession(kAccountId, kFakeTestEmail);
}
protected:
PrefService* GetUserPrefService() {
return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
kAccountId);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(PersonalizationAppThemeProviderImplJellyTest, SetStaticColor) {
SetThemeObserver();
theme_provider_remote()->FlushForTesting();
SkColor color = SK_ColorMAGENTA;
EXPECT_NE(color,
GetUserPrefService()->GetUint64(prefs::kDynamicColorSeedColor));
theme_provider()->SetStaticColor(color);
EXPECT_EQ(color,
GetUserPrefService()->GetUint64(prefs::kDynamicColorSeedColor));
}
TEST_F(PersonalizationAppThemeProviderImplJellyTest,
ObserveStaticColorChanges) {
SetThemeObserver();
theme_provider_remote()->FlushForTesting();
SkColor color = SK_ColorMAGENTA;
EXPECT_NE(color,
GetUserPrefService()->GetUint64(prefs::kDynamicColorSeedColor));
// The static color is set via the UserPrefService in
// ColorPaletteController, and the pref listener is set via the
// ProfilePrefService in the ThemeProvider. In real life, these point to the
// same object, but not in this test. We have to set the pref in both places
// for the pref listener to work. Only the profile prefs will trigger the
// listener, and only the UserPrefService holds the information about which
// pref was updated.
theme_provider()->SetStaticColor(color);
profile()->GetPrefs()->SetUint64(prefs::kDynamicColorSeedColor, color);
EXPECT_EQ(color, GetStaticColor());
}
TEST_F(PersonalizationAppThemeProviderImplJellyTest, SetColorScheme) {
SetThemeObserver();
theme_provider_remote()->FlushForTesting();
auto color_scheme = ash::style::mojom::ColorScheme::kExpressive;
EXPECT_NE((int)color_scheme,
GetUserPrefService()->GetInteger(prefs::kDynamicColorColorScheme));
theme_provider()->SetColorScheme(color_scheme);
EXPECT_EQ((int)color_scheme,
GetUserPrefService()->GetInteger(prefs::kDynamicColorColorScheme));
}
TEST_F(PersonalizationAppThemeProviderImplJellyTest,
ObserveColorSchemeChanges) {
SetThemeObserver();
theme_provider_remote()->FlushForTesting();
auto color_scheme = ash::style::mojom::ColorScheme::kExpressive;
EXPECT_NE((int)color_scheme,
GetUserPrefService()->GetInteger(prefs::kDynamicColorColorScheme));
// The color scheme is set via the UserPrefService in
// ColorPaletteController, and the pref listener is set via the
// ProfilePrefService in the ThemeProvider. In real life, these point to the
// same object, but not in this test. We have to set the pref in both places
// for the pref listener to work. Only the profile prefs will trigger the
// listener, and only the UserPrefService holds the information about which
// pref was updated.
theme_provider()->SetColorScheme(color_scheme);
profile()->GetPrefs()->SetInteger(prefs::kDynamicColorColorScheme,
(int)color_scheme);
EXPECT_EQ(color_scheme, GetColorScheme());
}
TEST_F(PersonalizationAppThemeProviderImplJellyTest,
GenerateSampleColorSchemes) {
SetThemeObserver();
theme_provider_remote()->FlushForTesting();
base::MockOnceCallback<void(const std::vector<ash::SampleColorScheme>&)>
generate_sample_color_schemes_callback;
// Matcher for the vector in the callback.
auto matcher = testing::UnorderedElementsAre(
testing::Field(&SampleColorScheme::scheme,
ash::style::mojom::ColorScheme::kTonalSpot),
testing::Field(&SampleColorScheme::scheme,
ash::style::mojom::ColorScheme::kNeutral),
testing::Field(&SampleColorScheme::scheme,
ash::style::mojom::ColorScheme::kVibrant),
testing::Field(&SampleColorScheme::scheme,
ash::style::mojom::ColorScheme::kExpressive));
base::RunLoop run_loop;
EXPECT_CALL(generate_sample_color_schemes_callback, Run(matcher))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
theme_provider()->GenerateSampleColorSchemes(
generate_sample_color_schemes_callback.Get());
run_loop.Run();
}
} // namespace ash::personalization_app