blob: 3620f44b97c56ca95ef4559fb76f5058a786b431 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// 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/chromeos_buildflags.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/chrome_notification_types.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/increased_contrast_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/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/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 "testing/gtest/include/gtest/gtest.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(USE_GTK)
#include "ui/gtk/gtk_ui_factory.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/views/linux_ui/linux_ui.h"
#endif // BUILDFLAG(USE_GTK)
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
#include "chrome/browser/supervised_user/supervised_user_service.h"
#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
#endif // BUILDFLAG(ENABLE_SUPERVISED_USERS)
namespace {
enum class ContrastMode { kNonHighContrast, kHighContrast };
enum class SystemTheme { kDefault, kCustom };
// Struct to distinguish SkColor (aliased to uint32_t) for printing.
struct PrintableSkColor {
bool operator==(const PrintableSkColor& other) const {
return color == other.color;
}
bool operator!=(const PrintableSkColor& other) const {
return !operator==(other);
}
const SkColor color;
};
std::ostream& operator<<(std::ostream& os, PrintableSkColor printable_color) {
SkColor color = printable_color.color;
return os << base::StringPrintf("SkColorARGB(0x%02x, 0x%02x, 0x%02x, 0x%02x)",
SkColorGetA(color), SkColorGetR(color),
SkColorGetG(color), SkColorGetB(color));
}
std::string ColorIdToString(int id) {
#define E(color_id, theme_property_id, ...) \
{theme_property_id, #theme_property_id},
#define E_CPONLY(color_id)
static constexpr const auto kMap =
base::MakeFixedFlatMap<int, const char*>({CHROME_COLOR_IDS});
#undef E
#undef E_CPONLY
constexpr char kPrefix[] = "ThemeProperties::";
std::string id_str = kMap.find(id)->second;
if (base::StartsWith(id_str, kPrefix))
return id_str.substr(strlen(kPrefix));
return id_str;
}
std::pair<PrintableSkColor, PrintableSkColor> GetOriginalAndRedirected(
const ui::ThemeProvider& theme_provider,
int color_id) {
PrintableSkColor original{theme_provider.GetColor(color_id)};
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
features::kColorProviderRedirectionForThemeProvider);
PrintableSkColor redirected{theme_provider.GetColor(color_id)};
return std::make_pair(original, redirected);
}
// 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);
}
}
std::string 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;
std::string extension_id_;
base::ScopedTempDir temp_dir_;
};
} // namespace
namespace theme_service_internal {
class ThemeServiceTest : public extensions::ExtensionServiceTestBase {
public:
ThemeServiceTest() = default;
~ThemeServiceTest() override = default;
void SetUp() override {
extensions::ExtensionServiceTestBase::SetUp();
extensions::ExtensionServiceTestBase::ExtensionServiceInitParams params =
CreateDefaultInitParams();
params.pref_file = base::FilePath();
InitializeExtensionService(params);
service_->Init();
registry_ = extensions::ExtensionRegistry::Get(profile());
ASSERT_TRUE(registry_);
theme_service_ = ThemeServiceFactory::GetForProfile(profile());
ASSERT_TRUE(theme_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/1182630) : 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();
}
void set_theme_supplier(ThemeService* theme_service,
scoped_refptr<CustomThemeSupplier> theme_supplier) {
theme_service->theme_supplier_ = theme_supplier;
}
SkColor GetColor(ThemeService* theme_service, int id, bool incognito) const {
return theme_service->theme_helper_.GetColor(
id, incognito, theme_service->GetThemeSupplier());
}
bool IsExtensionDisabled(const std::string& id) const {
return registry_->GetExtensionById(id,
extensions::ExtensionRegistry::DISABLED);
}
// Returns the separator color as the opaque result of blending it atop the
// frame color (which is the color we use when calculating the contrast of the
// separator with the tab and frame colors).
static SkColor GetSeparatorColor(SkColor tab_color, SkColor frame_color) {
return color_utils::GetResultingPaintColor(
ThemeHelper::GetSeparatorColor(tab_color, frame_color), frame_color);
}
protected:
ui::TestNativeTheme native_theme_;
raw_ptr<extensions::ExtensionRegistry> registry_ = nullptr;
raw_ptr<ThemeService> theme_service_ = nullptr;
};
class ThemeProviderRedirectedEquivalenceTest
: public ThemeServiceTest,
public testing::WithParamInterface<
std::tuple<ui::NativeTheme::ColorScheme, ContrastMode, SystemTheme>> {
public:
ThemeProviderRedirectedEquivalenceTest() = default;
// ThemeServiceTest:
void SetUp() override {
ThemeServiceTest::SetUp();
// Only perform mixer initialization once.
static bool initialized_mixers = false;
if (!initialized_mixers) {
#if BUILDFLAG(USE_GTK)
// Ensures GTK is configured on supported linux platforms. Initializing
// GTK also adds the native GTK ColorMixers.
ui::OzonePlatform::InitParams ozone_params;
ozone_params.single_process = true;
ui::OzonePlatform::InitializeForUI(ozone_params);
auto linux_ui = BuildGtkUi();
linux_ui->SetUseSystemThemeCallback(base::BindRepeating(
[](bool use_system_theme, aura::Window* window) {
return use_system_theme;
},
std::get<SystemTheme>(GetParam()) == SystemTheme::kCustom));
linux_ui->Initialize();
views::LinuxUI::SetInstance(std::move(linux_ui));
#endif // BUILDFLAG(USE_GTK)
// Add the Chrome ColorMixers after native ColorMixers.
ui::ColorProviderManager::Get().AppendColorProviderInitializer(
base::BindRepeating(AddChromeColorMixers));
initialized_mixers = true;
}
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.
ui::NativeTheme* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
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->set_preferred_contrast(
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.
ui::NativeTheme* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
native_theme->set_forced_colors(original_forced_colors_);
native_theme->set_preferred_contrast(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));
}
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;
};
#define E(color_id, theme_property_id, ...) theme_property_id,
#define E_CPONLY(color_id)
static constexpr int kTestIdValues[] = {CHROME_COLOR_IDS};
#undef E
#undef E_CPONLY
INSTANTIATE_TEST_SUITE_P(
,
ThemeProviderRedirectedEquivalenceTest,
::testing::Combine(::testing::Values(ui::NativeTheme::ColorScheme::kLight,
ui::NativeTheme::ColorScheme::kDark),
::testing::Values(ContrastMode::kNonHighContrast,
ContrastMode::kHighContrast),
::testing::Values(SystemTheme::kDefault,
SystemTheme::kCustom)),
ThemeProviderRedirectedEquivalenceTest::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(https://crbug.com/1039006): 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.
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 =
profile_->GetPrefs()->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 = profile_->GetPrefs()->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_F(ThemeServiceTest, OmniboxContrast) {
using TP = ThemeProperties;
for (bool dark : {false, true}) {
native_theme_.SetDarkMode(dark);
for (bool high_contrast : {false, true}) {
set_theme_supplier(
theme_service_,
high_contrast ? base::MakeRefCounted<IncreasedContrastThemeSupplier>(
&native_theme_)
: nullptr);
constexpr int contrasting_ids[][2] = {
{TP::COLOR_OMNIBOX_TEXT, TP::COLOR_OMNIBOX_BACKGROUND},
{TP::COLOR_OMNIBOX_TEXT, TP::COLOR_OMNIBOX_BACKGROUND_HOVERED},
{TP::COLOR_OMNIBOX_TEXT, TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_TEXT, TP::COLOR_OMNIBOX_RESULTS_BG_HOVERED},
{TP::COLOR_OMNIBOX_TEXT_DIMMED, TP::COLOR_OMNIBOX_BACKGROUND},
{TP::COLOR_OMNIBOX_SELECTED_KEYWORD, TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_RESULTS_TEXT_SELECTED,
TP::COLOR_OMNIBOX_RESULTS_BG_SELECTED},
{TP::COLOR_OMNIBOX_RESULTS_TEXT_DIMMED, TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_RESULTS_TEXT_SELECTED,
TP::COLOR_OMNIBOX_RESULTS_BG_SELECTED},
{TP::COLOR_OMNIBOX_RESULTS_ICON, TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_RESULTS_ICON,
TP::COLOR_OMNIBOX_RESULTS_BG_HOVERED},
{TP::COLOR_OMNIBOX_RESULTS_ICON_SELECTED,
TP::COLOR_OMNIBOX_RESULTS_BG_SELECTED},
{TP::COLOR_OMNIBOX_RESULTS_URL, TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_RESULTS_URL_SELECTED,
TP::COLOR_OMNIBOX_RESULTS_BG_SELECTED},
{TP::COLOR_OMNIBOX_BUBBLE_OUTLINE, TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_BUBBLE_OUTLINE_EXPERIMENTAL_KEYWORD_MODE,
TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_DEFAULT,
TP::COLOR_OMNIBOX_BACKGROUND},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_DEFAULT,
TP::COLOR_OMNIBOX_BACKGROUND_HOVERED},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_DEFAULT,
TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_SECURE,
TP::COLOR_OMNIBOX_BACKGROUND},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_SECURE,
TP::COLOR_OMNIBOX_BACKGROUND_HOVERED},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_SECURE,
TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_DANGEROUS,
TP::COLOR_OMNIBOX_BACKGROUND},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_DANGEROUS,
TP::COLOR_OMNIBOX_RESULTS_BG},
{TP::COLOR_OMNIBOX_TEXT_DIMMED, TP::COLOR_OMNIBOX_BACKGROUND_HOVERED},
{TP::COLOR_OMNIBOX_RESULTS_TEXT_DIMMED,
TP::COLOR_OMNIBOX_RESULTS_BG_HOVERED},
{TP::COLOR_OMNIBOX_RESULTS_URL, TP::COLOR_OMNIBOX_RESULTS_BG_HOVERED},
{TP::COLOR_OMNIBOX_SECURITY_CHIP_DANGEROUS,
TP::COLOR_OMNIBOX_BACKGROUND_HOVERED},
};
auto check_sufficient_contrast = [&](int id1, int id2) {
const float contrast =
color_utils::GetContrastRatio(GetColor(theme_service_, id1, dark),
GetColor(theme_service_, id2, dark));
EXPECT_GE(contrast, color_utils::kMinimumReadableContrastRatio);
};
for (const int* ids : contrasting_ids)
check_sufficient_contrast(ids[0], ids[1]);
if (high_contrast)
check_sufficient_contrast(TP::COLOR_OMNIBOX_RESULTS_BG_SELECTED,
TP::COLOR_OMNIBOX_RESULTS_BG);
}
}
}
TEST_F(ThemeServiceTest, NativeIncreasedContrastChanged) {
theme_service_->UseDefaultTheme();
native_theme_.SetUserHasContrastPreference(true);
theme_service_->OnNativeThemeUpdated(&native_theme_);
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
bool using_increased_contrast =
theme_service_->GetThemeSupplier() &&
theme_service_->GetThemeSupplier()->get_theme_type() ==
CustomThemeSupplier::ThemeType::INCREASED_CONTRAST;
bool expecting_increased_contrast =
theme_service_->theme_helper_for_testing()
.ShouldUseIncreasedContrastThemeSupplier(&native_theme_);
EXPECT_EQ(using_increased_contrast, expecting_increased_contrast);
native_theme_.SetUserHasContrastPreference(false);
theme_service_->OnNativeThemeUpdated(&native_theme_);
EXPECT_TRUE(theme_service_->UsingDefaultTheme());
EXPECT_EQ(theme_service_->GetThemeSupplier(), nullptr);
}
// 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.
profile_->GetPrefs()->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()));
}
// TODO(crbug.com/1056953): Enable on Mac, Ash Chrome, Fuchsia, and Linux GTK.
// Flaky on linux-chromeos-rel crbug.com/1273727
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS_ASH) || \
BUILDFLAG(IS_FUCHSIA) || BUILDFLAG(USE_GTK)
#define MAYBE_GetColor DISABLED_GetColor
#else
#define MAYBE_GetColor GetColor
#endif
TEST_P(ThemeProviderRedirectedEquivalenceTest, MAYBE_GetColor) {
static constexpr const auto kTolerances = base::MakeFixedFlatMap<int, int>(
{{ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_INACTIVE, 1}});
auto get_tolerance = [](int id) {
auto* it = kTolerances.find(id);
if (it != kTolerances.end())
return it->second;
return 0;
};
const ui::ThemeProvider& theme_provider =
ThemeService::GetThemeProviderForProfile(profile());
for (auto color_id : kTestIdValues) {
// Verifies that colors with and without the ColorProvider are the same.
auto pair = GetOriginalAndRedirected(theme_provider, color_id);
auto original = pair.first;
auto redirected = pair.second;
auto tolerance = get_tolerance(color_id);
std::string error_message =
base::StrCat({ColorIdToString(color_id), " has mismatched values"});
if (!tolerance) {
EXPECT_EQ(original, redirected) << error_message;
} else {
EXPECT_LE(std::abs(static_cast<int>(SkColorGetA(original.color) -
SkColorGetA(redirected.color))),
tolerance)
<< error_message;
EXPECT_LE(std::abs(static_cast<int>(SkColorGetR(original.color) -
SkColorGetR(redirected.color))),
tolerance)
<< error_message;
EXPECT_LE(std::abs(static_cast<int>(SkColorGetG(original.color) -
SkColorGetG(redirected.color))),
tolerance)
<< error_message;
EXPECT_LE(std::abs(static_cast<int>(SkColorGetB(original.color) -
SkColorGetB(redirected.color))),
tolerance)
<< error_message;
}
}
}
} // namespace theme_service_internal