// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file tests that Service Workers (a Content feature) work in the Chromium
// embedder.

#include <optional>
#include <string_view>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/numerics/safe_conversions.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/service_worker/service_worker_prewarm.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/favicon/content/content_favicon_driver.h"
#include "components/favicon/core/favicon_driver_observer.h"
#include "components/metrics/content/subprocess_metrics_provider.h"
#include "components/page_load_metrics/browser/observers/service_worker_page_load_metrics_observer.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "components/prefs/pref_service.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/page.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/messaging/string_message_codec.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
#include "url/origin.h"

using PageLoadMetricsTestWaiter = page_load_metrics::PageLoadMetricsTestWaiter;

namespace chrome_service_worker_browser_test {

const char kInstallAndWaitForActivatedPage[] =
    "<script>"
    "navigator.serviceWorker.register('./sw.js', {scope: './scope/'})"
    "  .then(function(reg) {"
    "      reg.addEventListener('updatefound', function() {"
    "          var worker = reg.installing;"
    "          worker.addEventListener('statechange', function() {"
    "              if (worker.state == 'activated')"
    "                document.title = 'READY';"
    "            });"
    "        });"
    "    });"
    "</script>";

const char kInstallAndWaitForActivatedPageWithModuleScript[] =
    R"(<script>
    navigator.serviceWorker.register(
        './sw.js', {scope: './scope/', type: 'module'})
      .then(function(reg) {
          reg.addEventListener('updatefound', function() {
              var worker = reg.installing;
              worker.addEventListener('statechange', function() {
                  if (worker.state == 'activated')
                    document.title = 'READY';
                });
            });
        });
    </script>)";

template <typename T>
static void ExpectResultAndRun(T expected,
                               base::OnceClosure continuation,
                               T actual) {
  EXPECT_EQ(expected, actual);
  std::move(continuation).Run();
}

class ChromeServiceWorkerTest : public InProcessBrowserTest {
 public:
  ChromeServiceWorkerTest(const ChromeServiceWorkerTest&) = delete;
  ChromeServiceWorkerTest& operator=(const ChromeServiceWorkerTest&) = delete;

 protected:
  ChromeServiceWorkerTest() {
    EXPECT_TRUE(service_worker_dir_.CreateUniqueTempDir());
    EXPECT_TRUE(base::CreateDirectoryAndGetError(
        service_worker_dir_.GetPath().Append(FILE_PATH_LITERAL("scope")),
        nullptr));
  }
  ~ChromeServiceWorkerTest() override = default;

  void WriteFile(const base::FilePath::StringType& filename,
                 std::string_view contents) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    EXPECT_TRUE(base::WriteFile(service_worker_dir_.GetPath().Append(filename),
                                contents));
  }

  void NavigateToPageAndWaitForReadyTitle(const std::string path) {
    const std::u16string expected_title1 = u"READY";
    content::TitleWatcher title_watcher1(
        browser()->tab_strip_model()->GetActiveWebContents(), expected_title1);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL(path)));
    EXPECT_EQ(expected_title1, title_watcher1.WaitAndGetTitle());
  }

  void InitializeServer() {
    embedded_test_server()->ServeFilesFromDirectory(
        service_worker_dir_.GetPath());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  content::ServiceWorkerContext* GetServiceWorkerContext() {
    return browser()
        ->GetProfile()
        ->GetDefaultStoragePartition()
        ->GetServiceWorkerContext();
  }

  void TestFallbackMainResourceRequestWhenJSDisabled(const char* test_script) {
    WriteFile(FILE_PATH_LITERAL("sw.js"),
              "self.onfetch = function(e) {"
              "  e.respondWith(new Response('<title>Fail</title>',"
              "                             {headers: {"
              "                             'Content-Type': 'text/html'}}));"
              "};");
    WriteFile(FILE_PATH_LITERAL("scope/done.html"), "<title>Done</title>");
    WriteFile(FILE_PATH_LITERAL("test.html"), test_script);
    InitializeServer();
    NavigateToPageAndWaitForReadyTitle("/test.html");

    GetServiceWorkerContext()->StopAllServiceWorkersForStorageKey(
        blink::StorageKey::CreateFirstParty(
            url::Origin::Create(embedded_test_server()->base_url())));
    HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile())
        ->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT,
                                   CONTENT_SETTING_BLOCK);

    const std::u16string expected_title = u"Done";
    content::TitleWatcher title_watcher(
        browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/scope/done.html")));

    EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());

    content::RenderFrameHost* main_frame = browser()
                                               ->tab_strip_model()
                                               ->GetActiveWebContents()
                                               ->GetPrimaryMainFrame();
    EXPECT_TRUE(
        content_settings::PageSpecificContentSettings::GetForFrame(main_frame)
            ->IsContentBlocked(ContentSettingsType::JAVASCRIPT));
  }

  void TestStartServiceWorkerAndDispatchMessage(const char* test_script) {
    base::RunLoop run_loop;
    const std::u16string message_data = u"testMessage";

    WriteFile(FILE_PATH_LITERAL("sw.js"), "self.onfetch = function(e) {};");
    WriteFile(FILE_PATH_LITERAL("test.html"), test_script);

    InitializeServer();
    NavigateToPageAndWaitForReadyTitle("/test.html");
    blink::TransferableMessage msg =
        blink::EncodeWebMessagePayload(message_data);
    msg.sender_agent_cluster_id = base::UnguessableToken::Create();

    GURL url = embedded_test_server()->GetURL("/scope/");
    GetServiceWorkerContext()->StartServiceWorkerAndDispatchMessage(
        url, blink::StorageKey::CreateFirstParty(url::Origin::Create(url)),
        std::move(msg),
        base::BindRepeating(&ExpectResultAndRun<bool>, true,
                            run_loop.QuitClosure()));
    run_loop.Run();
  }

  std::unique_ptr<PageLoadMetricsTestWaiter> CreatePageLoadMetricsTestWaiter() {
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    return std::make_unique<PageLoadMetricsTestWaiter>(web_contents);
  }

  base::ScopedTempDir service_worker_dir_;
};

