[Service Workers] Add a test-only method to execute javascript

There are times that a C++ test needs to coordinate code execution
between the C++ and running JS code. For frame-based contexts, we
can use EvalJs() and ExecuteScript*() methods from content
browser_test_utils; however, these require a RenderFrameHost or
WebContents to target.

Introduce a new method to allow executing javascript in the context
of a running worker. This method is exposed on ServiceWorkerContext
(for public access outside the //content layer) and in
ServiceWorkerVersion (internally in the content layer), and then
funnels to a mojo message on the ServiceWorker mojom interface. This
is somewhat inspired by and analogous to the RenderFrame equivalents
(where the method is exposed on RenderFrameHost and pipes to the
Frame mojom interface).

The result of the running script is serialized to a base::Value, and
an error string is populated if an exception is reached in executing
the script.

Also add an extension usage of this, allowing execution of script in
an extensions background service worker.

Add browsertests for both the extension-specific functionality and the
new method on ServiceWorkerVersion.

Bug: 1280077
Change-Id: I03939c5e0bb99c23ed348ee1b9b1bfaeaa4d0942
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3342489
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Reviewed-by: Chris Hamilton <chrisha@chromium.org>
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#953723}
diff --git a/chrome/browser/extensions/browsertest_util_browsertest.cc b/chrome/browser/extensions/browsertest_util_browsertest.cc
new file mode 100644
index 0000000..70e3a5a
--- /dev/null
+++ b/chrome/browser/extensions/browsertest_util_browsertest.cc
@@ -0,0 +1,53 @@
+// Copyright 2021 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 "base/test/bind.h"
+#include "base/test/values_test_util.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/browsertest_util.h"
+#include "extensions/test/extension_test_message_listener.h"
+#include "extensions/test/test_extension_dir.h"
+
+namespace extensions {
+
+using BrowserTestUtilBrowserTest = ExtensionBrowserTest;
+
+// Tests the ability to run JS in an extension-registered service worker.
+IN_PROC_BROWSER_TEST_F(BrowserTestUtilBrowserTest,
+                       ExecuteScriptInServiceWorker) {
+  constexpr char kManifest[] =
+      R"({
+          "name": "Test",
+          "manifest_version": 3,
+          "background": {"service_worker": "background.js"},
+          "version": "0.1"
+        })";
+  constexpr char kBackgroundScript[] =
+      R"(self.myTestFlag = 'HELLO!';
+         chrome.test.sendMessage('ready');)";
+
+  TestExtensionDir test_dir;
+  test_dir.WriteManifest(kManifest);
+  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundScript);
+  ExtensionTestMessageListener listener("ready", /*will_reply=*/false);
+  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+  ASSERT_TRUE(listener.WaitUntilSatisfied());
+
+  base::RunLoop run_loop;
+  base::Value value_out;
+  auto callback = [&run_loop, &value_out](base::Value value) {
+    value_out = std::move(value);
+    run_loop.Quit();
+  };
+
+  browsertest_util::ExecuteScriptInServiceWorker(
+      profile(), extension->id(), "console.warn('script ran'); myTestFlag;",
+      base::BindLambdaForTesting(callback));
+  run_loop.Run();
+  EXPECT_THAT(value_out, base::test::IsJson(R"("HELLO!")"));
+}
+
+}  // namespace extensions
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 0c936b98..f501b489 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2631,6 +2631,7 @@
         "../browser/extensions/background_page_apitest.cc",
         "../browser/extensions/background_scripts_apitest.cc",
         "../browser/extensions/background_xhr_browsertest.cc",
+        "../browser/extensions/browsertest_util_browsertest.cc",
         "../browser/extensions/calculator_app_browsertest.cc",
         "../browser/extensions/chrome_app_api_browsertest.cc",
         "../browser/extensions/chrome_test_extension_loader_browsertest.cc",
