910e62b5创建于 1月15日历史提交
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>

#include "base/base_paths.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/devtools/simple_devtools_protocol_client/simple_devtools_protocol_client.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/switches.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "headless/test/headless_devtooled_browsertest.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/register_basic_auth_handler.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "third_party/inspector_protocol/crdtp/dispatch.h"
#include "url/gurl.h"

#define EXPECT_SIZE_EQ(expected, actual)               \
  do {                                                 \
    EXPECT_EQ((expected).width(), (actual).width());   \
    EXPECT_EQ((expected).height(), (actual).height()); \
  } while (false)

using simple_devtools_protocol_client::SimpleDevToolsProtocolClient;

using testing::ElementsAre;
using testing::NotNull;
using testing::UnorderedElementsAre;

namespace headless {

class HeadlessDevToolsClientNavigationTest
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &HeadlessDevToolsClientNavigationTest::OnLoadEventFired,
            base::Unretained(this)));
    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()->GetURL("/hello.html").spec()));
  }

  void OnLoadEventFired(const base::Value::Dict& params) {
    devtools_client_.SendCommand("Page.disable");
    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientNavigationTest);

class HeadlessDevToolsNavigationControlTest
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    SendCommandSync(devtools_client_, "Page.enable");
    SendCommandSync(devtools_client_, "Network.enable");

    base::Value::List patterns;
    patterns.Append(Param("urlPattern", "*"));
    devtools_client_.SendCommand("Network.setRequestInterception",
                                 Param("patterns", std::move(patterns)));

    devtools_client_.AddEventHandler(
        "Network.requestIntercepted",
        base::BindRepeating(
            &HeadlessDevToolsNavigationControlTest::OnRequestIntercepted,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Page.frameStoppedLoading",
        base::BindRepeating(
            &HeadlessDevToolsNavigationControlTest::OnFrameStoppedLoading,
            base::Unretained(this)));

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()->GetURL("/hello.html").spec()));
  }

  void OnRequestIntercepted(const base::Value::Dict& params) {
    if (DictBool(params, "params.isNavigationRequest"))
      navigation_requested_ = true;

    // Allow the navigation to proceed.
    devtools_client_.SendCommand(
        "Network.continueInterceptedRequest",
        Param("interceptionId", DictString(params, "params.interceptionId")));
  }

  void OnFrameStoppedLoading(const base::Value::Dict& params) {
    EXPECT_TRUE(navigation_requested_);
    FinishAsynchronousTest();
  }

 private:
  bool navigation_requested_ = false;
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsNavigationControlTest);

class HeadlessCrashObserverTest : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    devtools_client_.AddEventHandler(
        "Inspector.targetCrashed",
        base::BindRepeating(&HeadlessCrashObserverTest::OnTargetCrashed,
                            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Inspector.enable");

    devtools_client_.SendCommand("Page.navigate",
                                 Param("url", blink::kChromeUICrashURL));
  }

  void OnTargetCrashed(const base::Value::Dict&) { FinishAsynchronousTest(); }

  // Make sure we don't fail because the renderer crashed!
  void PrimaryMainFrameRenderProcessGone(
      base::TerminationStatus status) override {
#if BUILDFLAG(IS_WIN) && defined(ADDRESS_SANITIZER)
    // TODO(crbug.com/40577245): Make ASan not interfere and expect a crash.
    // ASan's normal error exit code is 1, which base categorizes as the process
    // being killed.
    EXPECT_EQ(base::TERMINATION_STATUS_PROCESS_WAS_KILLED, status);
#elif BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
    BUILDFLAG(IS_FUCHSIA)
    EXPECT_EQ(base::TERMINATION_STATUS_PROCESS_CRASHED, status);
#else
    EXPECT_EQ(base::TERMINATION_STATUS_ABNORMAL_TERMINATION, status);
#endif
  }

 private:
  content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes_;
};