// http://crbug.com/40363335
IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       CanShutDownWithRegisteredServiceWorker) {
  WriteFile(FILE_PATH_LITERAL("service_worker.js"), "");

  embedded_test_server()->ServeFilesFromDirectory(
      service_worker_dir_.GetPath());
  ASSERT_TRUE(embedded_test_server()->Start());

  base::RunLoop run_loop;
  blink::mojom::ServiceWorkerRegistrationOptions options(
      embedded_test_server()->GetURL("/"), blink::mojom::ScriptType::kClassic,
      blink::mojom::ServiceWorkerUpdateViaCache::kImports);
  const blink::StorageKey key =
      blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
  GetServiceWorkerContext()->RegisterServiceWorker(
      embedded_test_server()->GetURL("/service_worker.js"), key, options,
      content::GlobalRenderFrameHostId(),
      base::BindOnce(&ExpectResultAndRun<blink::ServiceWorkerStatusCode>,
                     blink::ServiceWorkerStatusCode::kOk,
                     run_loop.QuitClosure()));
  run_loop.Run();

  // Leave the Service Worker registered, and make sure that the browser can
  // shut down without DCHECK'ing. It'd be nice to check here that the SW is
  // actually occupying a process, but we don't yet have the public interface to
  // do that.
}

// http://crbug.com/40387150
IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       CanCloseIncognitoWindowWithServiceWorkerController) {
  WriteFile(FILE_PATH_LITERAL("service_worker.js"), "");
  WriteFile(FILE_PATH_LITERAL("service_worker.js.mock-http-headers"),
            "HTTP/1.1 200 OK\nContent-Type: text/javascript");
  WriteFile(FILE_PATH_LITERAL("test.html"), "");
  InitializeServer();

  Browser* incognito = CreateIncognitoBrowser();

  base::RunLoop run_loop;
  blink::mojom::ServiceWorkerRegistrationOptions options(
      embedded_test_server()->GetURL("/"), blink::mojom::ScriptType::kClassic,
      blink::mojom::ServiceWorkerUpdateViaCache::kImports);
  const blink::StorageKey key =
      blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
  GetServiceWorkerContext()->RegisterServiceWorker(
      embedded_test_server()->GetURL("/service_worker.js"), key, options,
      content::GlobalRenderFrameHostId(),
      base::BindOnce(&ExpectResultAndRun<blink::ServiceWorkerStatusCode>,
                     blink::ServiceWorkerStatusCode::kOk,
                     run_loop.QuitClosure()));
  run_loop.Run();

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      incognito, embedded_test_server()->GetURL("/test.html")));

  CloseBrowserSynchronously(incognito);

  // Test passes if we don't crash.
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       FailRegisterServiceWorkerWhenJSDisabled) {
  WriteFile(FILE_PATH_LITERAL("service_worker.js"), "");
  InitializeServer();

  HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile())
      ->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT,
                                 CONTENT_SETTING_BLOCK);

  base::RunLoop run_loop;
  blink::mojom::ServiceWorkerRegistrationOptions options(
      embedded_test_server()->GetURL("/"), blink::mojom::ScriptType::kClassic,
      blink::mojom::ServiceWorkerUpdateViaCache::kImports);
  const blink::StorageKey key =
      blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
  GetServiceWorkerContext()->RegisterServiceWorker(
      embedded_test_server()->GetURL("/service_worker.js"), key, options,
      content::GlobalRenderFrameHostId(),
      base::BindOnce(&ExpectResultAndRun<blink::ServiceWorkerStatusCode>,
                     blink::ServiceWorkerStatusCode::kErrorDisallowed,
                     run_loop.QuitClosure()));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(
    ChromeServiceWorkerTest,
    FallbackMainResourceRequestWhenJSDisabledForClassicServiceWorker) {
  TestFallbackMainResourceRequestWhenJSDisabled(
      kInstallAndWaitForActivatedPage);
}