diff --git a/components/performance_manager/service_worker_context_adapter.cc b/components/performance_manager/service_worker_context_adapter.cc
index 960a489..2bc7902 100644
--- a/components/performance_manager/service_worker_context_adapter.cc
+++ b/components/performance_manager/service_worker_context_adapter.cc
@@ -140,6 +140,14 @@
   return 0u;
 }
 
+bool ServiceWorkerContextAdapter::ExecuteScriptForTest(
+    const std::string& script,
+    int64_t version_id,
+    content::ServiceWorkerScriptExecutionCallback callback) {
+  NOTIMPLEMENTED();
+  return false;
+}
+
 bool ServiceWorkerContextAdapter::MaybeHasRegistrationForStorageKey(
     const blink::StorageKey& key) {
   NOTIMPLEMENTED();
diff --git a/components/performance_manager/service_worker_context_adapter.h b/components/performance_manager/service_worker_context_adapter.h
index e46c41b..2d39a76 100644
--- a/components/performance_manager/service_worker_context_adapter.h
+++ b/components/performance_manager/service_worker_context_adapter.h
@@ -65,6 +65,10 @@
       int64_t service_worker_version_id,
       const std::string& request_uuid) override;
   size_t CountExternalRequestsForTest(const blink::StorageKey& key) override;
+  bool ExecuteScriptForTest(
+      const std::string& script,
+      int64_t service_worker_version_id,
+      content::ServiceWorkerScriptExecutionCallback callback) override;
   bool MaybeHasRegistrationForStorageKey(const blink::StorageKey& key) override;
   void GetAllOriginsInfo(GetUsageInfoCallback callback) override;
   void DeleteForStorageKey(const blink::StorageKey& key,
diff --git a/content/browser/service_worker/fake_service_worker.cc b/content/browser/service_worker/fake_service_worker.cc
index 9e34f3f..06dc1e3 100644
--- a/content/browser/service_worker/fake_service_worker.cc
+++ b/content/browser/service_worker/fake_service_worker.cc
@@ -243,6 +243,13 @@
   NOTIMPLEMENTED();
 }
 
+void FakeServiceWorker::ExecuteScriptForTest(
+    const std::u16string& script,
+    bool wants_result,
+    ExecuteScriptForTestCallback callback) {
+  NOTIMPLEMENTED();
+}
+
 void FakeServiceWorker::OnConnectionError() {
   // Destroys |this|.
   helper_->RemoveServiceWorker(this);
diff --git a/content/browser/service_worker/fake_service_worker.h b/content/browser/service_worker/fake_service_worker.h
index fc4df77..8c5c3c8 100644
--- a/content/browser/service_worker/fake_service_worker.h
+++ b/content/browser/service_worker/fake_service_worker.h
@@ -134,6 +134,9 @@
   void SetIdleDelay(base::TimeDelta delay) override;
   void AddMessageToConsole(blink::mojom::ConsoleMessageLevel level,
                            const std::string& message) override;
+  void ExecuteScriptForTest(const std::u16string& script,
+                            bool wants_result,
+                            ExecuteScriptForTestCallback callback) override;
 
   virtual void OnConnectionError();
 
diff --git a/content/browser/service_worker/service_worker_context_wrapper.cc b/content/browser/service_worker/service_worker_context_wrapper.cc
index 5d65081..ae83b91c 100644
--- a/content/browser/service_worker/service_worker_context_wrapper.cc
+++ b/content/browser/service_worker/service_worker_context_wrapper.cc
@@ -516,6 +516,21 @@
   return version->StartExternalRequest(request_uuid);
 }
 
