blob: a1d5a79b9dbc40e6318cbbaefc11f644872b0810 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/embedder_support/switches.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "third_party/blink/public/common/features_generated.h"
namespace {
// This is the public key of tools/origin_trials/eftest.key, used to validate
// origin trial tokens generated by tools/origin_trials/generate_token.py.
// https://chromium.googlesource.com/chromium/src/+/main/docs/origin_trials_integration.md
constexpr char kOriginTrialPublicKeyForTesting[] =
"dRCs+TocuKkocNKa0AtZ4awrt9XKH2SQCI6o4FY6BNA=";
// Origin trial tokens (expire on 2033-08-06) generated by
// tools/origin_trials/generate_token.py https://a.test:32123 AIFooAPI \
// --expire-days 3000
constexpr char kAIRewriterAPIOTToken[] =
"A7gvtQAwPhmBOadB9rGCwqWwgmba7wU+zXqjfDR9cfTzR8Xi2Tkedxawd/"
"PMg4SLjABtNGJZf3Iel4zqG/"
"iqZQ8AAABUeyJvcmlnaW4iOiAiaHR0cHM6Ly9hLnRlc3Q6MzIxMjMiLCAiZmVhdHVyZSI6ICJB"
"SVJld3JpdGVyQVBJIiwgImV4cGlyeSI6IDIwMDY5NzA3NDF9";
constexpr char kAIWriterAPIOTToken[] =
"A0jJGgLmqGgNaHNH7my4hKMTvp7oBOvGoLvZhH3tzAGKY3SNkmSQCSTxFtgXNGxloQ7rFqxaut"
"85MKQRKEug+"
"Q4AAABSeyJvcmlnaW4iOiAiaHR0cHM6Ly9hLnRlc3Q6MzIxMjMiLCAiZmVhdHVyZSI6ICJBSVd"
"yaXRlckFQSSIsICJleHBpcnkiOiAyMDA2OTcwNjU4fQ==";
// Execute script on the current Window and yield the posted message.
constexpr char kRunWindowCheck[] = R"JS(
new Promise(r => { self.onmessage = e => { r(e.data); }; %s });
)JS";
// Execute script on a new Worker and yield the posted message.
constexpr char kRunWorkerCheck[] = R"JS(
const workerScript = `%s`;
const blob = new Blob([workerScript], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
new Promise(r => { worker.onmessage = e => { r(e.data); }});
)JS";
// Check if a global identifier is exposed and post an OK/error message.
constexpr char kCheckExposed[] = R"JS(
try { %s; self.postMessage('OK');
} catch (e) { self.postMessage(e.name); }
)JS";
// Check if FooAPI.availability() yields a string and post an OK/error message.
constexpr char kCheckAvailability[] = R"JS(
try { %s.availability().then(a => {
self.postMessage(typeof(a) == 'string' ? 'OK' : 'NO'); });
} catch (e) { self.postMessage(e.name); }
)JS";
// The boolean tuple describing:
// 1. if the `kAIFooAPI` chrome://flag entries are explicitly enabled;
// 2. if the `kAIFooAPIForWorkers` are explicitly enabled;
// 3. if the `kAIFooAPI` kill switches are triggered;
// 4. if the `kAIFooAPI` OT tokens are supplied (for any APIs in OT).
using Variant = std::tuple<bool, bool, bool, bool>;
bool IsAPIFlagEnabled(Variant v) {
return std::get<0>(v);
}
bool IsAPIWorkerFlagEnabled(Variant v) {
return std::get<1>(v);
}
bool IsAPIKillSwitchTriggered(Variant v) {
return std::get<2>(v);
}
bool IsOTTokenSupplied(Variant v) {
return std::get<3>(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 = IsAPIFlagEnabled(info.param) ? "FlagEnabledByUser"
: "FlagNotEnabledByUser";
std::string worker_flag =
IsAPIWorkerFlagEnabled(info.param) ? "WithWorkerFlag" : "NoWorkerFlag";
std::string kill_switch = IsAPIKillSwitchTriggered(info.param)
? "WithAPIKillswitch"
: "NoAPIKillswitch";
std::string ot_token =
IsOTTokenSupplied(info.param) ? "WithOTToken" : "NoOTToken";
return base::JoinString({api_flag, worker_flag, kill_switch, ot_token}, "_");
}
// Get the names of all the APIs tested in this suite.
std::vector<std::string> GetAPINames() {
return {"LanguageModel", "Rewriter", "Summarizer", "Writer"};
}
// Returns whether the API is enabled by default.
bool IsAPIEnabledByDefault(std::string_view name) {
return name == "Summarizer";
}
// Returns whether the API name matches those currently in origin trial.
bool IsAPIInOT(std::string_view name) {
return name == "Rewriter" || name == "Writer";
}
// Injects an Origin Trial `token` into the page.
void InjectOTToken(content::WebContents* tab, std::string_view token) {
static constexpr char kScript[] =
R"JS(
const meta = document.createElement('meta');
meta.httpEquiv = 'origin-trial';
meta.content = '%s';
document.head.appendChild(meta);
)JS";
EXPECT_TRUE(ExecJs(tab, base::StringPrintf(kScript, token)));
}
// TODO(crbug.com/419321441): Support Built-In AI APIs on ChromeOS.
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_AIOnDeviceBrowserTest DISABLED_AIOnDeviceBrowserTest
#else
#define MAYBE_AIOnDeviceBrowserTest AIOnDeviceBrowserTest
#endif // BUILDFLAG(IS_CHROMEOS)
class MAYBE_AIOnDeviceBrowserTest
: public InProcessBrowserTest,
public testing::WithParamInterface<Variant> {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
if (IsAPIFlagEnabled(GetParam())) {
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"AIPromptAPI,AIRewriterAPI,"
"AISummarizationAPI,AIWriterAPI");
}
if (IsAPIWorkerFlagEnabled(GetParam())) {
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"AIPromptAPIForWorkers,"
"AIRewriterAPIForWorkers,"
"AISummarizationAPIForWorkers,"
"AIWriterAPIForWorkers");
}
// Specify the OT test public key to make the test token effective.
command_line->AppendSwitchASCII(embedder_support::kOriginTrialPublicKey,
kOriginTrialPublicKeyForTesting);
if (IsAPIKillSwitchTriggered(GetParam())) {
base::flat_map<base::test::FeatureRef, bool> feature_states;
feature_states[blink::features::kAIPromptAPI] = false;
feature_states[blink::features::kAIRewriterAPI] = false;
feature_states[blink::features::kAISummarizationAPI] = false;
feature_states[blink::features::kAIWriterAPI] = false;
feature_list_.InitWithFeatureStates(feature_states);
}
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_https_test_server().SetSSLConfig(
net::EmbeddedTestServer::CERT_TEST_NAMES);
net::test_server::RegisterDefaultHandlers(&embedded_https_test_server());
// Specify a port to match the generated test OT tokens.
// TODO(421053094): Remove port and move to browser_tests target after OTs.
ASSERT_TRUE(embedded_https_test_server().Start(/*port=*/32123));
auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
GURL url(embedded_https_test_server().GetURL("a.test", "/empty.html"));
ASSERT_TRUE(NavigateToURL(tab, url));
if (IsOTTokenSupplied(GetParam())) {
InjectOTToken(tab, kAIRewriterAPIOTToken);
InjectOTToken(tab, kAIWriterAPIOTToken);
}
}
bool ExpectExposedToWindow(std::string_view name) const {
return IsAPIFlagEnabled(GetParam()) ||
((IsAPIEnabledByDefault(name) ||
(IsAPIInOT(name) && IsOTTokenSupplied(GetParam()))) &&
!IsAPIKillSwitchTriggered(GetParam()));
}
bool ExpectExposedToWorker(std::string_view name) const {
// Worker access requires an additional flag, even with a valid OT.
return ExpectExposedToWindow(name) && IsAPIWorkerFlagEnabled(GetParam());
}
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
MAYBE_AIOnDeviceBrowserTest,
testing::Combine(testing::Bool(),
testing::Bool(),
testing::Bool(),
testing::Bool()),
&DescribeTestVariant);
// Check whether the APIs are exposed to the window or worker when expected.
IN_PROC_BROWSER_TEST_P(MAYBE_AIOnDeviceBrowserTest, ExposedToWindowOrWorker) {
auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
for (const auto& name : GetAPINames()) {
auto check = absl::StrFormat(kCheckExposed, name);
SCOPED_TRACE(testing::Message() << "Checking " << name);
EXPECT_EQ(ExpectExposedToWindow(name) ? "OK" : "ReferenceError",
content::EvalJs(tab, absl::StrFormat(kRunWindowCheck, check)));
EXPECT_EQ(ExpectExposedToWorker(name) ? "OK" : "ReferenceError",
content::EvalJs(tab, absl::StrFormat(kRunWorkerCheck, check)));
}
}
// Invoke availability() for basic API functionality coverage beyond WPTs.
IN_PROC_BROWSER_TEST_P(MAYBE_AIOnDeviceBrowserTest, AvailableInWindowOrWorker) {
auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
for (const auto& name : GetAPINames()) {
auto check = absl::StrFormat(kCheckAvailability, name);
SCOPED_TRACE(testing::Message() << "Checking " << name);
EXPECT_EQ(ExpectExposedToWindow(name) ? "OK" : "ReferenceError",
content::EvalJs(tab, absl::StrFormat(kRunWindowCheck, check)));
EXPECT_EQ(ExpectExposedToWorker(name) ? "OK" : "ReferenceError",
content::EvalJs(tab, absl::StrFormat(kRunWorkerCheck, check)));
}
}
} // namespace