IN_PROC_BROWSER_TEST_F(
    ChromeServiceWorkerTest,
    FallbackMainResourceRequestWhenJSDisabledForModuleServiceWorker) {
  TestFallbackMainResourceRequestWhenJSDisabled(
      kInstallAndWaitForActivatedPageWithModuleScript);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       StartServiceWorkerAndDispatchMessage) {
  TestStartServiceWorkerAndDispatchMessage(kInstallAndWaitForActivatedPage);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       StartServiceWorkerWithModuleScriptAndDispatchMessage) {
  TestStartServiceWorkerAndDispatchMessage(
      kInstallAndWaitForActivatedPageWithModuleScript);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest, SubresourceCountUKM) {
  base::RunLoop ukm_loop;
  ukm::TestAutoSetUkmRecorder test_recorder;
  test_recorder.SetOnAddEntryCallback(
      ukm::builders::ServiceWorker_OnLoad::kEntryName,
      // In the following test, there are two kinds of sub resources loaded;
      // one is handled with "respondWith", and the other is not.
      // `ukm_loop.Quit()` is called when both of them are recorded in UKM.
      base::BindLambdaForTesting([&]() {
        auto entries = test_recorder.GetEntriesByName(
            ukm::builders::ServiceWorker_OnLoad::kEntryName);
        CHECK(!entries.empty());
        const int64_t* v = ukm::TestAutoSetUkmRecorder::GetEntryMetric(
            entries[0],
            ukm::builders::ServiceWorker_OnLoad::kTotalSubResourceLoadName);
        CHECK(v);
        if (*v == 2) {
          ukm_loop.Quit();
        }
      }));

  WriteFile(FILE_PATH_LITERAL("fallback.css"), "");
  WriteFile(FILE_PATH_LITERAL("nofallback.css"), "");
  WriteFile(FILE_PATH_LITERAL("subresources.html"),
            "<link href='./fallback.css' rel='stylesheet'>"
            "<link href='./nofallback.css' rel='stylesheet'>");
  WriteFile(FILE_PATH_LITERAL("sw.js"),
            "this.onactivate = function(event) {"
            "  event.waitUntil(self.clients.claim());"
            "};"
            "this.onfetch = function(event) {"
            // We will fallback fallback.css.
            "  if (event.request.url.endsWith('/fallback.css')) {"
            "    return;"
            "  }"
            "  event.respondWith(fetch(event.request));"
            "};");
  WriteFile(FILE_PATH_LITERAL("test.html"),
            "<script>"
            "navigator.serviceWorker.register('./sw.js', {scope: './'})"
            "  .then(function(reg) {"
            "      reg.addEventListener('updatefound', function() {"
            "          var worker = reg.installing;"
            "          worker.addEventListener('statechange', function() {"
            "              if (worker.state == 'activated')"
            "                document.title = 'READY';"
            "            });"
            "        });"
            "    });"
            "</script>");

  InitializeServer();

  {
    // The message "READY" will be sent when the service worker is activated.
    const std::u16string expected_title = u"READY";
    content::TitleWatcher title_watcher(
        browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/test.html")));
    EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
  }

  {
    // Navigate to the service worker controlled page.
    auto waiter = CreatePageLoadMetricsTestWaiter();
    waiter->AddPageExpectation(
        PageLoadMetricsTestWaiter::TimingField::kLoadEvent);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/subresources.html")));
    waiter->Wait();
  }

  // Navigate away to record metrics.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  // Wait until the UKM record has enough entries.
  ukm_loop.Run();

  auto entries = test_recorder.GetEntriesByName(
      ukm::builders::ServiceWorker_OnLoad::kEntryName);
  ASSERT_EQ(entries.size(), 1u);
  test_recorder.ExpectEntryMetric(
      entries[0],
      ukm::builders::ServiceWorker_OnLoad::kMainAndSubResourceLoadLocationName,
      6 /* = kMainResourceNotFallbackAndSubResourceMixed */);
  test_recorder.ExpectEntryMetric(
      entries[0],
      ukm::builders::ServiceWorker_OnLoad::kTotalSubResourceLoadName, 2);
  test_recorder.ExpectEntryMetric(
      entries[0],
      ukm::builders::ServiceWorker_OnLoad::kTotalSubResourceFallbackName, 1);
  test_recorder.ExpectEntryMetric(
      entries[0],
      ukm::builders::ServiceWorker_OnLoad::kSubResourceFallbackRatioName, 50);
  test_recorder.ExpectEntryMetric(
      entries[0], ukm::builders::ServiceWorker_OnLoad::kAudioFallbackName, 0);
  test_recorder.ExpectEntryMetric(
      entries[0], ukm::builders::ServiceWorker_OnLoad::kAudioHandledName, 0);
  test_recorder.ExpectEntryMetric(
      entries[0],
      ukm::builders::ServiceWorker_OnLoad::kCSSStyleSheetFallbackName, 1);
  test_recorder.ExpectEntryMetric(
      entries[0],
      ukm::builders::ServiceWorker_OnLoad::kCSSStyleSheetHandledName, 1);
  test_recorder.ExpectEntryMetric(
      entries[0], ukm::builders::ServiceWorker_OnLoad::kFontFallbackName, 0);
  test_recorder.ExpectEntryMetric(
      entries[0], ukm::builders::ServiceWorker_OnLoad::kFontHandledName, 0);
  test_recorder.ExpectEntryMetric(
      entries[0], ukm::builders::ServiceWorker_OnLoad::kImageFallbackName, 0);
  test_recorder.ExpectEntryMetric(
      entries[0], ukm::builders::ServiceWorker_OnLoad::kImageHandledName, 0);
}

// TODO(crbug.com/480291001): The test is flaky. Re-enable it.
IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       DISABLED_StaticRoutingAPISubresourceHistogramTest) {
  base::HistogramTester histogram_tester;
  WriteFile(FILE_PATH_LITERAL("scope/fallback.css"), "");
  WriteFile(FILE_PATH_LITERAL("scope/nofallback.css"), "");
  WriteFile(FILE_PATH_LITERAL("scope/subresources.html"),
            "<link href='./fallback.css' rel='stylesheet'>"
            "<link href='./nofallback.css' rel='stylesheet'>");
  WriteFile(FILE_PATH_LITERAL("sw.js"),
            R"( this.onactivate = function(event) {
                  event.waitUntil(self.clients.claim());
                };
                this.addEventListener('install', e => {
                  e.addRoutes([{
                    condition: {
                      urlPattern: new URLPattern()
                    },
                    source: 'fetch-event',
                  },
                  ]);
                });
                this.onfetch = function(event) {
                  if (event.request.url.endsWith('/fallback.css')) {
                    return;
                  }
                  event.respondWith(fetch(event.request));
                };)");

  WriteFile(FILE_PATH_LITERAL("test.html"), kInstallAndWaitForActivatedPage);

  InitializeServer();

  {
    // The message "READY" will be sent when the service worker is activated.
    const std::u16string expected_title = u"READY";
    content::TitleWatcher title_watcher(
        browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/test.html")));
    EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
  }

  {
    // Navigate to the service worker controlled page.
    content::TestFrameNavigationObserver observer(
        browser()->tab_strip_model()->GetActiveWebContents());
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/scope/subresources.html")));
    observer.WaitForCommit();
  }

  {
    // Navigate away to record metrics.
    content::TestFrameNavigationObserver observer(
        browser()->tab_strip_model()->GetActiveWebContents());
    ASSERT_TRUE(
        ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
    observer.WaitForCommit();
  }

  histogram_tester.ExpectTotalCount(
      internal::kHistogramServiceWorkerSubresourceTotalRouterEvaluationTime, 1);
}

// TODO(crbug.com/360158408): The test is flaky on mac bots.
#if BUILDFLAG(IS_MAC)
#define MAYBE_SubresourceCountUMA DISABLED_SubresourceCountUMA
#else
#define MAYBE_SubresourceCountUMA SubresourceCountUMA
#endif
IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest, MAYBE_SubresourceCountUMA) {
  base::HistogramTester histogram_tester;

  WriteFile(FILE_PATH_LITERAL("fallback.css"), "");
  WriteFile(FILE_PATH_LITERAL("nofallback.css"), "");
  WriteFile(FILE_PATH_LITERAL("subresources.html"),
            "<link href='./fallback.css' rel='stylesheet'>"
            "<link href='./nofallback.css' rel='stylesheet'>");
  WriteFile(FILE_PATH_LITERAL("sw.js"),
            "this.onactivate = function(event) {"
            "  event.waitUntil(self.clients.claim());"
            "};"
            "this.onfetch = function(event) {"
            // We will fallback fallback.css.
            "  if (event.request.url.endsWith('/fallback.css')) {"
            "    return;"
            "  }"
            "  event.respondWith(fetch(event.request));"
            "};");
  WriteFile(FILE_PATH_LITERAL("test.html"),
            "<script>"
            "navigator.serviceWorker.register('./sw.js', {scope: './'})"
            "  .then(function(reg) {"
            "      reg.addEventListener('updatefound', function() {"
            "          var worker = reg.installing;"
            "          worker.addEventListener('statechange', function() {"
            "              if (worker.state == 'activated')"
            "                document.title = 'READY';"
            "            });"
            "        });"
            "    });"
            "</script>");

  InitializeServer();

  {
    // The message "READY" will be sent when the service worker is activated.
    const std::u16string expected_title = u"READY";
    content::TitleWatcher title_watcher(
        browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/test.html")));
    EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
  }

  // Navigate to the service worker controlled page.
  auto waiter = CreatePageLoadMetricsTestWaiter();
  waiter->AddPageExpectation(
      PageLoadMetricsTestWaiter::TimingField::kLoadEvent);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/subresources.html")));
  waiter->Wait();

  // Navigate away to record metrics.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  // Sync the histogram data between the renderer and browser processes.
  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
  histogram_tester.ExpectTotalCount("ServiceWorker.Subresource.Handled.Type2",
                                    1);
  histogram_tester.ExpectUniqueSample("ServiceWorker.Subresource.Handled.Type2",
                                      2 /* kCSSStyleSheet */, 1);
  histogram_tester.ExpectTotalCount(
      "ServiceWorker.Subresource.Fallbacked.Type2", 1);
  histogram_tester.ExpectUniqueSample(
      "ServiceWorker.Subresource.Fallbacked.Type2", 2 /* kCSSStyleSheet */, 1);
}

class ChromeServiceWorkerFetchTest : public ChromeServiceWorkerTest {
 public:
  ChromeServiceWorkerFetchTest(const ChromeServiceWorkerFetchTest&) = delete;
  ChromeServiceWorkerFetchTest& operator=(const ChromeServiceWorkerFetchTest&) =
      delete;

 protected:
  ChromeServiceWorkerFetchTest() = default;
  ~ChromeServiceWorkerFetchTest() override = default;

  void SetUpOnMainThread() override {
    WriteServiceWorkerFetchTestFiles();
    embedded_test_server()->ServeFilesFromDirectory(
        service_worker_dir_.GetPath());
    base::FilePath test_data_dir;
    ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
    embedded_test_server()->ServeFilesFromDirectory(test_data_dir);
    ASSERT_TRUE(embedded_test_server()->Start());
    InitializeServiceWorkerFetchTestPage();
  }

  std::string RequestString(const std::string& url,
                            const std::string& mode,
                            const std::string& credentials,
                            const std::string& destination) const {
    return base::StringPrintf(
        "url:%s, mode:%s, credentials:%s, destination:%s\n", url.c_str(),
        mode.c_str(), credentials.c_str(), destination.c_str());
  }

  std::string GetURL(const std::string& relative_url) const {
    return embedded_test_server()->GetURL(relative_url).spec();
  }

 private:
  void WriteServiceWorkerFetchTestFiles() {
    WriteFile(
        FILE_PATH_LITERAL("sw.js"),
        "this.onactivate = function(event) {"
        "  event.waitUntil(self.clients.claim());"
        "};"
        "this.onfetch = function(event) {"
        // Ignore the default favicon request. The default favicon request
        // is sent after the page loading is finished, and we can't
        // control the timing of the request. If the request is sent after
        // clients.claim() is called, fetch event for the default favicon
        // request is triggered and the tests become flaky. See
        // https://crbug.com/41430391.
        "  if (event.request.url.endsWith('/favicon.ico')) {"
        "    return;"
        "  }"
        "  event.respondWith("
        "      self.clients.matchAll().then(function(clients) {"
        "          clients.forEach(function(client) {"
        "              client.postMessage("
        "                'url:' + event.request.url + ', ' +"
        "                'mode:' + event.request.mode + ', ' +"
        "                'credentials:' + event.request.credentials + ', ' +"
        "                'destination:' + event.request.destination"
        "              );"
        "            });"
        "          return fetch(event.request);"
        "        }));"
        "};");
    WriteFile(FILE_PATH_LITERAL("test.html"), R"(
              <script src='/result_queue.js'></script>
              <script>
              navigator.serviceWorker.register('./sw.js', {scope: './'})
                .then(function(reg) {
                    reg.addEventListener('updatefound', function() {
                        var worker = reg.installing;
                        worker.addEventListener('statechange', function() {
                            if (worker.state == 'activated')
                              document.title = 'READY';
                          });
                      });
                  });
              var reportOnFetch = true;
              var issuedRequests = [];
              var reports = new ResultQueue();
              function reportRequests() {
                var str = '';
                issuedRequests.forEach(function(data) {
                  str += data + '\n';
                });
                reports.push(str);
              }
              navigator.serviceWorker.addEventListener(
                  'message',
                  function(event) {
                    issuedRequests.push(event.data);
                    if (reportOnFetch) {
                      reportRequests();
                    }
                  }, false);
              </script>
              )");
  }

  void InitializeServiceWorkerFetchTestPage() {
    // The message "READY" will be sent when the service worker is activated.
    const std::u16string expected_title = u"READY";
    content::TitleWatcher title_watcher(
        browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/test.html")));
    EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
  }
};

class FaviconUpdateWaiter : public favicon::FaviconDriverObserver {
 public:
  explicit FaviconUpdateWaiter(content::WebContents* web_contents) {
    scoped_observation_.Observe(
        favicon::ContentFaviconDriver::FromWebContents(web_contents));
  }

  FaviconUpdateWaiter(const FaviconUpdateWaiter&) = delete;
  FaviconUpdateWaiter& operator=(const FaviconUpdateWaiter&) = delete;

  ~FaviconUpdateWaiter() override = default;

  void Wait() {
    if (updated_) {
      return;
    }

    base::RunLoop run_loop;
    quit_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

 private:
  void OnFaviconUpdated(favicon::FaviconDriver* favicon_driver,
                        NotificationIconType notification_icon_type,
                        const GURL& icon_url,
                        bool icon_url_changed,
                        const gfx::Image& image) override {
    updated_ = true;
    if (quit_closure_) {
      std::move(quit_closure_).Run();
    }
  }

  bool updated_ = false;
  base::ScopedObservation<favicon::FaviconDriver,
                          favicon::FaviconDriverObserver>
      scoped_observation_{this};
  base::OnceClosure quit_closure_;
};

class ChromeServiceWorkerLinkFetchTest : public ChromeServiceWorkerFetchTest {
 public:
  ChromeServiceWorkerLinkFetchTest(const ChromeServiceWorkerLinkFetchTest&) =
      delete;
  ChromeServiceWorkerLinkFetchTest& operator=(
      const ChromeServiceWorkerLinkFetchTest&) = delete;

 protected:
  ChromeServiceWorkerLinkFetchTest() = default;
  ~ChromeServiceWorkerLinkFetchTest() override = default;
  void SetUpOnMainThread() override {
    // Map all hosts to localhost and setup the EmbeddedTestServer for
    // redirects.
    host_resolver()->AddRule("*", "127.0.0.1");
    ChromeServiceWorkerFetchTest::SetUpOnMainThread();
  }
  std::string ExecuteManifestFetchTest(const std::string& url,
                                       const std::string& cross_origin) {
    std::string js(
        base::StringPrintf("reportOnFetch = false;"
                           "var link = document.createElement('link');"
                           "link.rel = 'manifest';"
                           "link.href = '%s';",
                           url.c_str()));
    if (!cross_origin.empty()) {
      js +=
          base::StringPrintf("link.crossOrigin = '%s';", cross_origin.c_str());
    }
    js += "document.head.appendChild(link);";
    ExecuteJavaScriptForTests(js);
    return GetManifestAndIssuedRequests();
  }

  std::string ExecuteFaviconFetchTest(const std::string& url) {
    FaviconUpdateWaiter waiter(
        browser()->tab_strip_model()->GetActiveWebContents());
    std::string js(
        base::StringPrintf("reportOnFetch = false;"
                           "var link = document.createElement('link');"
                           "link.rel = 'icon';"
                           "link.href = '%s';"
                           "document.head.appendChild(link);",
                           url.c_str()));
    ExecuteJavaScriptForTests(js);
    waiter.Wait();
    return EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                  "reportRequests(); reports.pop();")
        .ExtractString();
  }

  void CopyTestFile(const std::string& src, const std::string& dst) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::FilePath test_data_dir;
    base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
    EXPECT_TRUE(base::CopyFile(test_data_dir.AppendASCII(src),
                               service_worker_dir_.GetPath().AppendASCII(dst)));
  }

 private:
  void ExecuteJavaScriptForTests(const std::string& js) {
    base::RunLoop run_loop;
    browser()
        ->tab_strip_model()
        ->GetActiveWebContents()
        ->GetPrimaryMainFrame()
        ->ExecuteJavaScriptForTests(
            base::ASCIIToUTF16(js),
            base::BindOnce(
                [](base::OnceClosure quit_callback, base::Value result) {
                  std::move(quit_callback).Run();
                },
                run_loop.QuitClosure()),
            content::ISOLATED_WORLD_ID_GLOBAL);
    run_loop.Run();
  }

  std::string GetManifestAndIssuedRequests() {
    base::RunLoop run_loop;
    browser()
        ->tab_strip_model()
        ->GetActiveWebContents()
        ->GetPrimaryPage()
        .GetManifest(
            base::BindOnce(&ManifestCallbackAndRun, run_loop.QuitClosure()));
    run_loop.Run();
    return EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                  "if (issuedRequests.length != 0) reportRequests();"
                  "else reportOnFetch = true;"
                  "reports.pop();")
        .ExtractString();
  }

  static void ManifestCallbackAndRun(base::OnceClosure continuation,
                                     blink::mojom::ManifestRequestResult,
                                     const GURL&,
                                     blink::mojom::ManifestPtr) {
    std::move(continuation).Run();
  }
};

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerLinkFetchTest, ManifestSameOrigin) {
  // <link rel="manifest" href="manifest.json">
  EXPECT_EQ(RequestString(GetURL("/manifest.json"), "cors", "omit", "manifest"),
            ExecuteManifestFetchTest("manifest.json", ""));
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerLinkFetchTest,
                       ManifestSameOriginUseCredentials) {
  // <link rel="manifest" href="manifest.json" crossorigin="use-credentials">
  EXPECT_EQ(
      RequestString(GetURL("/manifest.json"), "cors", "include", "manifest"),
      ExecuteManifestFetchTest("manifest.json", "use-credentials"));
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerLinkFetchTest, ManifestOtherOrigin) {
  // <link rel="manifest" href="http://www.example.com:PORT/manifest.json">
  const std::string url = embedded_test_server()
                              ->GetURL("www.example.com", "/manifest.json")
                              .spec();
  EXPECT_EQ(RequestString(url, "cors", "omit", "manifest"),
            ExecuteManifestFetchTest(url, ""));
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerLinkFetchTest,
                       ManifestOtherOriginUseCredentials) {
  // <link rel="manifest" href="http://www.example.com:PORT/manifest.json"
  //  crossorigin="use-credentials">
  const std::string url = embedded_test_server()
                              ->GetURL("www.example.com", "/manifest.json")
                              .spec();
  EXPECT_EQ(RequestString(url, "cors", "include", "manifest"),
            ExecuteManifestFetchTest(url, "use-credentials"));
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerLinkFetchTest, FaviconSameOrigin) {
  // <link rel="favicon" href="fav.png">
  CopyTestFile("favicon/icon.png", "fav.png");
  EXPECT_EQ(RequestString(GetURL("/fav.png"), "no-cors", "include", "image"),
            ExecuteFaviconFetchTest("/fav.png"));
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerLinkFetchTest, FaviconOtherOrigin) {
  // <link rel="favicon" href="http://www.example.com:PORT/fav.png">
  CopyTestFile("favicon/icon.png", "fav.png");
  const std::string url =
      embedded_test_server()->GetURL("www.example.com", "/fav.png").spec();
  EXPECT_EQ("", ExecuteFaviconFetchTest(url));
}