+bool ServiceWorkerContextWrapper::ExecuteScriptForTest(
+    const std::string& script,
+    int64_t service_worker_version_id,
+    ServiceWorkerScriptExecutionCallback callback) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  if (!context())
+    return false;
+  scoped_refptr<ServiceWorkerVersion> version =
+      context()->GetLiveVersion(service_worker_version_id);
+  if (!version)
+    return false;
+  version->ExecuteScriptForTest(script, std::move(callback));  // IN-TEST
+  return true;
+}
+
 ServiceWorkerExternalRequestResult
 ServiceWorkerContextWrapper::FinishedExternalRequest(
     int64_t service_worker_version_id,
diff --git a/content/browser/service_worker/service_worker_context_wrapper.h b/content/browser/service_worker/service_worker_context_wrapper.h
index ffd6d90..69ee43d 100644
--- a/content/browser/service_worker/service_worker_context_wrapper.h
+++ b/content/browser/service_worker/service_worker_context_wrapper.h
@@ -173,6 +173,10 @@
       int64_t service_worker_version_id,
       const std::string& request_uuid) override;
   size_t CountExternalRequestsForTest(const blink::StorageKey& key) override;
+  bool ExecuteScriptForTest(
+      const std::string& script,
+      int64_t service_worker_version_id,
+      ServiceWorkerScriptExecutionCallback callback) override;
   bool MaybeHasRegistrationForStorageKey(const blink::StorageKey& key) override;
   void GetAllOriginsInfo(GetUsageInfoCallback callback) override;
   void DeleteForStorageKey(const blink::StorageKey& key,
diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index 3d57adf..19c1d9d02 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -1079,6 +1079,17 @@
   return false;
 }
 
+void ServiceWorkerVersion::ExecuteScriptForTest(
+    const std::string& script,
+    ServiceWorkerScriptExecutionCallback callback) {
+  DCHECK(running_status() == EmbeddedWorkerStatus::STARTING ||
+         running_status() == EmbeddedWorkerStatus::RUNNING)
+      << "Cannot execute a script in a non-running worker!";
+  bool wants_result = !callback.is_null();
+  endpoint()->ExecuteScriptForTest(  // IN-TEST
+      base::UTF8ToUTF16(script), wants_result, std::move(callback));
+}
+
 void ServiceWorkerVersion::SetValidOriginTrialTokens(
     const blink::TrialTokenValidator::FeatureToTokensMap& tokens) {
   origin_trial_tokens_ = validator_.GetValidTokens(
diff --git a/content/browser/service_worker/service_worker_version.h b/content/browser/service_worker/service_worker_version.h
index c5f05a6..61620cf 100644
--- a/content/browser/service_worker/service_worker_version.h
+++ b/content/browser/service_worker/service_worker_version.h
@@ -643,6 +643,12 @@
   // Returns true if |process_id| is a controllee process ID of this version.
   bool IsControlleeProcessID(int process_id) const;
 
+  // Executes the given `script` in the associated worker. If `callback` is
+  // non-empty, invokes `callback` with the result of the script after
+  // execution. See also service_worker.mojom.
+  void ExecuteScriptForTest(const std::string& script,
+                            ServiceWorkerScriptExecutionCallback callback);
+
  private:
   friend class base::RefCounted<ServiceWorkerVersion>;
   friend class EmbeddedWorkerInstanceTest;
diff --git a/content/browser/service_worker/service_worker_version_browsertest.cc b/content/browser/service_worker/service_worker_version_browsertest.cc
index 70bf378d..98b4ebab 100644
--- a/content/browser/service_worker/service_worker_version_browsertest.cc
+++ b/content/browser/service_worker/service_worker_version_browsertest.cc
@@ -23,6 +23,7 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/task_traits.h"
 #include "base/test/bind.h"
+#include "base/test/values_test_util.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
 #include "content/browser/service_worker/embedded_worker_instance.h"
@@ -1451,6 +1452,57 @@
             version_->cross_origin_embedder_policy());
 }
 