// TODO(crbug.com/40206073): HeadlessCrashObserverTest.RunAsyncTest is flaky on
// Win debug.
#if BUILDFLAG(IS_WIN) && !defined(NDEBUG)
DISABLED_HEADLESS_DEVTOOLED_TEST_F(HeadlessCrashObserverTest);
#else
HEADLESS_DEVTOOLED_TEST_F(HeadlessCrashObserverTest);
#endif

class HeadlessDevToolsNetworkBlockedUrlTest
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.requestWillBeSent",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnRequestWillBeSent,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.responseReceived",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnResponseReceived,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.loadingFailed",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnLoadingFailed,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnLoadEventFired,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    base::Value::List urls;
    base::Value::Dict url_pattern;
    url_pattern.Set("urlPattern", "*://*:*/hello.html");
    url_pattern.Set("block", true);
    urls.Append(std::move(url_pattern));
    devtools_client_.SendCommand("Network.setBlockedURLs",
                                 Param("urlPatterns", std::move(urls)));

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url",
              embedded_test_server()->GetURL("/dom_tree_test.html").spec()));
  }

  std::string GetUrlPath(const std::string& url) const {
    GURL gurl(url);
    return gurl.GetPath();
  }

  void OnRequestWillBeSent(const base::Value::Dict& params) {
    std::string path = GetUrlPath(DictString(params, "params.request.url"));
    requests_to_be_sent_.push_back(path);
    request_id_to_path_[DictString(params, "params.requestId")] = path;
  }

  void OnResponseReceived(const base::Value::Dict& params) {
    responses_received_.push_back(
        GetUrlPath(DictString(params, "params.response.url")));
  }

  void OnLoadingFailed(const base::Value::Dict& params) {
    failures_.push_back(
        request_id_to_path_[DictString(params, "params.requestId")]);
    EXPECT_EQ("inspector", DictString(params, "params.blockedReason"));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    EXPECT_THAT(requests_to_be_sent_,
                testing::UnorderedElementsAre(
                    "/dom_tree_test.html", "/dom_tree_test.css", "/iframe.html",
                    "/Ahem.ttf", "/hello.html"));
    EXPECT_THAT(responses_received_,
                testing::UnorderedElementsAre("/dom_tree_test.html",
                                              "/dom_tree_test.css",
                                              "/iframe.html", "/Ahem.ttf"));
    EXPECT_THAT(failures_, ElementsAre("/hello.html"));

    FinishAsynchronousTest();
  }

  std::map<std::string, std::string> request_id_to_path_;
  std::vector<std::string> requests_to_be_sent_;
  std::vector<std::string> responses_received_;
  std::vector<std::string> failures_;
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsNetworkBlockedUrlTest);

class DevToolsNetworkOfflineEmulationTest
    : public HeadlessDevTooledBrowserTest {
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.loadingFailed",
        base::BindRepeating(
            &DevToolsNetworkOfflineEmulationTest::OnLoadingFailed,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    base::Value::Dict params;
    params.Set("offline", true);
    params.Set("latency", 0);
    params.Set("downloadThroughput", 0);
    params.Set("uploadThroughput", 0);
    devtools_client_.SendCommand("Network.emulateNetworkConditions",
                                 std::move(params));

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()->GetURL("/hello.html").spec()));
  }

  void OnLoadingFailed(const base::Value::Dict& params) {
    EXPECT_THAT(params, DictHasValue("params.errorText",
                                     "net::ERR_INTERNET_DISCONNECTED"));
    EXPECT_EQ("net::ERR_INTERNET_DISCONNECTED",
              DictString(params, "params.errorText"));

    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(DevToolsNetworkOfflineEmulationTest);

class DomTreeExtractionBrowserTest : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(&DomTreeExtractionBrowserTest::OnLoadEventFired,
                            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url",
              embedded_test_server()->GetURL("/dom_tree_test.html").spec()));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    SendCommandSync(devtools_client_, "Page.disable");

    base::Value::List css_whitelist;
    css_whitelist.Append("color");
    css_whitelist.Append("display");
    css_whitelist.Append("font-style");
    css_whitelist.Append("font-family");
    css_whitelist.Append("margin-left");
    css_whitelist.Append("margin-right");
    css_whitelist.Append("margin-top");
    css_whitelist.Append("margin-bottom");
    devtools_client_.SendCommand(
        "DOMSnapshot.getSnapshot",
        Param("computedStyleWhitelist", css_whitelist),
        base::BindOnce(&DomTreeExtractionBrowserTest::OnGetSnapshotDone,
                       base::Unretained(this)));
  }

  void OnGetSnapshotDone(base::Value::Dict result) {
    std::vector<base::Value::Dict> dom_nodes;
    GetDomNodes(result, &dom_nodes);

    std::vector<base::Value::Dict> computed_styles;
    GetComputedStyles(result, &computed_styles);

    base::ScopedAllowBlockingForTesting allow_blocking;
    base::FilePath source_root_dir;
    base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root_dir);

    CompareToGolden(
        dom_nodes,
        source_root_dir.Append(FILE_PATH_LITERAL(
            "headless/test/dom_tree_extraction_expected_nodes.txt")));

    CompareToGolden(
        computed_styles,
        source_root_dir.Append(FILE_PATH_LITERAL(
            "headless/test/dom_tree_extraction_expected_styles.txt")));

    FinishAsynchronousTest();
  }

  void GetDomNodes(const base::Value::Dict& result,
                   std::vector<base::Value::Dict>* dom_nodes) {
    GURL::Replacements replace_port;
    replace_port.SetPortStr("");

    const base::Value::List* dom_nodes_list =
        result.FindListByDottedPath("result.domNodes");
    ASSERT_NE(dom_nodes_list, nullptr);
    ASSERT_GT(dom_nodes_list->size(), 0ul);

    const base::Value::List* layout_tree_nodes_list =
        result.FindListByDottedPath("result.layoutTreeNodes");
    ASSERT_NE(layout_tree_nodes_list, nullptr);
    ASSERT_GT(layout_tree_nodes_list->size(), 0ul);

    // For convenience, flatten the dom tree into an array of dicts.
    dom_nodes->reserve(dom_nodes_list->size());
    for (const auto& dom_node : *dom_nodes_list) {
      ASSERT_TRUE(dom_node.is_dict());
      dom_nodes->push_back(dom_node.GetDict().Clone());
      base::Value::Dict& node_dict = dom_nodes->back();

      // Node IDs are assigned in a non deterministic way.
      if (node_dict.Find("backendNodeId"))
        node_dict.Set("backendNodeId", "?");

      // Frame IDs are random.
      if (node_dict.Find("frameId"))
        node_dict.Set("frameId", "?");

      // Ports are random.
      if (base::Value* base_url_value = node_dict.Find("baseURL")) {
        node_dict.Set("baseURL", GURL(base_url_value->GetString())
                                     .ReplaceComponents(replace_port)
                                     .spec());
      }

      if (base::Value* document_url_value = node_dict.Find("documentURL")) {
        node_dict.Set("documentURL", GURL(document_url_value->GetString())
                                         .ReplaceComponents(replace_port)
                                         .spec());
      }

      // Golden file expects scrollOffsetXY to have fractional part.
      // TODO(kvitekp): Consider updating golden files.
      if (std::optional<double> x = node_dict.FindDouble("scrollOffsetX")) {
        node_dict.Set("scrollOffsetX", *x);
      }

      if (std::optional<double> y = node_dict.FindDouble("scrollOffsetY")) {
        node_dict.Set("scrollOffsetY", *y);
      }

      // Merge LayoutTreeNode data into the dom_node dictionary.
      if (std::optional<int> layout_node_index =
              node_dict.FindInt("layoutNodeIndex")) {
        ASSERT_LE(0, *layout_node_index);
        ASSERT_GT(layout_tree_nodes_list->size(),
                  static_cast<size_t>(*layout_node_index));

        const base::Value::Dict& layout_tree_node =
            (*layout_tree_nodes_list)[*layout_node_index].GetDict();

        if (const base::Value::Dict* bounding_box =
                layout_tree_node.FindDict("boundingBox")) {
          node_dict.Set("boundingBox", bounding_box->Clone());
          FixBoundingBox(node_dict);
        }

        if (const std::string* layout_text =
                layout_tree_node.FindString("layoutText")) {
          node_dict.Set("layoutText", *layout_text);
        }

        if (std::optional<int> style_index =
                layout_tree_node.FindInt("styleIndex")) {
          node_dict.Set("styleIndex", *style_index);
        }

        if (const base::Value::List* inline_text_nodes =
                layout_tree_node.FindList("inlineTextNodes")) {
          base::Value::List list = inline_text_nodes->Clone();
          for (auto& list_entry : list) {
            ASSERT_TRUE(list_entry.is_dict());
            FixBoundingBox(list_entry.GetDict());
          }
          node_dict.Set("inlineTextNodes", std::move(list));
        }
      }
    }
  }

  void FixBoundingBox(base::Value::Dict& dict) {
    if (base::Value::Dict* bounding_box = dict.FindDict("boundingBox")) {
      // The golden file expects double values in boundingBox.
      // TODO(kvitekp): Consider updating the golden and just
      // cloning the |boundingBox| dictionary here.
      bounding_box->Set("x", *bounding_box->FindDouble("x"));
      bounding_box->Set("y", *bounding_box->FindDouble("y"));
      bounding_box->Set("width", *bounding_box->FindDouble("width"));
      bounding_box->Set("height", *bounding_box->FindDouble("height"));
    }
  }

  void GetComputedStyles(const base::Value::Dict& result,
                         std::vector<base::Value::Dict>* computed_styles) {
    const base::Value::List* computed_styles_list =
        result.FindListByDottedPath("result.computedStyles");
    ASSERT_NE(computed_styles_list, nullptr);
    ASSERT_GT(computed_styles_list->size(), 0ul);

    computed_styles->reserve(computed_styles_list->size());
    for (const auto& computed_style : *computed_styles_list) {
      ASSERT_TRUE(computed_style.is_dict());
      const base::Value::List* properties_list =
          computed_style.GetDict().FindList("properties");
      ASSERT_NE(properties_list, nullptr);

      base::Value::Dict computed_style_dict;
      for (const auto& property : *properties_list) {
        ASSERT_TRUE(property.is_dict());
        const base::Value::Dict& property_dict = property.GetDict();
        computed_style_dict.Set(*property_dict.FindString("name"),
                                *property_dict.FindString("value"));
      }
      computed_styles->push_back(std::move(computed_style_dict));
    }
  }

  void CompareToGolden(const std::vector<base::Value::Dict>& entries,
                       base::FilePath expected_filepath) {
    std::string expected_entries;
    ASSERT_TRUE(base::ReadFileToString(expected_filepath, &expected_entries));

    std::string actual_entries;
    for (const base::Value::Dict& entry : entries) {
      std::string entry_json;
      base::JSONWriter::WriteWithOptions(
          entry, base::JSONWriter::OPTIONS_PRETTY_PRINT, &entry_json);
      actual_entries += entry_json;
    }

#if BUILDFLAG(IS_WIN)
    ASSERT_TRUE(base::RemoveChars(actual_entries, "\r", &actual_entries));
#endif

    EXPECT_EQ(expected_entries, actual_entries);
  }
};