class ChromeServiceWorkerNavigationHintTest : public ChromeServiceWorkerTest {
 protected:
  void RunNavigationHintTest(
      const char* scope,
      content::StartServiceWorkerForNavigationHintResult expected_result,
      bool expected_started) {
    base::RunLoop run_loop;
    GURL url = embedded_test_server()->GetURL(scope);
    GetServiceWorkerContext()->StartServiceWorkerForNavigationHint(
        url, blink::StorageKey::CreateFirstParty(url::Origin::Create(url)),
        base::BindOnce(&ExpectResultAndRun<
                           content::StartServiceWorkerForNavigationHintResult>,
                       expected_result, run_loop.QuitClosure()));
    run_loop.Run();
    if (expected_started) {
      histogram_tester_.ExpectBucketCount(
          "ServiceWorker.StartWorker.Purpose",
          27 /* ServiceWorkerMetrics::EventType::NAVIGATION_HINT  */, 1);
      histogram_tester_.ExpectBucketCount(
          "ServiceWorker.StartWorker.StatusByPurpose_NAVIGATION_HINT",
          0 /* SERVICE_WORKER_OK  */, 1);
    } else {
      histogram_tester_.ExpectTotalCount("ServiceWorker.StartWorker.Purpose",
                                         0);
      histogram_tester_.ExpectTotalCount(
          "ServiceWorker.StartWorker.StatusByPurpose_NAVIGATION_HINT", 0);
    }
  }

