File: ai_on_device_browsertest.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (236 lines) | stat: -rw-r--r-- 9,947 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// 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