+// Tests that JS can be executed in the context of a running service worker.
+IN_PROC_BROWSER_TEST_F(ServiceWorkerVersionBrowserTest,
+                       ExecuteScriptForTesting) {
+  StartServerAndNavigateToSetup();
+  SetUpRegistration("/service_worker/execute_script_worker.js");
+  ASSERT_EQ(StartWorker(), blink::ServiceWorkerStatusCode::kOk);
+  ASSERT_TRUE(version_);
+
+  auto execute_script_helper = [this](const std::string& script,
+                                      base::Value* value_out,
+                                      std::string* error_out) {
+    base::RunLoop run_loop;
+    auto callback = [&run_loop, value_out, error_out](
+                        base::Value value,
+                        const absl::optional<std::string>& error) {
+      *value_out = std::move(value);
+      *error_out = error.value_or("<no error>");
+      run_loop.Quit();
+    };
+
+    version_->ExecuteScriptForTest(script,
+                                   base::BindLambdaForTesting(callback));
+    run_loop.Run();
+  };
+
+  {
+    base::Value value;
+    std::string error;
+    execute_script_helper("self.workerFlag;", &value, &error);
+    EXPECT_THAT(value, base::test::IsJson(R"("worker flag")"));
+    EXPECT_EQ("<no error>", error);
+  }
+  {
+    base::Value value;
+    std::string error;
+    // Execute a script that will hit an exception.
+    execute_script_helper("foo = bar + baz;", &value, &error);
+    EXPECT_TRUE(value.is_none());
+    EXPECT_EQ("Uncaught ReferenceError: bar is not defined", error);
+  }
+  {
+    base::Value value;
+    std::string error;
+    // Execute a script that evaluates to undefined. This converts to an empty
+    // base::Value, and should not throw an error.
+    execute_script_helper("(function() { })();", &value, &error);
+    EXPECT_TRUE(value.is_none());
+    EXPECT_EQ("<no error>", error);
+  }
+}
+
 class ServiceWorkerVersionBrowserV8FullCodeCacheTest
     : public ServiceWorkerVersionBrowserTest,
       public ServiceWorkerVersion::Observer {
diff --git a/content/public/browser/service_worker_context.h b/content/public/browser/service_worker_context.h
index 0b0e5a1..4ca1629f 100644
--- a/content/public/browser/service_worker_context.h
+++ b/content/public/browser/service_worker_context.h
@@ -14,6 +14,7 @@
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/service_worker_external_request_result.h"
 #include "content/public/browser/service_worker_running_info.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/common/messaging/transferable_message.h"
 #include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom-forward.h"
@@ -63,6 +64,12 @@
   kMaxValue = FAILED,
 };
 
+// A callback invoked with the result of executing script in a service worker
+// context.
+using ServiceWorkerScriptExecutionCallback =
+    base::OnceCallback<void(base::Value value,
+                            const absl::optional<std::string>& error)>;
+
 // Represents the per-StoragePartition service worker data.
 //
 // See service_worker_context_wrapper.cc for the implementation
@@ -154,6 +161,14 @@
   // specified `key`.
   virtual size_t CountExternalRequestsForTest(const blink::StorageKey& key) = 0;
 
+  // Executes the given `script` in the context of the worker specified by the
+  // given `service_worker_version_id`. If non-empty, `callback` is invoked
+  // with the result of the script. See also service_worker.mojom.
+  virtual bool ExecuteScriptForTest(
+      const std::string& script,
+      int64_t service_worker_version_id,
+      ServiceWorkerScriptExecutionCallback callback) = 0;
+
   // Whether `key` has any registrations. Uninstalling and uninstalled
   // registrations do not cause this to return true, that is, only registrations
   // with status ServiceWorkerRegistration::Status::kIntact are considered, such
diff --git a/content/public/test/fake_service_worker_context.cc b/content/public/test/fake_service_worker_context.cc
index 160869c8..f61e31a 100644
--- a/content/public/test/fake_service_worker_context.cc
+++ b/content/public/test/fake_service_worker_context.cc
@@ -58,6 +58,13 @@
   NOTREACHED();
   return 0u;
 }