  void TestStarted(const char* test_script) {
    WriteFile(FILE_PATH_LITERAL("sw.js"), "self.onfetch = function(e) {};");
    WriteFile(FILE_PATH_LITERAL("test.html"), test_script);
    InitializeServer();
    NavigateToPageAndWaitForReadyTitle("/test.html");
    GetServiceWorkerContext()->StopAllServiceWorkersForStorageKey(
        blink::StorageKey::CreateFirstParty(
            url::Origin::Create(embedded_test_server()->base_url())));
    RunNavigationHintTest(
        "/scope/", content::StartServiceWorkerForNavigationHintResult::STARTED,
        true);
  }

  void TestAlreadyRunning(const char* test_script) {
    WriteFile(FILE_PATH_LITERAL("sw.js"), "self.onfetch = function(e) {};");
    WriteFile(FILE_PATH_LITERAL("test.html"), test_script);
    InitializeServer();
    NavigateToPageAndWaitForReadyTitle("/test.html");
    RunNavigationHintTest(
        "/scope/",
        content::StartServiceWorkerForNavigationHintResult::ALREADY_RUNNING,
        false);
  }

  void TestNoFetchHandler(const char* test_script) {
    WriteFile(FILE_PATH_LITERAL("sw.js"), "/* empty */");
    WriteFile(FILE_PATH_LITERAL("test.html"), test_script);
    InitializeServer();
    NavigateToPageAndWaitForReadyTitle("/test.html");
    GetServiceWorkerContext()->StopAllServiceWorkersForStorageKey(
        blink::StorageKey::CreateFirstParty(
            url::Origin::Create(embedded_test_server()->base_url())));
    RunNavigationHintTest(
        "/scope/",
        content::StartServiceWorkerForNavigationHintResult::NO_FETCH_HANDLER,
        false);
  }
  base::HistogramTester histogram_tester_;
};

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest, Started) {
  TestStarted(kInstallAndWaitForActivatedPage);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest,
                       StartedModuleScript) {
  TestStarted(kInstallAndWaitForActivatedPageWithModuleScript);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest, AlreadyRunning) {
  TestAlreadyRunning(kInstallAndWaitForActivatedPage);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest,
                       AlreadyRunningModuleScript) {
  TestAlreadyRunning(kInstallAndWaitForActivatedPageWithModuleScript);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest,
                       NoServiceWorkerRegistration) {
  InitializeServer();
  RunNavigationHintTest("/scope/",
                        content::StartServiceWorkerForNavigationHintResult::
                            NO_SERVICE_WORKER_REGISTRATION,
                        false);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest,
                       NoActiveServiceWorkerVersion) {
  WriteFile(FILE_PATH_LITERAL("sw.js"),
            "self.oninstall = function(e) {\n"
            "    e.waitUntil(new Promise(r => { /* never resolve */ }));\n"
            "  };\n"
            "self.onfetch = function(e) {};");
  InitializeServer();
  base::RunLoop run_loop;
  blink::mojom::ServiceWorkerRegistrationOptions options(
      embedded_test_server()->GetURL("/scope/"),
      blink::mojom::ScriptType::kClassic,
      blink::mojom::ServiceWorkerUpdateViaCache::kImports);
  const blink::StorageKey key =
      blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
  GetServiceWorkerContext()->RegisterServiceWorker(
      embedded_test_server()->GetURL("/sw.js"), key, options,
      content::GlobalRenderFrameHostId(),
      base::BindOnce(&ExpectResultAndRun<blink::ServiceWorkerStatusCode>,
                     blink::ServiceWorkerStatusCode::kOk,
                     run_loop.QuitClosure()));
  run_loop.Run();
  RunNavigationHintTest("/scope/",
                        content::StartServiceWorkerForNavigationHintResult::
                            NO_ACTIVE_SERVICE_WORKER_VERSION,
                        false);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest, NoFetchHandler) {
  TestNoFetchHandler(kInstallAndWaitForActivatedPage);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationHintTest,
                       NoFetchHandlerModuleScript) {
  TestNoFetchHandler(kInstallAndWaitForActivatedPageWithModuleScript);
}

