| // 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 <tuple> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/version_info/channel.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "extensions/test/result_catcher.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // The `key` field stores the public key for the extension with id |
| // "jnapclmfkaejhjkddbmiafekigmcbmma". |
| static constexpr char kManifestTemplate[] = |
| R"JS( |
| { |
| "name": "AI language model test", |
| "version": "0.1", |
| "manifest_version": 3, |
| "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3H6Jc0On6l0H3DJ6bx4aOW3+srCfjSdr+3ukwIEZrL6jDy500XweIwOp9PItpM9sijwu8v1rdyoBPubm/ottp/oz42aKp+2xIxcMTa6/cA2BL2kOWxwv+WP9d01IOFbFpWmQBDQNpp2UmH67OFbie6zHhyrSJKL2o9d05iX0a9Xwv9W48JKYpldo+/2JTP/5en0jxgiN+qkOCZuLag2cS/6Az0LArqsf5D+ReJemIBCNJhVxu3P0naxfEG6B6XczzuuptrX3H2vDr1LxZasLh9bzV88+8BxarjETACebOfqy366QxXluwAjnu/NHPv53edXlXvXrZ0C69RvvlMh1qQIDAQAB", |
| "description": "Extension for testing the AI language model API.", |
| "background": { |
| "service_worker": "sw.js" |
| } |
| } |
| )JS"; |
| |
| // The boolean tuple describing: |
| // 1. if the `kAIPromptAPI` chrome://flag is explicitly enabled; |
| // 2. if the `kAIPromptAPI` kill switch is triggered; |
| // 3. if the `kAIPromptAPIForExtension` kill switch is triggered; |
| using Variant = std::tuple<bool, bool, bool>; |
| bool IsAPIFlagEnabled(Variant v) { |
| return std::get<0>(v); |
| } |
| bool IsAPIKillSwitchTriggered(Variant v) { |
| return std::get<1>(v); |
| } |
| bool IsExtensionKillSwitchTriggered(Variant v) { |
| return std::get<2>(v); |
| } |
| |
| // Describes the test variants in a meaningful way in the parameterized tests. |
| std::string DescribeTestVariant(const testing::TestParamInfo<Variant> info) { |
| std::string api_flag_enabled = |
| IsAPIFlagEnabled(info.param) ? "WithAPIFlag" : "NoAPIFlag"; |
| std::string api_kill_switch = IsAPIKillSwitchTriggered(info.param) |
| ? "WithAPIKillswitch" |
| : "NoAPIKillswitch"; |
| std::string extension_kill_switch = IsExtensionKillSwitchTriggered(info.param) |
| ? "WithExtensionKillswitch" |
| : "NoExtensionKillswitch"; |
| return base::JoinString( |
| {api_flag_enabled, api_kill_switch, extension_kill_switch}, "_"); |
| } |
| |
| } // namespace |
| |
| // TODO(crbug.com/419321441): Support Built-In AI APIs on ChromeOS. |
| #if BUILDFLAG(IS_CHROMEOS) |
| #define MAYBE_ExtensionAILanguageModelBrowserTest \ |
| DISABLED_ExtensionAILanguageModelBrowserTest |
| #else |
| #define MAYBE_ExtensionAILanguageModelBrowserTest \ |
| ExtensionAILanguageModelBrowserTest |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| class MAYBE_ExtensionAILanguageModelBrowserTest |
| : public ExtensionBrowserTest, |
| public testing::WithParamInterface<Variant> { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionBrowserTest::SetUpCommandLine(command_line); |
| if (IsAPIFlagEnabled(GetParam())) { |
| command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, |
| "AIPromptAPI"); |
| } |
| |
| base::flat_map<base::test::FeatureRef, bool> feature_states; |
| if (IsAPIKillSwitchTriggered(GetParam())) { |
| feature_states[blink::features::kAIPromptAPI] = false; |
| } |
| if (IsExtensionKillSwitchTriggered(GetParam())) { |
| feature_states[blink::features::kAIPromptAPIForExtension] = false; |
| } |
| feature_list_.InitWithFeatureStates(feature_states); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix */, |
| MAYBE_ExtensionAILanguageModelBrowserTest, |
| testing::Combine(testing::Bool(), testing::Bool(), testing::Bool()), |
| &DescribeTestVariant); |
| |
| // Check whether the API is exposed to the extension worker when expected. |
| IN_PROC_BROWSER_TEST_P(MAYBE_ExtensionAILanguageModelBrowserTest, |
| ExposedToWorker) { |
| static constexpr char kScript[] = R"JS( |
| chrome.test.runTests([ |
| function verifyLanguageModelExposed() { |
| const expectLanguageModel = %s; |
| chrome.test.assertEq(expectLanguageModel, !!self.LanguageModel); |
| chrome.test.succeed(); |
| }, |
| ]); |
| )JS"; |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifestTemplate); |
| // Extension access is blocked by either kill switch. |
| const bool is_api_exposed = IsAPIFlagEnabled(GetParam()) || |
| (!IsAPIKillSwitchTriggered(GetParam()) && |
| !IsExtensionKillSwitchTriggered(GetParam())); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("sw.js"), |
| base::StringPrintf(kScript, base::ToString(is_api_exposed))); |
| ResultCatcher result_catcher; |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| // Invoke availability() for basic API functionality coverage beyond WPTs. |
| IN_PROC_BROWSER_TEST_P(MAYBE_ExtensionAILanguageModelBrowserTest, |
| AvailableInWorker) { |
| static constexpr char kScript[] = R"JS( |
| chrome.test.runTests([ |
| async function verifyLanguageModelAvailability() { |
| if (!!self.LanguageModel) { // Skip checking when not exposed. |
| const availability = await LanguageModel.availability(); |
| chrome.test.assertEq(typeof(availability), 'string'); |
| } |
| chrome.test.succeed(); |
| }, |
| ]); |
| )JS"; |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifestTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("sw.js"), kScript); |
| ResultCatcher result_catcher; |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| // TODO(crbug.com/421031829): Resolve underlying issue behind UnloadExtension. |
| UnloadExtension(extension->id()); |
| } |
| |
| } // namespace extensions |