+bool FakeServiceWorkerContext::ExecuteScriptForTest(
+    const std::string& script,
+    int64_t version_id,
+    ServiceWorkerScriptExecutionCallback callback) {
+  NOTREACHED();
+  return false;
+}
 bool FakeServiceWorkerContext::MaybeHasRegistrationForStorageKey(
     const blink::StorageKey& key) {
   return registered_storage_keys_.find(key) != registered_storage_keys_.end();
diff --git a/content/public/test/fake_service_worker_context.h b/content/public/test/fake_service_worker_context.h
index 6b6ded5..76a2b4d 100644
--- a/content/public/test/fake_service_worker_context.h
+++ b/content/public/test/fake_service_worker_context.h
@@ -57,6 +57,10 @@
       int64_t service_worker_version_id,
       const std::string& request_uuid) override;
   size_t CountExternalRequestsForTest(const blink::StorageKey& key) override;
+  bool ExecuteScriptForTest(
+      const std::string& script,
+      int64_t service_worker_version_id,
+      ServiceWorkerScriptExecutionCallback callback) override;
   bool MaybeHasRegistrationForStorageKey(const blink::StorageKey& key) override;
   void GetAllOriginsInfo(GetUsageInfoCallback callback) override;
   void DeleteForStorageKey(const blink::StorageKey& key,
diff --git a/content/test/data/service_worker/execute_script_worker.js b/content/test/data/service_worker/execute_script_worker.js
new file mode 100644
index 0000000..3262db6c
--- /dev/null
+++ b/content/test/data/service_worker/execute_script_worker.js
@@ -0,0 +1,5 @@
+// Copyright 2021 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.
+
+self.workerFlag = 'worker flag';
diff --git a/extensions/browser/browsertest_util.cc b/extensions/browser/browsertest_util.cc
index efd004a..d9324d5 100644
--- a/extensions/browser/browsertest_util.cc
+++ b/extensions/browser/browsertest_util.cc
@@ -4,8 +4,12 @@
 
 #include "extensions/browser/browsertest_util.h"
 
+#include "base/callback.h"
+#include "content/public/browser/service_worker_context.h"
+#include "content/public/browser/storage_partition.h"
 #include "content/public/test/browser_test_utils.h"
 #include "extensions/browser/extension_host.h"
+#include "extensions/browser/extension_util.h"
 #include "extensions/browser/process_manager.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -60,5 +64,30 @@
   return true;
 }
 
+void ExecuteScriptInServiceWorker(
+    content::BrowserContext* browser_context,
+    const std::string& extension_id,
+    const std::string& script,
+    base::OnceCallback<void(base::Value)> callback) {
+  ProcessManager* process_manager = ProcessManager::Get(browser_context);
+  ASSERT_TRUE(process_manager);
+  std::vector<WorkerId> worker_ids =
+      process_manager->GetServiceWorkersForExtension(extension_id);
+  ASSERT_EQ(1u, worker_ids.size())
+      << "Incorrect number of workers registered for extension.";
+  content::ServiceWorkerContext* service_worker_context =
+      util::GetStoragePartitionForExtensionId(extension_id, browser_context)
+          ->GetServiceWorkerContext();
+  auto callback_adapter =
+      [](base::OnceCallback<void(base::Value)> original_callback,
+         base::Value value, const absl::optional<std::string>& error) {
+        ASSERT_FALSE(error.has_value()) << *error;
+        std::move(original_callback).Run(std::move(value));
+      };
+  service_worker_context->ExecuteScriptForTest(  // IN-TEST
+      script, worker_ids[0].version_id,
+      base::BindOnce(callback_adapter, std::move(callback)));
+}
+
 }  // namespace browsertest_util
 }  // namespace extensions
diff --git a/extensions/browser/browsertest_util.h b/extensions/browser/browsertest_util.h
index 9774259..44dcd3ed 100644
--- a/extensions/browser/browsertest_util.h
+++ b/extensions/browser/browsertest_util.h
@@ -7,6 +7,12 @@
 
 #include <string>
 
+#include "base/callback_forward.h"
+
+namespace base {
+class Value;
+}  // namespace base
+
 namespace content {
 class BrowserContext;
 }  // namespace content
@@ -42,6 +48,15 @@
                                          const std::string& extension_id,
                                          const std::string& script);
 