enum class ServicifiedFeatures { kNone, kServiceWorker, kNetwork };

// A simple fixture used for navigation preload tests so far. The fixture
// stashes the HttpRequest to a certain URL, useful for inspecting the headers
// to see if it was a navigation preload request and if it contained cookies.
//
// This is in //chrome instead of //content since the tests exercise the
// kBlockThirdPartyCookies preference which is not a //content concept.
class ChromeServiceWorkerNavigationPreloadTest : public InProcessBrowserTest {
 public:
  ChromeServiceWorkerNavigationPreloadTest() = default;

  ChromeServiceWorkerNavigationPreloadTest(
      const ChromeServiceWorkerNavigationPreloadTest&) = delete;
  ChromeServiceWorkerNavigationPreloadTest& operator=(
      const ChromeServiceWorkerNavigationPreloadTest&) = delete;

  void SetUp() override {
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &ChromeServiceWorkerNavigationPreloadTest::HandleRequest,
        base::Unretained(this)));
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());

    InProcessBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    // Make all hosts resolve to 127.0.0.1 so the same embedded test server can
    // be used for cross-origin URLs.
    host_resolver()->AddRule("*", "127.0.0.1");

    embedded_test_server()->StartAcceptingConnections();
  }

  std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
      const net::test_server::HttpRequest& request) {
    // Intercept requests to the "test" endpoint.
    GURL url = request.base_url;
    url = url.Resolve(request.relative_url);
    if (url.GetPath() != "/service_worker/test") {
      return nullptr;
    }

    // Stash the request for testing. We'd typically prefer to echo back the
    // request and test the resulting page contents, but that becomes
    // cumbersome if the test involves cross-origin frames.
    EXPECT_FALSE(received_request_);
    received_request_ = request;

    // Respond with OK.
    std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
        new net::test_server::BasicHttpResponse());
    http_response->set_code(net::HTTP_OK);
    http_response->set_content("OK");
    http_response->set_content_type("text/plain");
    return http_response;
  }

  bool HasHeader(const net::test_server::HttpRequest& request,
                 const std::string& name) const {
    return request.headers.find(name) != request.headers.end();
  }

  std::string GetHeader(const net::test_server::HttpRequest& request,
                        const std::string& name) const {
    const auto& iter = request.headers.find(name);
    EXPECT_TRUE(iter != request.headers.end());
    if (iter == request.headers.end()) {
      return std::string();
    }
    return iter->second;
  }

  bool has_received_request() const { return received_request_.has_value(); }

  const net::test_server::HttpRequest& received_request() const {
    return *received_request_;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;

  // The request that hit the "test" endpoint.
  std::optional<net::test_server::HttpRequest> received_request_;
};

