blob: 0da8d0bee21b114901c965c634dbdd84560a324d [file] [log] [blame]
// Copyright 2012 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/themes/theme_service.h"
#include <cmath>
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "chrome/browser/themes/custom_theme_supplier.h"
#include "chrome/browser/themes/test/theme_service_changed_waiter.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/themes/theme_service_test_utils.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/color/chrome_color_mixers.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/color/color_mixers.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/browser/uninstall_reason.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/mojom/themes.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_manager.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/native_theme/test_native_theme.h"
#include "ui/views/views_features.h"
#if BUILDFLAG(IS_LINUX)
#include "ui/linux/linux_ui.h"
#include "ui/linux/linux_ui_factory.h"
#include "ui/linux/linux_ui_getter.h"
#include "ui/ozone/public/ozone_platform.h"
#endif
namespace {
enum class ContrastMode { kNonHighContrast, kHighContrast };
enum class SystemTheme { kDefault, kCustom };
// A class that ensures any installed extension is uninstalled before it goes
// out of scope. This ensures the temporary directory used to load the
// extension is unlocked and can be deleted.
class ThemeScoper {
public:
ThemeScoper() = default;
ThemeScoper(extensions::ExtensionService* extension_service,
extensions::ExtensionRegistry* extension_registry)
: extension_service_(extension_service),
extension_registry_(extension_registry) {}
ThemeScoper(ThemeScoper&&) noexcept = default;
ThemeScoper& operator=(ThemeScoper&&) = default;
~ThemeScoper() {
if (!extension_id_.empty() &&
extension_registry_->GetInstalledExtension(extension_id_)) {
extension_service_->UninstallExtension(
extension_id_, extensions::UNINSTALL_REASON_FOR_TESTING, nullptr);
}
}
extensions::ExtensionId extension_id() const { return extension_id_; }
void set_extension_id(std::string extension_id) {
extension_id_ = std::move(extension_id);
}
base::FilePath GetTempPath() {
return temp_dir_.CreateUniqueTempDir() ? temp_dir_.GetPath()
: base::FilePath();
}
private:
raw_ptr<extensions::ExtensionService> extension_service_ = nullptr;
raw_ptr<extensions::ExtensionRegistry> extension_registry_ = nullptr;
extensions::ExtensionId extension_id_;
base::ScopedTempDir temp_dir_;
};
#if BUILDFLAG(IS_LINUX)
class LinuxUiGetterImpl : public ui::LinuxUiGetter {
public:
explicit LinuxUiGetterImpl(bool use_system_theme)
: linux_ui_theme_(use_system_theme ? ui::GetDefaultLinuxUiTheme()
: nullptr) {}
~LinuxUiGetterImpl() override = default;
ui::LinuxUiTheme* GetForWindow(aura::Window* window) override {
return linux_ui_theme_;
}
ui::LinuxUiTheme* GetForProfile(Profile* profile) override {
return linux_ui_theme_;
}
private:
const raw_ptr<ui::LinuxUiTheme> linux_ui_theme_;
};
#endif
} // namespace
namespace theme_service_internal {
class ThemeServiceTest : public extensions::ExtensionServiceTestBase {
public:
ThemeServiceTest() = default;
~ThemeServiceTest() override = default;
void SetUp() override {
extensions::ExtensionServiceTestBase::SetUp();
InitializeExtensionService(ExtensionServiceInitParams());
service_->Init();
registry_ = extensions::ExtensionRegistry::Get(profile());
ASSERT_TRUE(registry_);
theme_service_ = ThemeServiceFactory::GetForProfile(profile());
ASSERT_TRUE(theme_service_);
pref_service_ = profile_->GetPrefs();
ASSERT_TRUE(pref_service_);
}
ThemeScoper LoadUnpackedTheme(const std::string& source_file_path =
"extensions/theme_minimal/manifest.json") {
ThemeScoper scoper(service_, registry_);
test::ThemeServiceChangedWaiter waiter(theme_service_);
base::FilePath temp_dir = scoper.GetTempPath();
base::FilePath dst_manifest_path = temp_dir.AppendASCII("manifest.json");
base::FilePath test_data_dir;
EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
base::FilePath src_manifest_path =
test_data_dir.AppendASCII(source_file_path);
EXPECT_TRUE(base::CopyFile(src_manifest_path, dst_manifest_path));
scoped_refptr<extensions::UnpackedInstaller> installer(
extensions::UnpackedInstaller::Create(service_));
extensions::TestExtensionRegistryObserver observer(registry_);
installer->Load(temp_dir);
std::string extenson_id = observer.WaitForExtensionLoaded()->id();
scoper.set_extension_id(extenson_id);
waiter.WaitForThemeChanged();
// Make sure RegisterClient calls for storage are finished to avoid flaky
// crashes in QuotaManagerImpl::RegisterClient on test shutdown.
// TODO(crbug.com/40170877) : Remove this when 1182630 is fixed.
extensions::util::GetStoragePartitionForExtensionId(extenson_id, profile());
task_environment()->RunUntilIdle();
return scoper;
}
// Update the theme with |extension_id|.
void UpdateUnpackedTheme(const std::string& extension_id) {
const base::FilePath& path =
registry_->GetInstalledExtension(extension_id)->path();
scoped_refptr<extensions::UnpackedInstaller> installer(
extensions::UnpackedInstaller::Create(service_));
extensions::TestExtensionRegistryObserver observer(registry_);
installer->Load(path);
observer.WaitForExtensionInstalled();
// Let the ThemeService finish creating the theme pack.
base::RunLoop().RunUntilIdle();
}
bool IsExtensionDisabled(const extensions::ExtensionId& id) const {
return registry_->disabled_extensions().GetByID(id);
}
protected:
ui::TestNativeTheme test_native_theme_;
raw_ptr<extensions::ExtensionRegistry> registry_ = nullptr;
raw_ptr<PrefService> pref_service_ = nullptr;
raw_ptr<ThemeService> theme_service_ = nullptr;
};
class ColorProviderTest
: public ThemeServiceTest,
public testing::WithParamInterface<
std::tuple<ui::NativeTheme::ColorScheme, ContrastMode, SystemTheme>> {
public:
ColorProviderTest() = default;
// ThemeServiceTest:
void SetUp() override {
ThemeServiceTest::SetUp();
// Only perform mixer initialization once.
static bool initialized_mixers = false;
if (!initialized_mixers) {
#if BUILDFLAG(IS_LINUX)
// Ensures LinuxUi is configured on supported linux platforms.
// Initializing the toolkit also adds the native toolkit ColorMixers.
ui::OzonePlatform::InitParams ozone_params;
ozone_params.single_process = true;
ui::OzonePlatform::InitializeForUI(ozone_params);
auto* linux_ui = ui::GetDefaultLinuxUi();
ASSERT_TRUE(linux_ui);
ui::LinuxUi::SetInstance(linux_ui);
#endif // BUILDFLAG(IS_LINUX)
// Add the components and Chrome ColorMixers after native ColorMixers.
ui::ColorProviderManager::Get().AppendColorProviderInitializer(
base::BindRepeating(color::AddComponentsColorMixers));
ui::ColorProviderManager::Get().AppendColorProviderInitializer(
base::BindRepeating(AddChromeColorMixers));
initialized_mixers = true;
}
#if BUILDFLAG(IS_LINUX)
linux_ui_getter_ = std::make_unique<LinuxUiGetterImpl>(
std::get<SystemTheme>(GetParam()) == SystemTheme::kCustom);
#endif // BUILDFLAG(IS_LINUX)
const auto param_tuple = GetParam();
auto color_scheme = std::get<ui::NativeTheme::ColorScheme>(param_tuple);
const auto contrast_mode = std::get<ContrastMode>(param_tuple);
const auto system_theme = std::get<SystemTheme>(param_tuple);
// ThemeService tracks the global NativeTheme instance for native UI. Update
// this to reflect test params and propagate any updates.
native_theme_ = ui::NativeTheme::GetInstanceForNativeUi();
#if BUILDFLAG(IS_LINUX)
if (system_theme == SystemTheme::kCustom) {
native_theme_ = ui::GetDefaultLinuxUiTheme()->GetNativeTheme();
}
#endif
original_forced_colors_ = native_theme_->InForcedColorsMode();
original_preferred_contrast_ = native_theme_->GetPreferredContrast();
original_should_use_dark_colors_ = native_theme_->ShouldUseDarkColors();
const bool high_contrast = contrast_mode == ContrastMode::kHighContrast;
#if BUILDFLAG(IS_WIN)
if (high_contrast) {
color_scheme = ui::NativeTheme::ColorScheme::kPlatformHighContrast;
}
native_theme_->set_forced_colors(high_contrast);
#endif // BUILDFLAG(IS_WIN)
native_theme_->SetPreferredContrast(
high_contrast ? ui::NativeTheme::PreferredContrast::kMore
: ui::NativeTheme::PreferredContrast::kNoPreference);
native_theme_->set_use_dark_colors(color_scheme ==
ui::NativeTheme::ColorScheme::kDark);
// If native_theme_ has changed, call
// NativeTheme::NotifyOnNativeThemeUpdated to notify observers that the
// NativeTheme has been updated so that the ThemeService will know to update
// its ThemeSupplier to match the NativeTheme. The ColorProvider cache will
// also be reset.
if (original_forced_colors_ != native_theme_->InForcedColorsMode() ||
original_preferred_contrast_ != native_theme_->GetPreferredContrast() ||
original_should_use_dark_colors_ !=
native_theme_->ShouldUseDarkColors()) {
native_theme_->NotifyOnNativeThemeUpdated();
}
// Update ThemeService to use the system theme if necessary.
if (system_theme == SystemTheme::kCustom) {
theme_service_->UseSystemTheme();
} else {
theme_service_->UseDefaultTheme();
}
}
void TearDown() override {
// Restore the original NativeTheme parameters.
native_theme_->set_forced_colors(original_forced_colors_);
native_theme_->SetPreferredContrast(original_preferred_contrast_);
native_theme_->set_use_dark_colors(original_should_use_dark_colors_);
native_theme_->NotifyOnNativeThemeUpdated();
ThemeServiceTest::TearDown();
}
static std::string ParamInfoToString(
::testing::TestParamInfo<
std::tuple<ui::NativeTheme::ColorScheme, ContrastMode, SystemTheme>>
param_info) {
auto param_tuple = param_info.param;
return ColorSchemeToString(
std::get<ui::NativeTheme::ColorScheme>(param_tuple)) +
ContrastModeToString(std::get<ContrastMode>(param_tuple)) +
SystemThemeToString(std::get<SystemTheme>(param_tuple));
}
SkColor GetColor(ui::ColorId id) const {
const auto* const color_provider =
ui::ColorProviderManager::Get().GetColorProviderFor(
native_theme_->GetColorProviderKey(nullptr));
return color_provider->GetColor(id);
}
private:
static std::string ColorSchemeToString(ui::NativeTheme::ColorScheme scheme) {
switch (scheme) {
case ui::NativeTheme::ColorScheme::kDefault:
NOTREACHED()
<< "Cannot unit test kDefault as it depends on machine state.";
return "InvalidColorScheme";
case ui::NativeTheme::ColorScheme::kLight:
return "kLight";
case ui::NativeTheme::ColorScheme::kDark:
return "kDark";
case ui::NativeTheme::ColorScheme::kPlatformHighContrast:
return "kPlatformHighContrast";
}
}
static std::string ContrastModeToString(ContrastMode contrast_mode) {
return contrast_mode == ContrastMode::kHighContrast ? "HighContrast" : "";
}
static std::string SystemThemeToString(SystemTheme system_theme) {
return system_theme == SystemTheme::kCustom ? "SystemTheme" : "";
}
// Store the parameter values of the global NativeTheme for UI instance
// configured during SetUp() to check if an update should be propagated and
// to restore the NativeTheme to its original state in TearDown().
bool original_forced_colors_ = false;
ui::NativeTheme::PreferredContrast original_preferred_contrast_ =
ui::NativeTheme::PreferredContrast::kNoPreference;
bool original_should_use_dark_colors_ = false;
raw_ptr<ui::NativeTheme> native_theme_;
#if BUILDFLAG(IS_LINUX)
std::unique_ptr<ui::LinuxUiGetter> linux_ui_getter_;
#endif
};
INSTANTIATE_TEST_SUITE_P(
,
ColorProviderTest,
::testing::Combine(::testing::Values(ui::NativeTheme::ColorScheme::kLight,
ui::NativeTheme::ColorScheme::kDark),
::testing::Values(ContrastMode::kNonHighContrast,
ContrastMode::kHighContrast),
::testing::Values(SystemTheme::kDefault,
SystemTheme::kCustom)),
ColorProviderTest::ParamInfoToString);
// Installs then uninstalls a theme and makes sure that the ThemeService
// reverts to the default theme after the uninstall.
TEST_F(ThemeServiceTest, ThemeInstallUninstall) {
ThemeScoper scoper = LoadUnpackedTheme();
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingExtensionTheme());
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
// Now uninstall the extension, should revert to the default theme.
service_->UninstallExtension(
scoper.extension_id(), extensions::UNINSTALL_REASON_FOR_TESTING, nullptr);
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(theme_service_->UsingExtensionTheme());
}
// Test that a theme extension is disabled when not in use. A theme may be
// installed but not in use if it there is an infobar to revert to the previous
// theme.
TEST_F(ThemeServiceTest, DisableUnusedTheme) {
// 1) Installing a theme should disable the previously active theme.
ThemeScoper scoper1 = LoadUnpackedTheme();
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_EQ(scoper1.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper1.extension_id()));
// Create theme reinstaller to prevent the current theme from being
// uninstalled.
std::unique_ptr<ThemeService::ThemeReinstaller> reinstaller =
theme_service_->BuildReinstallerForCurrentTheme();
ThemeScoper scoper2 = LoadUnpackedTheme();
EXPECT_EQ(scoper2.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper2.extension_id()));
EXPECT_TRUE(IsExtensionDisabled(scoper1.extension_id()));
// 2) Enabling a disabled theme extension should swap the current theme.
{
test::ThemeServiceChangedWaiter waiter(theme_service_);
service_->EnableExtension(scoper1.extension_id());
waiter.WaitForThemeChanged();
}
EXPECT_EQ(scoper1.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper1.extension_id()));
EXPECT_TRUE(IsExtensionDisabled(scoper2.extension_id()));
// 3) Using RevertToExtensionTheme() with a disabled theme should enable and
// set the theme. This is the case when the user reverts to the previous theme
// via an infobar.
{
test::ThemeServiceChangedWaiter waiter(theme_service_);
theme_service_->RevertToExtensionTheme(scoper2.extension_id());
waiter.WaitForThemeChanged();
}
EXPECT_EQ(scoper2.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper2.extension_id()));
EXPECT_TRUE(IsExtensionDisabled(scoper1.extension_id()));
// 4) Disabling the current theme extension should revert to the default theme
// and disable any installed theme extensions.
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
service_->DisableExtension(scoper2.extension_id(),
extensions::disable_reason::DISABLE_USER_ACTION);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(service_->IsExtensionEnabled(scoper1.extension_id()));
EXPECT_FALSE(service_->IsExtensionEnabled(scoper2.extension_id()));
}
// Test the ThemeService's behavior when a theme is upgraded.
TEST_F(ThemeServiceTest, ThemeUpgrade) {
std::unique_ptr<ThemeService::ThemeReinstaller> reinstaller =
theme_service_->BuildReinstallerForCurrentTheme();
ThemeScoper scoper1 = LoadUnpackedTheme();
ThemeScoper scoper2 = LoadUnpackedTheme();
// Test the initial state.
EXPECT_TRUE(IsExtensionDisabled(scoper1.extension_id()));
EXPECT_EQ(scoper2.extension_id(), theme_service_->GetThemeID());
// 1) Upgrading the current theme should not revert to the default theme.
test::ThemeServiceChangedWaiter waiter(theme_service_);
UpdateUnpackedTheme(scoper2.extension_id());
// The ThemeService should have sent an theme change notification even though
// the id of the current theme did not change.
waiter.WaitForThemeChanged();
EXPECT_EQ(scoper2.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(IsExtensionDisabled(scoper1.extension_id()));
// 2) Upgrading a disabled theme should not change the current theme.
UpdateUnpackedTheme(scoper1.extension_id());
EXPECT_EQ(scoper2.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(IsExtensionDisabled(scoper1.extension_id()));
}
TEST_F(ThemeServiceTest, NTPLogoAlternate) {
// TODO(crbug.com/40666609): Fix ScopedTempDir deletion errors on Win.
const ui::ThemeProvider& theme_provider =
ThemeService::GetThemeProviderForProfile(profile());
{
ThemeScoper scoper =
LoadUnpackedTheme("extensions/theme_grey_ntp/manifest.json");
// When logo alternate is not specified and ntp is grey, logo should be
// colorful.
EXPECT_EQ(0, theme_provider.GetDisplayProperty(
ThemeProperties::NTP_LOGO_ALTERNATE));
}
{
ThemeScoper scoper =
LoadUnpackedTheme("extensions/theme_grey_ntp_white_logo/manifest.json");
// Logo alternate should match what is specified in the manifest.
EXPECT_EQ(1, theme_provider.GetDisplayProperty(
ThemeProperties::NTP_LOGO_ALTERNATE));
}
{
ThemeScoper scoper = LoadUnpackedTheme(
"extensions/theme_color_ntp_white_logo/manifest.json");
// When logo alternate is not specified and ntp is colorful, logo should be
// white.
EXPECT_EQ(1, theme_provider.GetDisplayProperty(
ThemeProperties::NTP_LOGO_ALTERNATE));
}
{
ThemeScoper scoper = LoadUnpackedTheme(
"extensions/theme_color_ntp_colorful_logo/manifest.json");
// Logo alternate should match what is specified in the manifest.
EXPECT_EQ(0, theme_provider.GetDisplayProperty(
ThemeProperties::NTP_LOGO_ALTERNATE));
}
}
// crbug.com/468280
TEST_F(ThemeServiceTest, UninstallThemeWhenNoReinstallers) {
ThemeScoper scoper1 = LoadUnpackedTheme();
ASSERT_EQ(scoper1.extension_id(), theme_service_->GetThemeID());
ThemeScoper scoper2;
{
// Show an infobar.
std::unique_ptr<ThemeService::ThemeReinstaller> reinstaller =
theme_service_->BuildReinstallerForCurrentTheme();
// Install another theme. The first extension shouldn't be uninstalled yet
// as it should be possible to revert to it.
scoper2 = LoadUnpackedTheme();
EXPECT_TRUE(IsExtensionDisabled(scoper1.extension_id()));
EXPECT_EQ(scoper2.extension_id(), theme_service_->GetThemeID());
test::ThemeServiceChangedWaiter waiter(theme_service_);
reinstaller->Reinstall();
waiter.WaitForThemeChanged();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(IsExtensionDisabled(scoper2.extension_id()));
EXPECT_EQ(scoper1.extension_id(), theme_service_->GetThemeID());
}
// extension 2 should get uninstalled as no reinstallers are in scope.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(registry_->GetInstalledExtension(scoper2.extension_id()));
}
TEST_F(ThemeServiceTest, BuildFromColorTest) {
// Set theme from color.
theme_service_->UseDefaultTheme();
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(theme_service_->UsingAutogeneratedTheme());
theme_service_->BuildAutogeneratedThemeFromColor(
SkColorSetRGB(100, 100, 100));
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
// Set theme from data pack and then override it with theme from color.
ThemeScoper scoper = LoadUnpackedTheme();
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(theme_service_->UsingAutogeneratedTheme());
base::FilePath path =
pref_service_->GetFilePath(prefs::kCurrentThemePackFilename);
EXPECT_FALSE(path.empty());
theme_service_->BuildAutogeneratedThemeFromColor(
SkColorSetRGB(100, 100, 100));
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_EQ(ThemeService::kAutogeneratedThemeID, theme_service_->GetThemeID());
path = pref_service_->GetFilePath(prefs::kCurrentThemePackFilename);
EXPECT_TRUE(path.empty());
}
TEST_F(ThemeServiceTest, BuildFromColor_DisableExtensionTest) {
ThemeScoper scoper = LoadUnpackedTheme();
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper.extension_id()));
// Setting autogenerated theme should disable previous theme.
theme_service_->BuildAutogeneratedThemeFromColor(
SkColorSetRGB(100, 100, 100));
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(service_->IsExtensionEnabled(scoper.extension_id()));
}
TEST_F(ThemeServiceTest, UseDefaultTheme_DisableExtensionTest) {
ThemeScoper scoper = LoadUnpackedTheme();
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper.extension_id()));
// Resetting to default theme should disable previous theme.
theme_service_->UseDefaultTheme();
EXPECT_FALSE(service_->IsExtensionEnabled(scoper.extension_id()));
}
// Test that setting theme to default resets the NTP theme as well.
TEST_F(ThemeServiceTest, UseDefaultTheme_DisableNtpThemeTest) {
// Turn on Customize Chrome Side Panel.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(features::kCustomizeChromeSidePanel);
base::Value::Dict test_background_info;
test_background_info.Set("test_data", "foo");
pref_service_->SetDict(prefs::kNtpCustomBackgroundDict,
std::move(test_background_info));
const base::Value::Dict& background_info_with_theme =
pref_service_->GetDict(prefs::kNtpCustomBackgroundDict);
const base::Value* test_data = background_info_with_theme.Find("test_data");
EXPECT_NE(test_data, nullptr);
EXPECT_NE(test_data->GetIfString(), nullptr);
theme_service_->UseDefaultTheme();
const base::Value::Dict& background_info_without_theme =
pref_service_->GetDict(prefs::kNtpCustomBackgroundDict);
EXPECT_EQ(background_info_without_theme.Find("test_data"), nullptr);
}
TEST_P(ColorProviderTest, OmniboxContrast) {
// TODO(crbug.com/40847629): Windows platform high contrast colors are
// not sufficiently high-contrast to pass this test.
#if BUILDFLAG(IS_WIN)
if (std::get<ContrastMode>(GetParam()) == ContrastMode::kHighContrast) {
return;
}
#endif
#if BUILDFLAG(IS_LINUX)
// TODO(crbug.com/41494383): Linux platform native dark mode colors aren't
// sufficiently high contrast to pass.
if (std::get<SystemTheme>(GetParam()) == SystemTheme::kCustom &&
std::get<ui::NativeTheme::ColorScheme>(GetParam()) ==
ui::NativeTheme::ColorScheme::kDark) {
return;
}
#endif
constexpr ui::ColorId contrasting_ids[][2] = {
{kColorOmniboxResultsIcon, kColorOmniboxResultsBackground},
{kColorOmniboxResultsIcon, kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxResultsIconSelected,
kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxResultsTextSelected,
kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxResultsTextDimmed, kColorOmniboxResultsBackground},
{kColorOmniboxResultsTextDimmed, kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxResultsTextDimmedSelected,
kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxResultsTextNegative, kColorOmniboxResultsBackground},
{kColorOmniboxResultsTextNegative, kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxResultsTextNegativeSelected,
kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxResultsTextPositive, kColorOmniboxResultsBackground},
{kColorOmniboxResultsTextPositive, kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxResultsTextPositiveSelected,
kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxResultsTextSecondary, kColorOmniboxResultsBackground},
{kColorOmniboxResultsTextSecondary,
kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxResultsTextSecondarySelected,
kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxResultsUrl, kColorOmniboxResultsBackground},
{kColorOmniboxResultsUrl, kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxResultsUrlSelected, kColorOmniboxResultsBackgroundSelected},
{kColorOmniboxBubbleOutline, kColorOmniboxResultsBackground},
{kColorOmniboxBubbleOutlineExperimentalKeywordMode,
kColorOmniboxResultsBackground},
{kColorOmniboxSecurityChipDefault, kColorToolbarBackgroundSubtleEmphasis},
{kColorOmniboxSecurityChipDefault,
kColorToolbarBackgroundSubtleEmphasisHovered},
{kColorOmniboxSecurityChipSecure, kColorToolbarBackgroundSubtleEmphasis},
{kColorOmniboxSecurityChipSecure,
kColorToolbarBackgroundSubtleEmphasisHovered},
{kColorOmniboxSecurityChipDangerous,
kColorToolbarBackgroundSubtleEmphasis},
{kColorOmniboxSecurityChipDangerous,
kColorToolbarBackgroundSubtleEmphasisHovered},
{kColorOmniboxKeywordSelected, kColorToolbarBackgroundSubtleEmphasis},
{kColorOmniboxKeywordSelected,
kColorToolbarBackgroundSubtleEmphasisHovered},
{kColorOmniboxText, kColorToolbarBackgroundSubtleEmphasis},
{kColorOmniboxText, kColorToolbarBackgroundSubtleEmphasisHovered},
{kColorOmniboxText, kColorOmniboxResultsBackground},
{kColorOmniboxText, kColorOmniboxResultsBackgroundHovered},
{kColorOmniboxTextDimmed, kColorToolbarBackgroundSubtleEmphasis},
{kColorOmniboxTextDimmed, kColorToolbarBackgroundSubtleEmphasisHovered},
};
auto check_sufficient_contrast =
[&](ui::ColorId id1, ui::ColorId id2,
float expected_contrast_ratio =
color_utils::kMinimumReadableContrastRatio) {
const theme_service::test::PrintableSkColor color1{GetColor(id1)};
const theme_service::test::PrintableSkColor color2{GetColor(id2)};
const float contrast =
color_utils::GetContrastRatio(color1.color, color2.color);
EXPECT_GE(contrast, expected_contrast_ratio)
<< "Color 1: " << theme_service::test::ColorIdToString(id1) << " - "
<< color1
<< "\nColor 2: " << theme_service::test::ColorIdToString(id2)
<< " - " << color2;
};
for (const ui::ColorId* ids : contrasting_ids) {
check_sufficient_contrast(ids[0], ids[1]);
}
#if !BUILDFLAG(USE_GTK) && !BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO(crbug.com/40847971): GTK and LaCrOS do not have a sufficiently
// high-contrast selected row color to pass this test.
if (std::get<ContrastMode>(GetParam()) == ContrastMode::kHighContrast) {
check_sufficient_contrast(kColorOmniboxResultsBackgroundSelected,
kColorOmniboxResultsBackground,
color_utils::kMinimumVisibleContrastRatio);
}
#endif
}
// Sets and unsets themes using the BrowserThemeColor policy.
TEST_F(ThemeServiceTest, PolicyThemeColorSet) {
theme_service_->UseDefaultTheme();
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(theme_service_->UsingPolicyTheme());
// Setting a blank policy color shouldn't cause any theme updates.
pref_service_->ClearPref(prefs::kPolicyThemeColor);
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(theme_service_->UsingPolicyTheme());
// Setting a valid policy color causes theme to update. The applied theme is
// autogenerated based on the policy color.
profile_->GetTestingPrefService()->SetManagedPref(
prefs::kPolicyThemeColor, std::make_unique<base::Value>(100));
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_TRUE(theme_service_->UsingPolicyTheme());
// Policy theme is not saved in prefs.
EXPECT_EQ(theme_service_->GetThemeID(), std::string());
// Unsetting policy theme and setting autogenerated theme.
profile_->GetTestingPrefService()->RemoveManagedPref(
prefs::kPolicyThemeColor);
theme_service_->BuildAutogeneratedThemeFromColor(
SkColorSetRGB(100, 100, 100));
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(theme_service_->UsingPolicyTheme());
EXPECT_EQ(ThemeService::kAutogeneratedThemeID, theme_service_->GetThemeID());
// Setting a different policy color.
profile_->GetTestingPrefService()->SetManagedPref(
prefs::kPolicyThemeColor, std::make_unique<base::Value>(-100));
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_TRUE(theme_service_->UsingPolicyTheme());
EXPECT_EQ(ThemeService::kAutogeneratedThemeID, theme_service_->GetThemeID());
// Removing policy color reverts the theme to the one saved in prefs, or
// the default theme if prefs are empty.
profile_->GetTestingPrefService()->RemoveManagedPref(
prefs::kPolicyThemeColor);
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(theme_service_->UsingPolicyTheme());
EXPECT_EQ(ThemeService::kAutogeneratedThemeID, theme_service_->GetThemeID());
// Install extension theme.
ThemeScoper scoper = LoadUnpackedTheme();
EXPECT_TRUE(theme_service_->UsingExtensionTheme());
EXPECT_FALSE(theme_service_->UsingPolicyTheme());
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper.extension_id()));
EXPECT_TRUE(registry_->GetInstalledExtension(scoper.extension_id()));
// Applying policy theme should unset the extension theme but not disable the
// extension..
profile_->GetTestingPrefService()->SetManagedPref(
prefs::kPolicyThemeColor, std::make_unique<base::Value>(100));
EXPECT_FALSE(theme_service_->UsingExtensionTheme());
EXPECT_TRUE(theme_service_->UsingPolicyTheme());
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper.extension_id()));
EXPECT_TRUE(registry_->GetInstalledExtension(scoper.extension_id()));
// Cannot set other themes while a policy theme is applied.
theme_service_->BuildAutogeneratedThemeFromColor(
SkColorSetRGB(100, 100, 100));
theme_service_->UseDefaultTheme();
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(theme_service_->UsingExtensionTheme());
EXPECT_TRUE(theme_service_->UsingPolicyTheme());
// Removing policy color unsets the policy theme and restores the extension
// theme.
profile_->GetTestingPrefService()->RemoveManagedPref(
prefs::kPolicyThemeColor);
EXPECT_FALSE(theme_service_->UsingDefaultTheme());
EXPECT_FALSE(theme_service_->UsingAutogeneratedTheme());
EXPECT_TRUE(theme_service_->UsingExtensionTheme());
EXPECT_FALSE(theme_service_->UsingPolicyTheme());
EXPECT_EQ(scoper.extension_id(), theme_service_->GetThemeID());
EXPECT_TRUE(service_->IsExtensionEnabled(scoper.extension_id()));
EXPECT_TRUE(registry_->GetInstalledExtension(scoper.extension_id()));
}
TEST_F(ThemeServiceTest, UserColor) {
// Default should be empty.
EXPECT_EQ(theme_service_->GetUserColor(), std::nullopt);
// Set an autogenerated theme with background prefs.
base::Value::Dict background_info;
background_info.Set("test_data", "foo");
pref_service_->SetDict(prefs::kNtpCustomBackgroundDict,
std::move(background_info));
theme_service_->BuildAutogeneratedThemeFromColor(SK_ColorBLUE);
EXPECT_EQ(theme_service_->GetAutogeneratedThemeColor(), SK_ColorBLUE);
// Set kUserColor. It should remove the above theme but not the background.
theme_service_->SetUserColor(SK_ColorGREEN);
EXPECT_EQ(theme_service_->GetUserColor(), SK_ColorGREEN);
EXPECT_EQ(theme_service_->GetAutogeneratedThemeColor(), SK_ColorTRANSPARENT);
EXPECT_EQ(*pref_service_->GetDict(prefs::kNtpCustomBackgroundDict)
.FindString("test_data"),
"foo");
EXPECT_EQ(ThemeService::kUserColorThemeID, theme_service_->GetThemeID());
}
TEST_F(ThemeServiceTest, ThemeResetClearsUserColor) {
theme_service_->SetUserColor(SK_ColorGREEN);
EXPECT_EQ(theme_service_->GetUserColor(), SK_ColorGREEN);
// Check that kUserColor is removed as part of theme reset.
theme_service_->SetIsGrayscale(true);
EXPECT_EQ(std::nullopt, theme_service_->GetUserColor());
EXPECT_EQ(ThemeHelper::kDefaultThemeID, theme_service_->GetThemeID());
}
TEST_F(ThemeServiceTest, IsBaseline) {
theme_service_->SetUserColor(SK_ColorGREEN);
EXPECT_FALSE(theme_service_->GetIsBaseline());
theme_service_->SetUserColor(std::nullopt);
EXPECT_TRUE(theme_service_->GetIsBaseline());
}
TEST_F(ThemeServiceTest, IsGrayscale) {
// Default should be false.
EXPECT_FALSE(theme_service_->GetIsGrayscale());
// Set an autogenerated theme with background prefs.
base::Value::Dict background_info;
background_info.Set("test_data", "foo");
pref_service_->SetDict(prefs::kNtpCustomBackgroundDict,
std::move(background_info));
theme_service_->BuildAutogeneratedThemeFromColor(SK_ColorBLUE);
EXPECT_EQ(theme_service_->GetAutogeneratedThemeColor(), SK_ColorBLUE);
// Set kIsGrayscale. It should remove the above theme but not the background.
theme_service_->SetIsGrayscale(true);
EXPECT_TRUE(theme_service_->GetIsGrayscale());
EXPECT_EQ(theme_service_->GetAutogeneratedThemeColor(), SK_ColorTRANSPARENT);
EXPECT_EQ(*pref_service_->GetDict(prefs::kNtpCustomBackgroundDict)
.FindString("test_data"),
"foo");
}
TEST_F(ThemeServiceTest, ThemeResetClearsIsGrayscale) {
theme_service_->SetIsGrayscale(true);
EXPECT_TRUE(theme_service_->GetIsGrayscale());
// Check that kIsGrayscale is removed as part of theme reset.
theme_service_->SetUserColor(SK_ColorBLUE);
EXPECT_FALSE(theme_service_->GetIsGrayscale());
}
TEST_F(ThemeServiceTest, SetUserColorAndBrowserColorVariant) {
// Default should be empty and system.
EXPECT_EQ(theme_service_->GetUserColor(), std::nullopt);
EXPECT_EQ(theme_service_->GetBrowserColorVariant(),
ui::mojom::BrowserColorVariant::kSystem);
// Set an autogenerated theme with background prefs.
base::Value::Dict background_info;
background_info.Set("test_data", "foo");
pref_service_->SetDict(prefs::kNtpCustomBackgroundDict,
std::move(background_info));
theme_service_->BuildAutogeneratedThemeFromColor(SK_ColorBLUE);
EXPECT_EQ(theme_service_->GetAutogeneratedThemeColor(), SK_ColorBLUE);
// Set kUserColor and kBrowserColorVariant. It should remove the above
// theme but not the background.
theme_service_->SetUserColorAndBrowserColorVariant(
SK_ColorGREEN, ui::mojom::BrowserColorVariant::kTonalSpot);
EXPECT_EQ(theme_service_->GetUserColor(), SK_ColorGREEN);
EXPECT_EQ(theme_service_->GetBrowserColorVariant(),
ui::mojom::BrowserColorVariant::kTonalSpot);
EXPECT_EQ(theme_service_->GetAutogeneratedThemeColor(), SK_ColorTRANSPARENT);
EXPECT_EQ(*pref_service_->GetDict(prefs::kNtpCustomBackgroundDict)
.FindString("test_data"),
"foo");
EXPECT_EQ(ThemeService::kUserColorThemeID, theme_service_->GetThemeID());
}
TEST_F(ThemeServiceTest, ThemeResetClearsBrowserColorVariant) {
theme_service_->SetBrowserColorVariant(
ui::mojom::BrowserColorVariant::kTonalSpot);
EXPECT_EQ(theme_service_->GetBrowserColorVariant(),
ui::mojom::BrowserColorVariant::kTonalSpot);
// Check that kBrowserColorVariant is removed as part of theme reset.
theme_service_->SetIsGrayscale(true);
EXPECT_EQ(theme_service_->GetBrowserColorVariant(),
ui::mojom::BrowserColorVariant::kSystem);
EXPECT_EQ(ThemeHelper::kDefaultThemeID, theme_service_->GetThemeID());
}
TEST_F(ThemeServiceTest, UseDeviceTheme_DefaultValue) {
EXPECT_EQ(BUILDFLAG(IS_CHROMEOS), theme_service_->UsingDeviceTheme());
}
#if BUILDFLAG(IS_CHROMEOS)
// Verify that if uses device theme is unset and the user has a chosen color,
// they retain their chosen color.
TEST_F(ThemeServiceTest, UseDeviceTheme_DisabledByUserColor) {
theme_service_->SetUserColor(SK_ColorGREEN);
EXPECT_FALSE(theme_service_->UsingDeviceTheme());
}
// Verify that if the user has a theme loaded via extension and uses device
// theme is unset, they retain the extension.
TEST_F(ThemeServiceTest, UseDeviceTheme_DisabledByExtension) {
ThemeScoper scoper = LoadUnpackedTheme();
EXPECT_FALSE(theme_service_->UsingDeviceTheme());
}
TEST_F(ThemeServiceTest, UseDeviceTheme_DisabledByAutogeneratedTheme) {
theme_service_->BuildAutogeneratedThemeFromColor(
SkColorSetRGB(100, 100, 100));
ASSERT_TRUE(theme_service_->UsingAutogeneratedTheme());
EXPECT_FALSE(theme_service_->UsingDeviceTheme());
}
// Verify that if uses device theme has been set by the user, it is still
// true even if the user has a color from a different device.
TEST_F(ThemeServiceTest, UseDeviceTheme_ExplicitlyTrue) {
theme_service_->UseDeviceTheme(true);
theme_service_->SetUserColor(SK_ColorGREEN);
EXPECT_TRUE(theme_service_->UsingDeviceTheme());
}
#endif // IS_CHROMEOS
TEST_F(ThemeServiceTest, SetUseDeviceTheme) {
theme_service_->UseDeviceTheme(false);
EXPECT_FALSE(theme_service_->UsingDeviceTheme());
}
class BrowserColorSchemeTest : public ThemeServiceTest,
public testing::WithParamInterface<bool> {
protected:
BrowserColorSchemeTest() {
feature_list_.InitWithFeatureState(features::kChromeRefresh2023,
GetParam());
}
};
// Sets and gets browser color scheme.
TEST_P(BrowserColorSchemeTest, SetBrowserColorScheme) {
// Default without anything explicitly set should be kSystem.
ThemeService::BrowserColorScheme color_scheme =
theme_service_->GetBrowserColorScheme();
EXPECT_EQ(color_scheme, ThemeService::BrowserColorScheme::kSystem);
// Set browser color scheme to light mode.
theme_service_->SetBrowserColorScheme(
ThemeService::BrowserColorScheme::kLight);
color_scheme = theme_service_->GetBrowserColorScheme();
// If not running ChromeRefresh2023 the pref should always track the system's
// color scheme.
if (features::IsChromeRefresh2023()) {
EXPECT_EQ(color_scheme, ThemeService::BrowserColorScheme::kLight);
} else {
EXPECT_EQ(color_scheme, ThemeService::BrowserColorScheme::kSystem);
}
}
INSTANTIATE_TEST_SUITE_P(All, BrowserColorSchemeTest, testing::Bool());
} // namespace theme_service_internal