| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <optional> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/version_info/channel.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/features/feature_channel.h" |
| #include "extensions/common/icons/extension_icon_set.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/manifest_handlers/icon_variants_handler.h" |
| #include "extensions/common/manifest_handlers/icons_handler.h" |
| #include "extensions/common/manifest_test.h" |
| #include "extensions/common/warnings_test_util.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| namespace extensions { |
| |
| // Don't enable the icon variants feature. Warn if the key is used, but don't |
| // create an error. |
| using NoIconVariantsManifestTest = ManifestTest; |
| |
| TEST_F(NoIconVariantsManifestTest, Warnings) { |
| // Test simple feature's AvailabilityResult::kUnsupportedChannel. |
| LoadAndExpectWarning("icon_variants.json", |
| "'icon_variants' requires canary channel or newer, " |
| "but this is the stable channel."); |
| } |
| |
| // Enable the icon variants feature. |
| class IconVariantsManifestTest : public ManifestTest { |
| public: |
| IconVariantsManifestTest() { |
| feature_list_.InitAndEnableFeature( |
| extensions_features::kExtensionIconVariants); |
| } |
| |
| protected: |
| ManifestData GetManifestData(const std::string& icon_variants, |
| int manifest_version = 3) { |
| static constexpr char kManifestStub[] = |
| R"({ |
| "name": "Test", |
| "version": "0.1", |
| "manifest_version": %d, |
| "icon_variants": %s |
| })"; |
| return ManifestData::FromJSON(base::StringPrintf( |
| kManifestStub, manifest_version, icon_variants.c_str())); |
| } |
| |
| private: |
| const ScopedCurrentChannel current_channel_{version_info::Channel::CANARY}; |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Parse `icon_variants` in manifest.json. |
| TEST_F(IconVariantsManifestTest, Success) { |
| static constexpr struct { |
| const char* title; |
| const char* icon_variants; |
| } test_cases[] = { |
| {"Define a `size`.", |
| R"([ |
| { |
| "128": "128.png", |
| } |
| ])"}, |
| {"Define `any`.", |
| R"([ |
| { |
| "any": "any.png", |
| } |
| ])"}, |
| {"Define `color_schemes`.", |
| R"([ |
| { |
| "16": "16.png", |
| "color_schemes": ["dark"] |
| } |
| ])"}, |
| }; |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("Error: '%s'", test_case.title)); |
| LoadAndExpectSuccess(GetManifestData(test_case.icon_variants)); |
| } |
| } |
| |
| struct WarningTestCase { |
| const char* title; |
| const char* warning; |
| const char* icon_variants; |
| }; |
| |
| // Cases that could generate warnings after parsing successfully. |
| TEST_F(IconVariantsManifestTest, SuccessWithOptionalWarning) { |
| WarningTestCase test_cases[] = { |
| {"An icon size is below the minimum", "Icon variant 'size' is not valid.", |
| R"([ |
| { |
| "0": "0.png", |
| "16": "16.png", |
| } |
| ])"}, |
| {"An icon size is above the max.", "Icon variant 'size' is not valid.", |
| R"([ |
| { |
| "2048": "2048.png", |
| "2049": "2049.png", |
| } |
| ])"}, |
| {"An empty icon variant.", "Icon variant is empty.", |
| R"([ |
| { |
| "16": "16.png", |
| }, |
| {} |
| ])"}, |
| }; |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("Error: '%s'", test_case.title)); |
| LoadAndExpectWarning(GetManifestData(test_case.icon_variants), |
| test_case.warning); |
| } |
| } |
| |
| // Check cases where there could be multiple warnings. |
| TEST_F(IconVariantsManifestTest, SuccessWithOptionalWarnings) { |
| WarningTestCase test_cases[] = { |
| {"`color_schemes` should be an array of strings", |
| "Unexpected 'color_schemes' type.", |
| R"([ |
| { |
| "16": "16.png", |
| "color_schemes": [["type warning"]] |
| } |
| ])"}, |
| {"`color_schemes` is expected to be an array of strings.", |
| "Unexpected 'color_schemes' type.", |
| R"([ |
| { |
| "16": "16.png", |
| "color_schemes": "type warning" |
| } |
| ])"}, |
| {"Invalid color scheme", "Unexpected 'color_scheme'.", |
| R"([ |
| { |
| "16": "16.png", |
| "color_schemes": ["warning"] |
| } |
| ])"}, |
| }; |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("Error: '%s'", test_case.title)); |
| std::vector<std::string> warnings({test_case.warning, "Failed to parse."}); |
| LoadAndExpectWarnings(GetManifestData(test_case.icon_variants), warnings); |
| } |
| } |
| |
| // Cases that would otherwise cause errors and prevent the extension from |
| // loading. However, `icon_variants` aims to avoid causing errors preventing |
| // extensions from loading. That makes the key more flexible for later changes. |
| TEST_F(IconVariantsManifestTest, WarnOnUnrecognizedIconVariants) { |
| static constexpr struct { |
| const char* title; |
| const char* icon_variants; |
| } test_cases[] = { |
| {"Empty value", "[{}]"}, |
| {"Empty array", "[]"}, |
| {"Invalid item type", R"(["error"])"}, |
| {"Icon variants are empty (IsEmpty())", R"([ |
| { |
| "empty": "empty.png", |
| } |
| ])"}, |
| }; |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("Error: '%s'", test_case.title)); |
| scoped_refptr<extensions::Extension> extension = |
| LoadAndExpectSuccess(GetManifestData(test_case.icon_variants)); |
| ASSERT_FALSE(IconVariantsInfo::HasIconVariants(extension.get())); |
| EXPECT_TRUE(warnings_test_util::HasInstallWarning( |
| extension, "'icon_variants' is not valid.")); |
| } |
| } |
| |
| TEST_F(IconVariantsManifestTest, PreferIconVariantsOverIcons) { |
| ManifestData manifest_data = ManifestData::FromJSON( |
| R"({ |
| "name": "test", |
| "version": "1", |
| "manifest_version": 3, |
| "icons": { |
| "16": "icons.16.png" |
| }, |
| "icon_variants": [{ |
| "16": "icon_variants.16.png" |
| }] |
| })"); |
| scoped_refptr<extensions::Extension> extension( |
| LoadAndExpectSuccess(manifest_data)); |
| const ExtensionIconSet& icons = IconsInfo::GetIcons(extension.get()); |
| EXPECT_EQ("icon_variants.16.png", |
| icons.Get(extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly)); |
| } |
| |
| TEST_F(IconVariantsManifestTest, GetIconMethods) { |
| ManifestData manifest_data = ManifestData::FromJSON( |
| R"({ |
| "name": "test", |
| "version": "1", |
| "manifest_version": 3, |
| "icons": { |
| "16": "icons.16.png" |
| }, |
| "icon_variants": [ |
| { |
| "16": "icon_variants.16.png" |
| }, |
| { |
| "16": "icon_variants.16.dark.png", |
| "color_schemes": ["dark"] |
| } |
| ] |
| })"); |
| scoped_refptr<extensions::Extension> extension( |
| LoadAndExpectSuccess(manifest_data)); |
| |
| static constexpr struct { |
| const char* title; |
| const std::optional<ExtensionIconVariant::ColorScheme> color_scheme; |
| const char* expected; |
| } test_cases[] = { |
| { |
| "Light theme icons are returned by default sans color scheme.", |
| std::nullopt, |
| "icon_variants.16.png", |
| }, |
| { |
| "Light theme specified.", |
| ExtensionIconVariant::ColorScheme::kLight, |
| "icon_variants.16.png", |
| }, |
| { |
| "Dark theme specified.", |
| ExtensionIconVariant::ColorScheme::kDark, |
| "icon_variants.16.dark.png", |
| }}; |
| |
| // GetIcons. |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("GetIcons: '%s'", test_case.title)); |
| const ExtensionIconSet& icons = |
| !test_case.color_scheme.has_value() |
| ? IconsInfo::GetIcons(extension.get()) |
| : IconsInfo::GetIcons(*extension, test_case.color_scheme.value()); |
| EXPECT_EQ(test_case.expected, |
| icons.Get(extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly)); |
| } |
| |
| // GetIconResource. |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("GetIconResource: '%s'", test_case.title)); |
| const ExtensionResource& resource = |
| !test_case.color_scheme.has_value() |
| ? IconsInfo::GetIconResource(extension.get(), |
| extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly) |
| : IconsInfo::GetIconResource(extension.get(), |
| extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly, |
| test_case.color_scheme.value()); |
| EXPECT_EQ(test_case.expected, resource.relative_path().AsUTF8Unsafe()); |
| } |
| |
| // GetIconURL. |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf("GetIconURL: '%s'", test_case.title)); |
| const GURL& icon_url = |
| !test_case.color_scheme.has_value() |
| ? IconsInfo::GetIconURL(extension.get(), |
| extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly) |
| : IconsInfo::GetIconURL(extension.get(), |
| extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly, |
| test_case.color_scheme.value()); |
| EXPECT_EQ(test_case.expected, icon_url.GetPath().substr(1)); |
| } |
| } |
| |
| // "color_scheme" is not currently supported in singular form. It only generates |
| // a warning, and not an error. Because known keys are checked first, all |
| // subsequent keys are assumed to be integer sizes, until a warning such as this |
| // proves otherwise. |
| TEST_F(IconVariantsManifestTest, ColorSchemeKeyWarning) { |
| ManifestData manifest_data = ManifestData::FromJSON( |
| R"({ |
| "name": "Test", |
| "version": "1", |
| "manifest_version": 3, |
| "icon_variants": [ |
| { |
| "128": "128.png", |
| "color_scheme": ["dark"] |
| } |
| ] |
| })"); |
| scoped_refptr<extensions::Extension> extension( |
| LoadAndExpectSuccess(manifest_data)); |
| ASSERT_TRUE(extension->install_warnings().size() == 1); |
| ASSERT_EQ("Icon variant 'size' is not valid.", |
| extension->install_warnings().at(0).message); |
| } |
| |
| // "color_schemes" is represented as a plural key for manifest.json. |
| TEST_F(IconVariantsManifestTest, ColorSchemesKeyValid) { |
| ManifestData manifest_data = ManifestData::FromJSON( |
| R"({ |
| "name": "Test", |
| "version": "1", |
| "manifest_version": 3, |
| "icon_variants": [ |
| { |
| "128": "128.png", |
| "color_schemes": ["dark"] |
| } |
| ] |
| })"); |
| scoped_refptr<extensions::Extension> extension( |
| LoadAndExpectSuccess(manifest_data)); |
| ASSERT_TRUE(extension->install_warnings().empty()); |
| } |
| |
| TEST_F(IconVariantsManifestTest, GetIconUrlWithSpecialChars) { |
| ManifestData manifest_data = ManifestData::FromJSON( |
| R"({ |
| "name": "test", |
| "version": "1", |
| "manifest_version": 3, |
| "icons": { |
| "16": "#icons.16.png" |
| }, |
| "icon_variants": [ |
| { |
| "16": "#icon_variants.16.png" |
| }, |
| { |
| "16": "#icon_variants.16.dark.png", |
| "color_schemes": ["dark"] |
| } |
| ] |
| })"); |
| scoped_refptr<extensions::Extension> extension( |
| LoadAndExpectSuccess(manifest_data)); |
| |
| const GURL& icon_url = IconsInfo::GetIconURL( |
| extension.get(), extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly); |
| EXPECT_EQ("%23icon_variants.16.png", icon_url.GetPath().substr(1)); |
| |
| const GURL& icon_url_light = IconsInfo::GetIconURL( |
| extension.get(), extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly, |
| ExtensionIconVariant::ColorScheme::kLight); |
| EXPECT_EQ("%23icon_variants.16.png", icon_url_light.GetPath().substr(1)); |
| |
| const GURL& icon_url_dark = IconsInfo::GetIconURL( |
| extension.get(), extension_misc::EXTENSION_ICON_BITTY, |
| ExtensionIconSet::Match::kExactly, |
| ExtensionIconVariant::ColorScheme::kDark); |
| EXPECT_EQ("%23icon_variants.16.dark.png", icon_url_dark.GetPath().substr(1)); |
| } |
| |
| } // namespace extensions |