// TODO(crbug.com/40697467): Fix this test on Fuchsia and re-enable.
// NOTE: These macros expand to: DomTreeExtractionBrowserTest.RunAsyncTest
#if BUILDFLAG(IS_FUCHSIA)
DISABLED_HEADLESS_DEVTOOLED_TEST_F(DomTreeExtractionBrowserTest);
#else
HEADLESS_DEVTOOLED_TEST_F(DomTreeExtractionBrowserTest);
#endif

class BlockedByClient_NetworkObserver_Test
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.requestIntercepted",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnRequestIntercepted,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.requestWillBeSent",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnRequestWillBeSent,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.loadingFailed",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnLoadingFailed,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    // Intercept all network requests.
    base::Value::List patterns;
    patterns.Append(Param("urlPattern", "*"));
    devtools_client_.SendCommand("Network.setRequestInterception",
                                 Param("patterns", std::move(patterns)));

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnLoadEventFired,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate", Param("url", embedded_test_server()
                                          ->GetURL("/resource_cancel_test.html")
                                          .spec()));
  }

  void OnRequestIntercepted(const base::Value::Dict& params) {
    const std::string url = DictString(params, "params.request.url");

    urls_seen_.push_back(GURL(url).ExtractFileName());

    base::Value::Dict continue_intercept_params;
    continue_intercept_params.Set("interceptionId",
                                  DictString(params, "params.interceptionId"));

    // TODO(kvitekp): comment does not match the following code, review!!!

    // We *abort* fetching Ahem.ttf, and *fail* for test.jpg
    // to verify that both ways result in a failed loading event,
    // which we'll observe in OnLoadingFailed below.
    // Also, we abort iframe2.html because it turns out frame interception
    // uses a very different codepath than other resources.
    if (EndsWith(url, "/test.jpg", base::CompareCase::SENSITIVE)) {
      continue_intercept_params.Set("errorReason", "BlockedByClient");
    } else if (EndsWith(url, "/Ahem.ttf", base::CompareCase::SENSITIVE)) {
      continue_intercept_params.Set("errorReason", "BlockedByClient");
    } else if (EndsWith(url, "/iframe2.html", base::CompareCase::SENSITIVE)) {
      continue_intercept_params.Set("errorReason", "BlockedByClient");
    }

    devtools_client_.SendCommand("Network.continueInterceptedRequest",
                                 std::move(continue_intercept_params));
  }

  void OnRequestWillBeSent(const base::Value::Dict& params) {
    // Here, we just record the URLs (filenames) for each request ID, since
    // we won't have access to them in OnLoadingFailed below.
    urls_by_id_[DictString(params, "params.requestId")] =
        GURL(DictString(params, "params.request.url")).ExtractFileName();
  }

  void OnLoadingFailed(const base::Value::Dict& params) {
    // Record the failed loading events so we can verify below that we
    // received the events.
    urls_that_failed_to_load_.push_back(
        urls_by_id_[DictString(params, "params.requestId")]);
    EXPECT_EQ("inspector", DictString(params, "params.blockedReason"));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    EXPECT_THAT(urls_that_failed_to_load_,
                UnorderedElementsAre("test.jpg", "Ahem.ttf", "iframe2.html"));
    EXPECT_THAT(urls_seen_, UnorderedElementsAre("resource_cancel_test.html",
                                                 "dom_tree_test.css",
                                                 "test.jpg", "iframe.html",
                                                 "iframe2.html", "Ahem.ttf"));

    FinishAsynchronousTest();
  }

 private:
  std::vector<std::string> urls_seen_;
  std::vector<std::string> urls_that_failed_to_load_;
  std::map<std::string, std::string> urls_by_id_;
};