// Tests navigation preload during a navigation in the top-level frame
// when third-party cookies are blocked. The navigation preload request
// should be sent with cookies as normal. Regression test for
// https://crbug.com/40605694.
IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationPreloadTest,
                       TopFrameWithThirdPartyBlocking) {
  // Enable third-party cookie blocking.
  browser()->GetProfile()->GetPrefs()->SetInteger(
      prefs::kCookieControlsMode,
      static_cast<int>(content_settings::CookieControlsMode::kBlockThirdParty));

  // Load a page that registers a service worker.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL(
                     "/service_worker/create_service_worker.html")));
  EXPECT_EQ("DONE", EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                           "register('navigation_preload_worker.js');"));

  // Also set cookies.
  EXPECT_EQ("foo=bar",
            EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                   "document.cookie = 'foo=bar'; document.cookie;"));

  // Load the test page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/service_worker/test")));

  // The navigation preload request should have occurred and included cookies.
  ASSERT_TRUE(has_received_request());
  EXPECT_EQ("true",
            GetHeader(received_request(), "Service-Worker-Navigation-Preload"));
  EXPECT_EQ("foo=bar", GetHeader(received_request(), "Cookie"));
}

// Tests navigation preload during a navigation in a third-party iframe
// when third-party cookies are blocked. This blocks service worker as well,
// so the navigation preload request should not be sent. And the navigation
// request should not include cookies.
IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerNavigationPreloadTest,
                       SubFrameWithThirdPartyBlocking) {
  // Enable third-party cookie blocking.
  browser()->GetProfile()->GetPrefs()->SetInteger(
      prefs::kCookieControlsMode,
      static_cast<int>(content_settings::CookieControlsMode::kBlockThirdParty));

  // Load a page that registers a service worker.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL(
                     "/service_worker/create_service_worker.html")));
  EXPECT_EQ("DONE", EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                           "register('navigation_preload_worker.js');"));

  // Also set cookies.
  EXPECT_EQ("foo=bar",
            EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                   "document.cookie = 'foo=bar'; document.cookie;"));

  // Generate a cross-origin URL.
  GURL top_frame_url = embedded_test_server()->GetURL(
      "/service_worker/page_with_third_party_iframe.html");
  GURL::Replacements replacements;
  replacements.SetHostStr("cross-origin.example.com");
  top_frame_url = top_frame_url.ReplaceComponents(replacements);

  // Navigate to the page and embed a third-party iframe to the test
  // page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), top_frame_url));
  GURL iframe_url = embedded_test_server()->GetURL("/service_worker/test");
  EXPECT_EQ(true, EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                         "addIframe('" + iframe_url.spec() + "');"));

  // The request should have been received. Because the navigation was for a
  // third-party iframe with cookies blocked, the service worker should not have
  // handled the request so navigation preload should not have occurred.
  // Likewise, the cookies should not have been sent.
  ASSERT_TRUE(has_received_request());
  EXPECT_FALSE(
      HasHeader(received_request(), "Service-Worker-Navigation-Preload"));
  EXPECT_FALSE(HasHeader(received_request(), "Cookie"));
}

