// 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 "base/command_line.h" #include "base/files/file_util.h" #include "base/macros.h" #include "base/path_service.h" #include "build/build_config.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/theme_properties.h" #include "chrome/browser/themes/theme_service_factory.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 "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" #include "content/public/test/test_utils.h" #include "extensions/browser/extension_registry.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/base/material_design/material_design_controller.h" #include "ui/base/test/material_design_controller_test_api.h" #include "ui/base/ui_base_switches.h" #if defined(ENABLE_SUPERVISED_USERS) #include "chrome/browser/supervised_user/supervised_user_service.h" #include "chrome/browser/supervised_user/supervised_user_service_factory.h" #endif using extensions::ExtensionRegistry; namespace theme_service_internal { class ThemeServiceTest : public extensions::ExtensionServiceTestBase { public: ThemeServiceTest() : is_supervised_(false), registry_(NULL) {} ~ThemeServiceTest() override {} void SetUp() override { extensions::ExtensionServiceTestBase::SetUp(); extensions::ExtensionServiceTestBase::ExtensionServiceInitParams params = CreateDefaultInitParams(); params.profile_is_supervised = is_supervised_; InitializeExtensionService(params); service_->Init(); registry_ = ExtensionRegistry::Get(profile_.get()); ASSERT_TRUE(registry_); } // Moves a minimal theme to |temp_dir_path| and unpacks it from that // directory. std::string LoadUnpackedThemeAt(const base::FilePath& temp_dir) { base::FilePath dst_manifest_path = temp_dir.AppendASCII("manifest.json"); base::FilePath test_data_dir; EXPECT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir)); base::FilePath src_manifest_path = test_data_dir.AppendASCII("extensions/theme_minimal/manifest.json"); EXPECT_TRUE(base::CopyFile(src_manifest_path, dst_manifest_path)); scoped_refptr installer( extensions::UnpackedInstaller::Create(service_)); extensions::TestExtensionRegistryObserver observer( ExtensionRegistry::Get(profile())); installer->Load(temp_dir); std::string extension_id = observer.WaitForExtensionLoaded()->id(); // Let the ThemeService finish creating the theme pack. base::MessageLoop::current()->RunUntilIdle(); return extension_id; } // Update the theme with |extension_id|. void UpdateUnpackedTheme(const std::string& extension_id) { const base::FilePath& path = service_->GetInstalledExtension(extension_id)->path(); scoped_refptr installer( extensions::UnpackedInstaller::Create(service_)); if (service_->IsExtensionEnabled(extension_id)) { extensions::TestExtensionRegistryObserver observer( ExtensionRegistry::Get(profile())); installer->Load(path); observer.WaitForExtensionLoaded(); } else { content::WindowedNotificationObserver observer( extensions::NOTIFICATION_EXTENSION_UPDATE_DISABLED, content::Source(profile_.get())); installer->Load(path); observer.Wait(); } // Let the ThemeService finish creating the theme pack. base::MessageLoop::current()->RunUntilIdle(); } const CustomThemeSupplier* get_theme_supplier(ThemeService* theme_service) { return theme_service->get_theme_supplier(); } // Alpha blends a non-opaque foreground color against an opaque background. // This is not the same as color_utils::AlphaBlend() since it gets the opacity // from the foreground color and then does not blend the two colors' alpha // values together. static SkColor AlphaBlend(SkColor foreground, SkColor background) { return color_utils::AlphaBlend(SkColorSetA(foreground, SK_AlphaOPAQUE), background, SkColorGetA(foreground)); } // 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 AlphaBlend(ThemeService::GetSeparatorColor(tab_color, frame_color), frame_color); } protected: bool is_supervised_; ExtensionRegistry* registry_; }; // Installs then uninstalls a theme and makes sure that the ThemeService // reverts to the default theme after the uninstall. TEST_F(ThemeServiceTest, ThemeInstallUninstall) { ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); // Let the ThemeService uninstall unused themes. base::MessageLoop::current()->RunUntilIdle(); base::ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); const std::string& extension_id = LoadUnpackedThemeAt(temp_dir.path()); EXPECT_FALSE(theme_service->UsingDefaultTheme()); EXPECT_EQ(extension_id, theme_service->GetThemeID()); // Now uninstall the extension, should revert to the default theme. service_->UninstallExtension(extension_id, extensions::UNINSTALL_REASON_FOR_TESTING, base::Bind(&base::DoNothing), NULL); EXPECT_TRUE(theme_service->UsingDefaultTheme()); } // 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) { ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); // Let the ThemeService uninstall unused themes. base::MessageLoop::current()->RunUntilIdle(); base::ScopedTempDir temp_dir1; ASSERT_TRUE(temp_dir1.CreateUniqueTempDir()); base::ScopedTempDir temp_dir2; ASSERT_TRUE(temp_dir2.CreateUniqueTempDir()); // 1) Installing a theme should disable the previously active theme. const std::string& extension1_id = LoadUnpackedThemeAt(temp_dir1.path()); EXPECT_FALSE(theme_service->UsingDefaultTheme()); EXPECT_EQ(extension1_id, theme_service->GetThemeID()); EXPECT_TRUE(service_->IsExtensionEnabled(extension1_id)); // Show an infobar to prevent the current theme from being uninstalled. theme_service->OnInfobarDisplayed(); const std::string& extension2_id = LoadUnpackedThemeAt(temp_dir2.path()); EXPECT_EQ(extension2_id, theme_service->GetThemeID()); EXPECT_TRUE(service_->IsExtensionEnabled(extension2_id)); EXPECT_TRUE(registry_->GetExtensionById(extension1_id, ExtensionRegistry::DISABLED)); // 2) Enabling a disabled theme extension should swap the current theme. service_->EnableExtension(extension1_id); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(extension1_id, theme_service->GetThemeID()); EXPECT_TRUE(service_->IsExtensionEnabled(extension1_id)); EXPECT_TRUE(registry_->GetExtensionById(extension2_id, ExtensionRegistry::DISABLED)); // 3) Using SetTheme() 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. const extensions::Extension* extension2 = service_->GetInstalledExtension(extension2_id); theme_service->SetTheme(extension2); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(extension2_id, theme_service->GetThemeID()); EXPECT_TRUE(service_->IsExtensionEnabled(extension2_id)); EXPECT_TRUE(registry_->GetExtensionById(extension1_id, ExtensionRegistry::DISABLED)); // 4) Disabling the current theme extension should revert to the default theme // and uninstall any installed theme extensions. theme_service->OnInfobarDestroyed(); EXPECT_FALSE(theme_service->UsingDefaultTheme()); service_->DisableExtension(extension2_id, extensions::Extension::DISABLE_USER_ACTION); base::MessageLoop::current()->RunUntilIdle(); EXPECT_TRUE(theme_service->UsingDefaultTheme()); EXPECT_FALSE(service_->GetInstalledExtension(extension1_id)); EXPECT_FALSE(service_->GetInstalledExtension(extension2_id)); } // Test the ThemeService's behavior when a theme is upgraded. TEST_F(ThemeServiceTest, ThemeUpgrade) { // Setup. ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); // Let the ThemeService uninstall unused themes. base::MessageLoop::current()->RunUntilIdle(); theme_service->OnInfobarDisplayed(); base::ScopedTempDir temp_dir1; ASSERT_TRUE(temp_dir1.CreateUniqueTempDir()); base::ScopedTempDir temp_dir2; ASSERT_TRUE(temp_dir2.CreateUniqueTempDir()); const std::string& extension1_id = LoadUnpackedThemeAt(temp_dir1.path()); const std::string& extension2_id = LoadUnpackedThemeAt(temp_dir2.path()); // Test the initial state. EXPECT_TRUE(registry_->GetExtensionById(extension1_id, ExtensionRegistry::DISABLED)); EXPECT_EQ(extension2_id, theme_service->GetThemeID()); // 1) Upgrading the current theme should not revert to the default theme. content::WindowedNotificationObserver theme_change_observer( chrome::NOTIFICATION_BROWSER_THEME_CHANGED, content::Source(theme_service)); UpdateUnpackedTheme(extension2_id); // The ThemeService should have sent an theme change notification even though // the id of the current theme did not change. theme_change_observer.Wait(); EXPECT_EQ(extension2_id, theme_service->GetThemeID()); EXPECT_TRUE(registry_->GetExtensionById(extension1_id, ExtensionRegistry::DISABLED)); // 2) Upgrading a disabled theme should not change the current theme. UpdateUnpackedTheme(extension1_id); EXPECT_EQ(extension2_id, theme_service->GetThemeID()); EXPECT_TRUE(registry_->GetExtensionById(extension1_id, ExtensionRegistry::DISABLED)); } TEST_F(ThemeServiceTest, IncognitoTest) { ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); // Let the ThemeService uninstall unused themes. base::MessageLoop::current()->RunUntilIdle(); // Should get the same ThemeService for incognito and original profiles. ThemeService* otr_theme_service = ThemeServiceFactory::GetForProfile(profile_->GetOffTheRecordProfile()); EXPECT_EQ(theme_service, otr_theme_service); #if !defined(OS_MACOSX) // Should get a different ThemeProvider for incognito and original profiles. const ui::ThemeProvider& provider = ThemeService::GetThemeProviderForProfile(profile_.get()); const ui::ThemeProvider& otr_provider = ThemeService::GetThemeProviderForProfile( profile_->GetOffTheRecordProfile()); EXPECT_NE(&provider, &otr_provider); // And (some) colors should be different in MD mode. if (ui::MaterialDesignController::IsModeMaterial()) { EXPECT_NE(provider.GetColor(ThemeProperties::COLOR_TOOLBAR), otr_provider.GetColor(ThemeProperties::COLOR_TOOLBAR)); } #endif } namespace { // NotificationObserver which emulates an infobar getting destroyed when the // theme changes. class InfobarDestroyerOnThemeChange : public content::NotificationObserver { public: explicit InfobarDestroyerOnThemeChange(Profile* profile) : theme_service_(ThemeServiceFactory::GetForProfile(profile)) { registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, content::Source(theme_service_)); } ~InfobarDestroyerOnThemeChange() override {} private: void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) override { theme_service_->OnInfobarDestroyed(); } // Not owned. ThemeService* theme_service_; content::NotificationRegistrar registrar_; DISALLOW_COPY_AND_ASSIGN(InfobarDestroyerOnThemeChange); }; } // namespace // crbug.com/468280 TEST_F(ThemeServiceTest, UninstallThemeOnThemeChangeNotification) { // Setup. ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); // Let the ThemeService uninstall unused themes. base::MessageLoop::current()->RunUntilIdle(); base::ScopedTempDir temp_dir1; ASSERT_TRUE(temp_dir1.CreateUniqueTempDir()); base::ScopedTempDir temp_dir2; ASSERT_TRUE(temp_dir2.CreateUniqueTempDir()); const std::string& extension1_id = LoadUnpackedThemeAt(temp_dir1.path()); ASSERT_EQ(extension1_id, theme_service->GetThemeID()); // Show an infobar. theme_service->OnInfobarDisplayed(); // Install another theme. Emulate the infobar destroying itself (and // causing unused themes to be uninstalled) as a result of the // NOTIFICATION_BROWSER_THEME_CHANGED notification. { InfobarDestroyerOnThemeChange destroyer(profile_.get()); const std::string& extension2_id = LoadUnpackedThemeAt(temp_dir2.path()); ASSERT_EQ(extension2_id, theme_service->GetThemeID()); ASSERT_FALSE(service_->GetInstalledExtension(extension1_id)); } // Check that it is possible to reinstall extension1. ASSERT_EQ(extension1_id, LoadUnpackedThemeAt(temp_dir1.path())); EXPECT_EQ(extension1_id, theme_service->GetThemeID()); } #if defined(ENABLE_SUPERVISED_USERS) class ThemeServiceSupervisedUserTest : public ThemeServiceTest { public: ThemeServiceSupervisedUserTest() {} ~ThemeServiceSupervisedUserTest() override {} void SetUp() override { is_supervised_ = true; ThemeServiceTest::SetUp(); } }; // Checks that supervised users have their own default theme. TEST_F(ThemeServiceSupervisedUserTest, SupervisedUserThemeReplacesDefaultTheme) { ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); EXPECT_TRUE(theme_service->UsingDefaultTheme()); EXPECT_TRUE(get_theme_supplier(theme_service)); EXPECT_EQ(get_theme_supplier(theme_service)->get_theme_type(), CustomThemeSupplier::SUPERVISED_USER_THEME); } #if defined(OS_LINUX) && !defined(OS_CHROMEOS) // Checks that supervised users don't use the system theme even if it is the // default. The system theme is only available on Linux. TEST_F(ThemeServiceSupervisedUserTest, SupervisedUserThemeReplacesNativeTheme) { profile_->GetPrefs()->SetBoolean(prefs::kUsesSystemTheme, true); ThemeService* theme_service = ThemeServiceFactory::GetForProfile(profile_.get()); theme_service->UseDefaultTheme(); EXPECT_TRUE(theme_service->UsingDefaultTheme()); EXPECT_TRUE(get_theme_supplier(theme_service)); EXPECT_EQ(get_theme_supplier(theme_service)->get_theme_type(), CustomThemeSupplier::SUPERVISED_USER_THEME); } #endif // defined(OS_LINUX) && !defined(OS_CHROMEOS) #endif // defined(ENABLE_SUPERVISED_USERS) #if !defined(OS_MACOSX) // Mac uses different colors than other platforms. // Simple class to run tests in material design mode. class ThemeServiceMaterialDesignTest : public ThemeServiceTest { public: void SetUp() override { ThemeServiceTest::SetUp(); ui::test::MaterialDesignControllerTestAPI::SetMode( ui::MaterialDesignController::MATERIAL_NORMAL); } void TearDown() override { ThemeServiceTest::TearDown(); ui::test::MaterialDesignControllerTestAPI::UninitializeMode(); } }; // Check that the function which computes the separator color behaves as // expected for a variety of inputs. We run in material design mode so we can // use the material normal and incognito color combinations, which differ from // each other in ways that are interesting to test. TEST_F(ThemeServiceMaterialDesignTest, SeparatorColor) { // Ensure Windows 10 machines use the built-in default colors rather than the // current system native colors. base::CommandLine::ForCurrentProcess()->AppendSwitch( switches::kDisableDwmComposition); // Check that the TOOLBAR_TOP_SEPARATOR color is the same whether we ask the // theme provider or compute it manually. const ui::ThemeProvider& theme_provider = ThemeService::GetThemeProviderForProfile(profile_.get()); SkColor frame_color = theme_provider.GetColor(ThemeProperties::COLOR_FRAME); SkColor theme_color = AlphaBlend( theme_provider.GetColor(ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR), frame_color); SkColor tab_color = theme_provider.GetColor(ThemeProperties::COLOR_TOOLBAR); SkColor separator_color = GetSeparatorColor(tab_color, frame_color); EXPECT_EQ(theme_color, separator_color); // For the default theme, the separator should darken the frame. double frame_luminance = color_utils::GetRelativeLuminance(frame_color); EXPECT_LT(color_utils::GetRelativeLuminance(separator_color), frame_luminance); // If we reverse the colors, the separator should darken the "frame" (which // in this case is actually the tab color), since otherwise the contrast with // the "frame" would be too minimal. It should also be darker than the "tab" // (frame color) since otherwise the contrast the contrast with the "tab // color" would be too minimal. separator_color = GetSeparatorColor(frame_color, tab_color); double tab_luminance = color_utils::GetRelativeLuminance(tab_color); double separator_luminance = color_utils::GetRelativeLuminance(separator_color); EXPECT_LT(separator_luminance, tab_luminance); EXPECT_LT(separator_luminance, frame_luminance); // When the frame color is black, the separator should lighten the frame, but // it should still be darker than the tab color. separator_color = GetSeparatorColor(tab_color, SK_ColorBLACK); separator_luminance = color_utils::GetRelativeLuminance(separator_color); EXPECT_GT(separator_luminance, 0); EXPECT_LT(separator_luminance, tab_luminance); // When the frame color is white, the separator should darken the frame; it // should also be lighter than the tab color since otherwise the contrast with // the tab would be too minimal. separator_color = GetSeparatorColor(tab_color, SK_ColorWHITE); separator_luminance = color_utils::GetRelativeLuminance(separator_color); EXPECT_LT(separator_luminance, 1); EXPECT_LT(separator_luminance, tab_luminance); // Now make similar checks as above but for the incognito theme. const ui::ThemeProvider& otr_provider = ThemeService::GetThemeProviderForProfile( profile_->GetOffTheRecordProfile()); frame_color = otr_provider.GetColor(ThemeProperties::COLOR_FRAME); theme_color = AlphaBlend( otr_provider.GetColor(ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR), frame_color); tab_color = otr_provider.GetColor(ThemeProperties::COLOR_TOOLBAR); separator_color = GetSeparatorColor(tab_color, frame_color); EXPECT_EQ(theme_color, separator_color); // For the default incognito theme, the separator should darken the frame. EXPECT_LT(color_utils::GetRelativeLuminance(separator_color), color_utils::GetRelativeLuminance(frame_color)); // And if we reverse the colors, the separator should lighten the "frame" // (tab color). separator_color = GetSeparatorColor(frame_color, tab_color); tab_luminance = color_utils::GetRelativeLuminance(tab_color); EXPECT_GT(color_utils::GetRelativeLuminance(separator_color), tab_luminance); // When the frame color is black, the separator should lighten the frame; it // should also be lighter than the tab color since otherwise the contrast with // the tab would be too minimal. separator_color = GetSeparatorColor(tab_color, SK_ColorBLACK); separator_luminance = color_utils::GetRelativeLuminance(separator_color); EXPECT_GT(separator_luminance, 0); EXPECT_GT(separator_luminance, tab_luminance); // When the frame color is white, the separator should darken the frame, but // it should still be lighter than the tab color. separator_color = GetSeparatorColor(tab_color, SK_ColorWHITE); separator_luminance = color_utils::GetRelativeLuminance(separator_color); EXPECT_LT(separator_luminance, 1); EXPECT_GT(separator_luminance, tab_luminance); } #endif // !defined(OS_MACOSX) }; // namespace theme_service_internal