+// Executes the given `script` in the context of the background service worker
+// registered by the extension with the given `extension_id`. If non-empty,
+// `callback` is invoked with the result of the execution.
+void ExecuteScriptInServiceWorker(
+    content::BrowserContext* browser_context,
+    const std::string& extension_id,
+    const std::string& script,
+    base::OnceCallback<void(base::Value)> callback);
+
 }  // namespace browsertest_util
 }  // namespace extensions
 
diff --git a/third_party/blink/public/mojom/service_worker/service_worker.mojom b/third_party/blink/public/mojom/service_worker/service_worker.mojom
index dfc95f9..5355f73 100644
--- a/third_party/blink/public/mojom/service_worker/service_worker.mojom
+++ b/third_party/blink/public/mojom/service_worker/service_worker.mojom
@@ -7,6 +7,7 @@
 import "mojo/public/mojom/base/read_only_buffer.mojom";
 import "mojo/public/mojom/base/string16.mojom";
 import "mojo/public/mojom/base/time.mojom";
+import "mojo/public/mojom/base/values.mojom";
 import "services/network/public/mojom/cookie_manager.mojom";
 import "services/network/public/mojom/url_loader_factory.mojom";
 import "third_party/blink/public/mojom/background_fetch/background_fetch.mojom";
@@ -307,4 +308,19 @@
 
   // Adds a message to DevTools console which is associated with this worker.
   AddMessageToConsole(ConsoleMessageLevel level, string message);
+
+  // Executes the given `javascript` in the context of the running worker.
+  // `wants_result` indicates whether a result from the executing `javascript`
+  // should be returned via the provided callback.
+  // Currently, this only supports synchronous script execution, and the
+  // returned value is the result of the final execution in the provided
+  // javascript.
+  // If an exception is thrown, a stringified version is returned through the
+  // error parameter.
+  // TODO(devlin): Expand this to allow returning a promise for asynchronous
+  // execution.
+  ExecuteScriptForTest(
+      mojo_base.mojom.BigString16 javascript,
+      bool wants_result)
+      => (mojo_base.mojom.Value result, string? error);
 };
diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc
index 0c749e04..54e55b40 100644
--- a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc
+++ b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc
@@ -57,6 +57,7 @@
 #include "third_party/blink/public/platform/modules/service_worker/web_service_worker_error.h"
 #include "third_party/blink/public/platform/platform.h"
 #include "third_party/blink/public/platform/web_url.h"
+#include "third_party/blink/public/platform/web_v8_value_converter.h"
 #include "third_party/blink/renderer/bindings/core/v8/callback_promise_adapter.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
@@ -82,6 +83,7 @@
 #include "third_party/blink/renderer/core/messaging/message_port.h"
 #include "third_party/blink/renderer/core/origin_trials/origin_trial_context.h"
 #include "third_party/blink/renderer/core/probe/core_probes.h"
+#include "third_party/blink/renderer/core/script/classic_script.h"
 #include "third_party/blink/renderer/core/trustedtypes/trusted_script_url.h"
 #include "third_party/blink/renderer/core/workers/global_scope_creation_params.h"
 #include "third_party/blink/renderer/core/workers/worker_classic_script_loader.h"
@@ -127,6 +129,7 @@
 #include "third_party/blink/renderer/modules/service_worker/wait_until_observer.h"
 #include "third_party/blink/renderer/modules/service_worker/web_service_worker_fetch_context_impl.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
 #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/loader/fetch/fetch_client_settings_object_snapshot.h"
@@ -2460,6 +2463,83 @@
                               /* column_number= */ 0)));
 }
 