class ChromeServiceWorkerPrewarmForDSETest : public InProcessBrowserTest {
 public:
  void SetUp() override {
    chrome_service_worker::
        PrewarmServiceWorkerRegistrationForDSECalledCountForTesting() = 0;
    // The following step starts the browser, and prewarms the registration of
    // ServiceWorker for DSE.
    InProcessBrowserTest::SetUp();
  }

  void TearDown() override {
    chrome_service_worker::
        PrewarmServiceWorkerRegistrationForDSECalledCountForTesting() =
            std::nullopt;
  }
};

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerPrewarmForDSETest, PrewarmIsCalled) {
  EXPECT_GE(chrome_service_worker::
                PrewarmServiceWorkerRegistrationForDSECalledCountForTesting()
                    .value(),
            1);
}

IN_PROC_BROWSER_TEST_F(ChromeServiceWorkerTest,
                       VerifyNoChromeApisInNormalServiceWorker) {
  WriteFile(
      FILE_PATH_LITERAL("sw.js"),
      "self.addEventListener('message', event => {"
      "  const chromeExists = typeof chrome !== 'undefined';"
      "  const loadTimesExists = chromeExists && typeof chrome.loadTimes !== "
      "'undefined';"
      "  const csiExists = chromeExists && typeof chrome.csi !== 'undefined';"
      "  const benchmarkingExists = chromeExists && typeof chrome.benchmarking "
      "!== 'undefined';"
      "  event.source.postMessage({"
      "    chromeExists,"
      "    loadTimesExists,"
      "    csiExists,"
      "    benchmarkingExists"
      "  });"
      "});");
  WriteFile(FILE_PATH_LITERAL("test.html"),
            "<script>"
            "navigator.serviceWorker.register('./sw.js')"
            "  .then(reg => {"
            "    if (reg.active) {"
            "      document.title = 'READY';"
            "    } else {"
            "      const worker = reg.installing || reg.waiting;"
            "      worker.addEventListener('statechange', () => {"
            "        if (worker.state === 'activated') {"
            "          document.title = 'READY';"
            "        }"
            "      });"
            "    }"
            "  });"
            "</script>");

  InitializeServer();
  NavigateToPageAndWaitForReadyTitle("/test.html");

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  // Set up message listener in the page
  ASSERT_TRUE(content::ExecJs(
      web_contents,
      "var receivedMessage = null;"
      "navigator.serviceWorker.addEventListener('message', event => {"
      "  receivedMessage = event.data;"
      "});"));

  // Send message to SW to trigger the check
  ASSERT_TRUE(content::ExecJs(web_contents,
                              "navigator.serviceWorker.ready.then(reg => {"
                              "  reg.active.postMessage('check');"
                              "});"));

  // Wait for message
  content::EvalJsResult result = content::EvalJs(
      web_contents,
      "new Promise(resolve => {"
      "  if (receivedMessage) {"
      "    resolve(receivedMessage);"
      "  } else {"
      "    navigator.serviceWorker.addEventListener('message', event => {"
      "      resolve(event.data);"
      "    }, {once: true});"
      "  }"
      "});");

  ASSERT_TRUE(result.is_dict());
  const base::DictValue& dict = result.ExtractDict();

  // Assert that these APIs are NOT present in a normal service worker.
  EXPECT_FALSE(dict.FindBool("loadTimesExists").value_or(true));
  EXPECT_FALSE(dict.FindBool("csiExists").value_or(true));
  EXPECT_FALSE(dict.FindBool("benchmarkingExists").value_or(true));
}

}  // namespace chrome_service_worker_browser_test