HEADLESS_DEVTOOLED_TEST_F(BlockedByClient_NetworkObserver_Test);

// This class simulates sending GETs to a proxy server by using a standard
// EmbeddedTestServer as the proxy server, and giving it a a proxy auth handler.
// While there is no actual proxy, the EmbeddedTestServer's file handler handles
// HTTP-proxy-style GETs directly, no actual proxy needed.
class DevtoolsInterceptionWithAuthProxyTest
    : public HeadlessDevTooledBrowserTest {
 public:
  DevtoolsInterceptionWithAuthProxyTest() = default;

  void SetUp() override {
    RegisterProxyBasicAuthHandler(*embedded_test_server(), "user", "pass");
    ASSERT_TRUE(embedded_test_server()->Start());
    HeadlessDevTooledBrowserTest::SetUp();
  }

  void RunDevTooledTest() override {
    devtools_client_.AddEventHandler(
        "Network.requestIntercepted",
        base::BindRepeating(
            &DevtoolsInterceptionWithAuthProxyTest::OnRequestIntercepted,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    // Intercept all network requests.
    base::Value::List patterns;
    patterns.Append(Param("urlPattern", "*"));
    devtools_client_.SendCommand("Network.setRequestInterception",
                                 Param("patterns", std::move(patterns)));

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &DevtoolsInterceptionWithAuthProxyTest::OnLoadEventFired,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    // Hostname used here doesn't actually matter, since the proxy handles the
    // requests itself without sending them to a server.
    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()
                         ->GetURL("host.test", "/dom_tree_test.html")
                         .spec()));
  }

  void OnRequestIntercepted(const base::Value::Dict& params) {
    base::Value::Dict continue_intercept_params;
    continue_intercept_params.Set("interceptionId",
                                  DictString(params, "params.interceptionId"));

    if (DictHas(params, "params.authChallenge")) {
      auth_challenge_seen_ = true;

      base::Value::Dict auth_challenge_response;
      auth_challenge_response.Set("response", "ProvideCredentials");
      auth_challenge_response.Set("username", "user");
      auth_challenge_response.Set("password", "pass");
      continue_intercept_params.Set("authChallengeResponse",
                                    std::move(auth_challenge_response));
    } else {
      GURL url(DictString(params, "params.request.url"));
      files_loaded_.insert(url.GetPath());
    }

    devtools_client_.SendCommand("Network.continueInterceptedRequest",
                                 std::move(continue_intercept_params));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    EXPECT_TRUE(auth_challenge_seen_);
    EXPECT_THAT(files_loaded_,
                testing::UnorderedElementsAre("/Ahem.ttf", "/dom_tree_test.css",
                                              "/dom_tree_test.html",
                                              "/iframe.html", "/hello.html"));

    FinishAsynchronousTest();
  }

  void CustomizeHeadlessBrowserContext(
      HeadlessBrowserContext::Builder& builder) override {
    std::unique_ptr<net::ProxyConfig> proxy_config(new net::ProxyConfig);
    proxy_config->proxy_rules().ParseFromString(
        embedded_test_server()->host_port_pair().ToString());
    // TODO(crbug.com/40600992): Don't rely on proxying localhost.
    proxy_config->proxy_rules().bypass_rules.AddRulesToSubtractImplicit();
    builder.SetProxyConfig(std::move(proxy_config));
  }

 private:
  bool auth_challenge_seen_ = false;
  std::set<std::string> files_loaded_;
};