+void ServiceWorkerGlobalScope::ExecuteScriptForTest(
+    const String& javascript,
+    bool wants_result,
+    ExecuteScriptForTestCallback callback) {
+  ScriptState* script_state = ScriptController()->GetScriptState();
+  v8::Isolate* isolate = script_state->GetIsolate();
+  v8::HandleScope handle_scope(isolate);
+
+  // It's safe to use `kDoNotSanitize` because this method is for testing only.
+  ClassicScript* script = ClassicScript::CreateUnspecifiedScript(
+      javascript, ScriptSourceLocationType::kUnknown,
+      SanitizeScriptErrors::kDoNotSanitize);
+
+  v8::TryCatch try_catch(isolate);
+  ScriptEvaluationResult result = script->RunScriptOnScriptStateAndReturnValue(
+      script_state, ExecuteScriptPolicy::kDoNotExecuteScriptWhenScriptsDisabled,
+      V8ScriptRunner::RethrowErrorsOption::Rethrow(String()));
+
+  // If the script throws an error, the returned value is a stringification of
+  // the error message.
+  if (try_catch.HasCaught()) {
+    String exception_string;
+    if (try_catch.Message().IsEmpty() || try_catch.Message()->Get().IsEmpty()) {
+      exception_string = "Unknown exception while executing script.";
+    } else {
+      exception_string = ToCoreStringWithNullCheck(try_catch.Message()->Get());
+    }
+    std::move(callback).Run(base::Value(), std::move(exception_string));
+    return;
+  }
+
+  // If the script didn't want a result, just return immediately.
+  if (!wants_result) {
+    std::move(callback).Run(base::Value(), String());
+    return;
+  }
+
+  // Otherwise, the script should have succeeded, and we return the value from
+  // the execution.
+  DCHECK_EQ(ScriptEvaluationResult::ResultType::kSuccess,
+            result.GetResultType());
+
+  v8::Local<v8::Value> v8_result = result.GetSuccessValue();
+  DCHECK(!v8_result.IsEmpty());
+
+  // Only convert the value to a base::Value if it's not null or undefined.
+  // Null and undefined are valid results, but will fail to convert using the
+  // WebV8ValueConverter. They are accurately represented (though no longer
+  // distinguishable) by the empty base::Value.
+  if (v8_result->IsNullOrUndefined()) {
+    std::move(callback).Run(base::Value(), String());
+    return;
+  }
+
+  base::Value value;
+  String exception;
+
+  // TODO(devlin): Is this thread-safe? Platform::Current() is set during
+  // blink initialization and the created V8ValueConverter is constructed
+  // without any special access, but it's *possible* a future implementation
+  // here would be thread-unsafe (if it relied on member data in Platform).
+  std::unique_ptr<WebV8ValueConverter> converter =
+      Platform::Current()->CreateWebV8ValueConverter();
+  converter->SetDateAllowed(true);
+  converter->SetRegExpAllowed(true);
+
+  std::unique_ptr<base::Value> converted_value =
+      converter->FromV8Value(v8_result, script_state->GetContext());
+  if (!converted_value) {
+    std::move(callback).Run(base::Value(),
+                            "Failed to convert V8 result from script");
+    return;
+  }
+
+  std::move(callback).Run(std::move(*converted_value), String());
+}
+
 void ServiceWorkerGlobalScope::NoteNewFetchEvent(
     const mojom::blink::FetchAPIRequest& request) {
   int range_increment = request.headers.Contains(http_names::kRange) ? 1 : 0;
diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h
index 67f3ed0a..0375f00 100644
--- a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h
+++ b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h
@@ -486,6 +486,9 @@
   void SetIdleDelay(base::TimeDelta delay) override;
   void AddMessageToConsole(mojom::blink::ConsoleMessageLevel,
                            const String& message) override;
+  void ExecuteScriptForTest(const String& script,
+                            bool wants_result,
+                            ExecuteScriptForTestCallback callback) override;
 
   void NoteNewFetchEvent(const mojom::blink::FetchAPIRequest& request);
   void NoteRespondedToFetchEvent(const KURL& request_url, bool range_request);
diff --git a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
index 8c9a23d..eca2f3d 100755
--- a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
+++ b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
@@ -1641,6 +1641,15 @@
             'net::RedirectInfo',
         ],
     },
+    {
+        # base::Value is used in test-only script execution in worker contexts.
+        'paths': [
+            'third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc',
+        ],
+        'allowed': [
+            'base::Value',
+        ],
+    },
 ]