HEADLESS_DEVTOOLED_TEST_F(DevtoolsInterceptionWithAuthProxyTest);

class NavigatorLanguages : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    devtools_client_.SendCommand(
        "Runtime.evaluate",
        Param("expression", "JSON.stringify(navigator.languages)"),
        base::BindOnce(&NavigatorLanguages::OnEvaluateResult,
                       base::Unretained(this)));
  }

  void OnEvaluateResult(base::Value::Dict result) {
    EXPECT_THAT(result, DictHasValue("result.result.value",
                                     "[\"en-UK\",\"DE\",\"FR\"]"));
    FinishAsynchronousTest();
  }

  void CustomizeHeadlessBrowserContext(
      HeadlessBrowserContext::Builder& builder) override {
    builder.SetAcceptLanguage("en-UK, DE, FR");
  }
};

HEADLESS_DEVTOOLED_TEST_F(NavigatorLanguages);

class AcceptLanguagesSwitch : public HeadlessDevTooledBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessDevTooledBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(switches::kAcceptLang, "cz-CZ");
  }

  void RunDevTooledTest() override {
    devtools_client_.SendCommand(
        "Runtime.evaluate",
        Param("expression", "JSON.stringify(navigator.languages)"),
        base::BindOnce(&AcceptLanguagesSwitch::OnEvaluateResult,
                       base::Unretained(this)));
  }

  void OnEvaluateResult(base::Value::Dict result) {
    EXPECT_THAT(result, DictHasValue("result.result.value", "[\"cz-CZ\"]"));
    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(AcceptLanguagesSwitch);

}  // namespace headless