blob: 0bc22c10716f54f66634bc0050894f5a410390c8 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include <string_view>
#include "base/base64.h"
#include "base/check_deref.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_switches.h"
#include "base/test/values_test_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/browser_app_launcher.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/custom_handlers/protocol_handler_registry_factory.h"
#include "chrome/browser/data_saver/data_saver.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h"
#include "chrome/browser/preloading/preloading_prefs.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations_mixin.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ssl/https_upgrades_util.h"
#include "chrome/browser/tpcd/metadata/manager_factory.h"
#include "chrome/browser/tpcd/support/trial_test_utils.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/custom_handlers/protocol_handler_registry.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/infobars/core/infobar_delegate.h"
#include "components/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations.h"
#include "content/public/browser/btm_redirect_info.h"
#include "content/public/browser/btm_service.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/ssl_status.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/referrer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/btm_service_test_utils.h"
#include "content/public/test/preloading_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/url_loader_interceptor.h"
#include "net/base/ip_address.h"
#include "net/dns/mock_host_resolver.h"
#include "net/ssl/ssl_cipher_suite_names.h"
#include "net/ssl/ssl_config.h"
#include "net/ssl/ssl_connection_status_flags.h"
#include "net/ssl/ssl_server_config.h"
#include "printing/buildflags/buildflags.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/boringssl/src/include/openssl/ssl.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/origin.h"
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/guest_view/browser/guest_view_manager_delegate.h"
#include "components/guest_view/browser/test_guest_view_manager.h"
#endif // !BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/test/extension_test_message_listener.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
#if BUILDFLAG(IS_WIN)
#include "base/base_paths_win.h"
#include "base/test/scoped_path_override.h"
#endif
using DevToolsProtocolTest = DevToolsProtocolTestBase;
using testing::AllOf;
using testing::Contains;
using testing::Eq;
using testing::Not;
namespace {
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
VisibleSecurityStateChangedNeutralState) {
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), GURL("about:blank")));
EXPECT_TRUE(content::WaitForLoadStop(web_contents()));
Attach();
SendCommandAsync("Security.enable");
base::Value::Dict params =
WaitForNotification("Security.visibleSecurityStateChanged", true);
std::string* security_state =
params.FindStringByDottedPath("visibleSecurityState.securityState");
ASSERT_TRUE(security_state);
EXPECT_EQ(std::string("neutral"), *security_state);
EXPECT_FALSE(params.FindStringByDottedPath(
"visibleSecurityState.certificateSecurityState"));
EXPECT_FALSE(
params.FindStringByDottedPath("visibleSecurityState.safetyTipInfo"));
const base::Value* security_state_issue_ids =
params.FindByDottedPath("visibleSecurityState.securityStateIssueIds");
EXPECT_TRUE(base::Contains(security_state_issue_ids->GetList(),
base::Value("scheme-is-not-cryptographic")));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateDeleteContext) {
AttachToBrowserTarget();
for (int i = 0; i < 2; i++) {
const base::Value::Dict* result =
SendCommandSync("Target.createBrowserContext");
std::string context_id = *result->FindString("browserContextId");
base::Value::Dict params;
params.Set("url", "about:blank");
params.Set("browserContextId", context_id);
SendCommandSync("Target.createTarget", std::move(params));
params = base::Value::Dict();
params.Set("browserContextId", context_id);
SendCommandSync("Target.disposeBrowserContext", std::move(params));
}
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
NewTabPageInCreatedContextDoesNotCrash) {
AttachToBrowserTarget();
const base::Value::Dict* result =
SendCommandSync("Target.createBrowserContext");
std::string context_id = *result->FindString("browserContextId");
base::Value::Dict params;
params.Set("url", chrome::kChromeUINewTabURL);
params.Set("browserContextId", context_id);
content::WebContentsAddedObserver observer;
SendCommandSync("Target.createTarget", std::move(params));
content::WebContents* wc = observer.GetWebContents();
ASSERT_TRUE(content::WaitForLoadStop(wc));
EXPECT_EQ(chrome::kChromeUINewTabURL, wc->GetLastCommittedURL().spec());
// Should not crash by this point.
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
CreateBrowserContextAcceptsProxyServer) {
ScopedAllowHttpForHostnamesForTesting allow_http(
{"this-page-does-not-exist.com"},
chrome_test_utils::GetProfile(this)->GetPrefs());
AttachToBrowserTarget();
embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting(
[&](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
std::make_unique<net::test_server::BasicHttpResponse>());
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("text/html");
http_response->set_content("<title>Hello from proxy server!</title>");
return std::move(http_response);
}));
ASSERT_TRUE(embedded_test_server()->Start());
base::Value::Dict params;
params.Set("proxyServer",
embedded_test_server()->host_port_pair().ToString());
const base::Value::Dict* result =
SendCommandSync("Target.createBrowserContext", std::move(params));
std::string context_id = *result->FindString("browserContextId");
content::WebContentsAddedObserver observer;
params = base::Value::Dict();
params.Set("url", "http://this-page-does-not-exist.com/site.html");
params.Set("browserContextId", context_id);
result = SendCommandSync("Target.createTarget", std::move(params));
content::WebContents* wc = observer.GetWebContents();
ASSERT_TRUE(content::WaitForLoadStop(wc));
EXPECT_EQ(GURL("http://this-page-does-not-exist.com/site.html"),
wc->GetURL());
EXPECT_EQ("Hello from proxy server!", base::UTF16ToUTF8(wc->GetTitle()));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateInDefaultContextById) {
AttachToBrowserTarget();
const base::Value::Dict* result = SendCommandSync("Target.getTargets");
const base::Value::List* list = result->FindList("targetInfos");
ASSERT_TRUE(list->size() == 1);
const std::string context_id =
*list->front().GetDict().FindString("browserContextId");
base::Value::Dict params;
params.Set("url", "about:blank");
params.Set("browserContextId", context_id);
result = SendCommandSync("Target.createTarget", std::move(params));
ASSERT_TRUE(result);
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
InputDispatchEventsToCorrectTarget) {
Attach();
std::string setup_logging = R"(
window.logs = [];
['dragenter', 'keydown', 'mousedown', 'mouseenter', 'mouseleave',
'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click', 'touchcancel',
'touchend', 'touchmove', 'touchstart',
].forEach((event) =>
window.addEventListener(event, (e) => logs.push(e.type)));)";
content::WebContents* target_web_contents =
chrome_test_utils::GetActiveWebContents(this);
ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("about:blank"), WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
content::WebContents* other_web_contents =
chrome_test_utils::GetActiveWebContents(this);
EXPECT_TRUE(content::EvalJs(target_web_contents, setup_logging).is_ok());
EXPECT_TRUE(content::EvalJs(other_web_contents, setup_logging).is_ok());
base::Value::Dict params;
params.Set("button", "left");
params.Set("clickCount", 1);
params.Set("x", 100);
params.Set("y", 250);
params.Set("clickCount", 1);
params.Set("type", "mousePressed");
SendCommandSync("Input.dispatchMouseEvent", params.Clone());
params.Set("type", "mouseMoved");
params.Set("y", 270);
SendCommandSync("Input.dispatchMouseEvent", params.Clone());
params.Set("type", "mouseReleased");
SendCommandSync("Input.dispatchMouseEvent", std::move(params));
params = base::Value::Dict();
params.Set("x", 100);
params.Set("y", 250);
params.Set("type", "dragEnter");
params.SetByDottedPath("data.dragOperationsMask", 1);
params.SetByDottedPath("data.items", base::Value::List());
SendCommandSync("Input.dispatchDragEvent", std::move(params));
params = base::Value::Dict();
params.Set("x", 100);
params.Set("y", 250);
SendCommandSync("Input.synthesizeTapGesture", std::move(params));
params = base::Value::Dict();
params.Set("type", "keyDown");
params.Set("key", "a");
SendCommandSync("Input.dispatchKeyEvent", std::move(params));
content::EvalJsResult main_target_events =
content::EvalJs(target_web_contents, "logs.join(' ')");
content::EvalJsResult other_target_events =
content::EvalJs(other_web_contents, "logs.join(' ')");
// mouse events might happen in the other_target if the real mouse pointer
// happens to be over the browser window
EXPECT_THAT(
base::SplitString(main_target_events.ExtractString(), " ",
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL),
AllOf(Contains("mouseover"), Contains("mousedown"), Contains("mousemove"),
Contains("mouseup"), Contains("click"), Contains("dragenter"),
Contains("keydown")));
EXPECT_THAT(base::SplitString(other_target_events.ExtractString(), " ",
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL),
AllOf(Not(Contains("click")), Not(Contains("dragenter")),
Not(Contains("keydown"))));
}
#endif // !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
NoInputEventsSentToBrowserWhenDisallowed) {
SetIsTrusted(false);
Attach();
base::Value::Dict params;
params.Set("type", "rawKeyDown");
params.Set("key", "F12");
params.Set("windowsVirtualKeyCode", 123);
params.Set("nativeVirtualKeyCode", 123);
SendCommandSync("Input.dispatchKeyEvent", std::move(params));
EXPECT_EQ(nullptr, DevToolsWindow::FindDevToolsWindow(agent_host_.get()));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
PreloadEnabledStateUpdatedDefault) {
Attach();
SendCommandAsync("Preload.enable");
const base::Value::Dict result =
WaitForNotification("Preload.preloadEnabledStateUpdated", true);
EXPECT_THAT(result.FindBool("disabledByPreference"), false);
EXPECT_THAT(result.FindBool("disabledByHoldbackPrefetchSpeculationRules"),
false);
EXPECT_THAT(result.FindBool("disabledByHoldbackPrerenderSpeculationRules"),
false);
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
PreloadEnabledStateUpdatedDisabledByPreference) {
Attach();
prefetch::SetPreloadPagesState(
chrome_test_utils::GetProfile(this)->GetPrefs(),
prefetch::PreloadPagesState::kNoPreloading);
SendCommandAsync("Preload.enable");
const base::Value::Dict result =
WaitForNotification("Preload.preloadEnabledStateUpdated", true);
EXPECT_THAT(result.FindBool("disabledByPreference"), true);
}
class DevToolsProtocolTest_PreloadEnabledStateUpdatedDisabledByHoldback
: public DevToolsProtocolTest {
protected:
void SetUp() override {
// This holds back speculation rules prefetch and prerender. Note that
// directly using enums (instead of strings) in the call to SetHoldback is
// usually preferred, but this is not possible here because
// content::content_preloading_predictor::kSpeculationRules, which is not
// exposed outside of content.
preloading_config_override_.SetHoldback("Prefetch", "SpeculationRules",
true);
preloading_config_override_.SetHoldback("Prerender", "SpeculationRules",
true);
DevToolsProtocolTest::SetUp();
}
private:
content::test::PreloadingConfigOverride preloading_config_override_;
};
IN_PROC_BROWSER_TEST_F(
DevToolsProtocolTest_PreloadEnabledStateUpdatedDisabledByHoldback,
PreloadEnabledStateUpdatedDisabledByHoldback) {
Attach();
SendCommandAsync("Preload.enable");
const base::Value::Dict result =
WaitForNotification("Preload.preloadEnabledStateUpdated", true);
EXPECT_THAT(result.FindBool("disabledByHoldbackPrefetchSpeculationRules"),
true);
EXPECT_THAT(result.FindBool("disabledByHoldbackPrerenderSpeculationRules"),
true);
}
class DevToolsProtocolTest_PrefetchHoldbackDisabledIfCDPClientConnected
: public DevToolsProtocolTest {
protected:
void SetUp() override {
preloading_config_override_.SetHoldback("Prefetch", "SpeculationRules",
true);
DevToolsProtocolTest::SetUp();
}
private:
content::test::PreloadingConfigOverride preloading_config_override_;
};
// Check that prefetch is enabled if DevToolsAgentHost exists even if it is
// disabled by PreloadingConfig.
IN_PROC_BROWSER_TEST_F(
DevToolsProtocolTest_PrefetchHoldbackDisabledIfCDPClientConnected,
PrefetchHoldbackDisabledIfCDPClientConnected) {
Attach();
{
SendCommandAsync("Preload.enable");
const base::Value::Dict result =
WaitForNotification("Preload.preloadEnabledStateUpdated", true);
EXPECT_THAT(result.FindBool("disabledByHoldbackPrefetchSpeculationRules"),
true);
}
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(embedded_test_server()->GetURL("/empty.html"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
const std::string add_specrules = R"(
const specrules = document.createElement("script");
specrules.type = "speculationrules";
specrules.text = `
{
"prefetch":[
{
"source": "list",
"urls": ["title1.html"]
}
]
}`;
document.body.appendChild(specrules);
)";
EXPECT_TRUE(content::EvalJs(web_contents(), add_specrules).is_ok());
{
base::Value::Dict result;
while (true) {
result = WaitForNotification("Preload.prefetchStatusUpdated", true);
if (*result.FindString("status") == "Ready") {
break;
}
}
}
}
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(
DevToolsProtocolTest,
NoPendingUrlShownWhenAttachedToBrowserInitiatedFailedNavigation) {
GURL url("invalid.scheme:for-sure");
ui_test_utils::AllBrowserTabAddedWaiter tab_added_waiter;
content::WebContents* web_contents = browser()->OpenURL(
content::OpenURLParams(url, content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_TYPED, false),
/*navigation_handle_callback=*/{});
tab_added_waiter.Wait();
ASSERT_TRUE(WaitForLoadStop(web_contents));
content::NavigationController& navigation_controller =
web_contents->GetController();
content::NavigationEntry* pending_entry =
navigation_controller.GetPendingEntry();
ASSERT_NE(nullptr, pending_entry);
EXPECT_EQ(url, pending_entry->GetURL());
EXPECT_EQ(pending_entry, navigation_controller.GetVisibleEntry());
agent_host_ = content::DevToolsAgentHost::GetOrCreateFor(web_contents);
agent_host_->AttachClient(this);
SendCommandSync("Page.enable");
// Ensure that a failed pending entry is cleared when the DevTools protocol
// attaches, so that any modified page content is not attributed to the failed
// URL. (crbug/1192417)
EXPECT_EQ(nullptr, navigation_controller.GetPendingEntry());
EXPECT_EQ(GURL(""), navigation_controller.GetVisibleEntry()->GetURL());
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
NoPendingUrlShownForPageNavigateFromChromeExtension) {
GURL url("https://example.com");
// DevTools protocol use cases that have an initiator origin (e.g., for
// extensions) should use renderer-initiated navigations and be subject to URL
// spoof defenses.
SetNavigationInitiatorOrigin(
url::Origin::Create(GURL("chrome-extension://abc123/")));
// Attach DevTools and start a navigation but don't wait for it to finish.
Attach();
SendCommandSync("Page.enable");
base::Value::Dict params;
params.Set("url", url.spec());
SendCommandAsync("Page.navigate", std::move(params));
content::NavigationController& navigation_controller =
web_contents()->GetController();
content::NavigationEntry* pending_entry =
navigation_controller.GetPendingEntry();
ASSERT_NE(nullptr, pending_entry);
EXPECT_EQ(url, pending_entry->GetURL());
// Attaching the DevTools protocol to the initial empty document of a new tab
// should prevent the pending URL from being visible, since the protocol
// allows modifying the initial empty document in a way that could be useful
// for URL spoofs.
EXPECT_NE(pending_entry, navigation_controller.GetVisibleEntry());
EXPECT_NE(nullptr, navigation_controller.GetPendingEntry());
EXPECT_EQ(GURL("about:blank"),
navigation_controller.GetVisibleEntry()->GetURL());
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, SetRPHRegistrationMode) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(embedded_test_server()->GetURL("/empty.html"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
Attach();
// Initial value
custom_handlers::ProtocolHandlerRegistry* registry =
ProtocolHandlerRegistryFactory::GetForBrowserContext(
chrome_test_utils::GetProfile(this));
EXPECT_EQ(custom_handlers::RphRegistrationMode::kNone,
registry->registration_mode());
// Set a invalid value
base::Value::Dict params_invalid;
params_invalid.Set("mode", "accept");
SendCommand("Page.setRPHRegistrationMode", std::move(params_invalid));
EXPECT_EQ(custom_handlers::RphRegistrationMode::kNone,
registry->registration_mode());
// Set a valid value
base::Value::Dict params;
params.Set("mode", "autoAccept");
SendCommand("Page.setRPHRegistrationMode", std::move(params));
EXPECT_EQ(custom_handlers::RphRegistrationMode::kAutoAccept,
registry->registration_mode());
}
class DevToolsProtocolTest_BounceTrackingMitigations
: public DevToolsProtocolTest {
protected:
void SetUp() override {
scoped_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/{{features::kBtm,
{{"triggering_action", "stateful_bounce"}}}},
/*disabled_features=*/{});
DevToolsProtocolTest::SetUp();
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
DevToolsProtocolTest::SetUpOnMainThread();
}
void SetBlockThirdPartyCookies(bool value) {
chrome_test_utils::GetProfile(this)->GetPrefs()->SetInteger(
prefs::kCookieControlsMode,
static_cast<int>(
value ? content_settings::CookieControlsMode::kBlockThirdParty
: content_settings::CookieControlsMode::kOff));
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
testing::AssertionResult SimulateBtmBounce(content::WebContents* web_contents,
const GURL& initial_url,
const GURL& bounce_url,
const GURL& final_url) {
web_contents = web_contents->OpenURL(
content::OpenURLParams(initial_url, content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PageTransition::PAGE_TRANSITION_TYPED,
/*is_renderer_initiated=*/false),
{});
if (!web_contents) {
return testing::AssertionFailure() << "OpenURL() returned nullptr";
}
if (!content::WaitForLoadStop(web_contents)) {
return testing::AssertionFailure() << "Failed to wait for loading to stop";
}
content::BtmService* btm_service =
content::BtmService::Get(web_contents->GetBrowserContext());
if (!content::NavigateToURLFromRenderer(web_contents, bounce_url)) {
return testing::AssertionFailure()
<< "Failed to navigate to " << bounce_url;
}
tpcd::trial::URLCookieAccessObserver cookie_observer(
web_contents, bounce_url, tpcd::trial::CookieOperation::kChange);
testing::AssertionResult js_result =
content::ExecJs(web_contents, "document.cookie = 'bounce=stateful';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE);
if (!js_result) {
return js_result;
}
cookie_observer.Wait();
content::BtmRedirectChainObserver final_observer(btm_service, final_url);
if (!content::NavigateToURLFromRendererWithoutUserGesture(web_contents,
final_url)) {
return testing::AssertionFailure() << "Failed to navigate to " << final_url;
}
// End redirect chain by closing the tab.
web_contents->Close();
final_observer.Wait();
if (testing::Test::HasFailure()) {
return testing::AssertionFailure() << "Failure generated while waiting for "
"the redirect chain to be reported";
}
if (final_observer.redirects()->size() != 1) {
return testing::AssertionFailure() << "Expected 1 redirect; found "
<< final_observer.redirects()->size();
}
const content::BtmRedirectInfo& redirect = *final_observer.redirects()->at(0);
if (redirect.redirector.url != bounce_url) {
return testing::AssertionFailure() << "Expected redirect at " << bounce_url
<< "; found " << redirect.redirector.url;
}
if (redirect.access_type != content::BtmDataAccessType::kWrite &&
redirect.access_type != content::BtmDataAccessType::kReadWrite) {
return testing::AssertionFailure()
<< "No write access recorded for redirect";
}
return testing::AssertionSuccess();
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_BounceTrackingMitigations,
RunBounceTrackingMitigations) {
SetBlockThirdPartyCookies(true);
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(embedded_test_server()->GetURL("/empty.html"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
Attach();
const GURL bouncer(
embedded_test_server()->GetURL("example.test", "/title1.html"));
// Record a stateful bounce for `bouncer`.
ASSERT_TRUE(SimulateBtmBounce(
web_contents(), embedded_test_server()->GetURL("a.test", "/empty.html"),
bouncer, embedded_test_server()->GetURL("b.test", "/empty.html")));
SendCommandSync("Storage.runBounceTrackingMitigations");
const base::Value::List* deleted_sites_list =
result()->FindList("deletedSites");
ASSERT_TRUE(deleted_sites_list);
std::vector<std::string> deleted_sites;
for (const auto& site : *deleted_sites_list) {
deleted_sites.push_back(site.GetString());
}
EXPECT_THAT(deleted_sites, testing::ElementsAre("example.test"));
}
class BtmStatusDevToolsProtocolTest
: public DevToolsProtocolTest,
public testing::WithParamInterface<std::tuple<bool, std::string>> {
// The fields of `GetParam()` indicate/control the following:
// `std::get<0>(GetParam())` => `features::kBtm`
// `std::get<1>(GetParam())` => `features::kBtmTriggeringAction`
//
// In order for Bounce Tracking Mitigations to take effect, `features::kBtm`
// must be enabled, and `features::kTriggeringAction` must NOT be none.
//
// Note: Bounce Tracking Mitigations issues only report sites that would
// be affected when `features::kTriggeringAction` is set to stateful_bounce.
protected:
void SetUp() override {
if (std::get<0>(GetParam())) {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
features::kBtm, {{"triggering_action", std::get<1>(GetParam())}});
} else {
scoped_feature_list_.InitAndDisableFeature(features::kBtm);
}
DevToolsProtocolTest::SetUp();
}
bool ShouldBeEnabled() {
return (std::get<0>(GetParam()) && (std::get<1>(GetParam()) != "none"));
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_P(BtmStatusDevToolsProtocolTest,
TrueWhenEnabledAndDeleting) {
AttachToBrowserTarget();
base::Value::Dict btm_params;
btm_params.Set("featureState", "DIPS");
SendCommand("SystemInfo.getFeatureState", std::move(btm_params));
EXPECT_EQ(result()->FindBool("featureEnabled"), ShouldBeEnabled());
}
INSTANTIATE_TEST_SUITE_P(
All,
BtmStatusDevToolsProtocolTest,
::testing::Combine(
::testing::Bool(),
::testing::Values("none", "storage", "bounce", "stateful_bounce")));
using DevToolsProtocolTest_AppId = DevToolsProtocolTest;
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, ReturnsManifestAppId) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(embedded_test_server()->GetURL(
"/banners/manifest_test_page.html?manifest=manifest_with_id.json"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
Attach();
const base::Value::Dict* result = SendCommandSync("Page.getAppId");
EXPECT_EQ(*result->FindString("appId"),
embedded_test_server()->GetURL("/some_id"));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId,
ReturnsStartUrlAsManifestAppIdIfNotSet) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(
embedded_test_server()->GetURL("/web_apps/no_service_worker.html"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
Attach();
const base::Value::Dict* result = SendCommandSync("Page.getAppId");
EXPECT_EQ(*result->FindString("appId"),
embedded_test_server()->GetURL("/web_apps/no_service_worker.html"));
EXPECT_EQ(*result->FindString("recommendedId"),
"/web_apps/no_service_worker.html");
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, ReturnsNoAppIdIfNoManifest) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(embedded_test_server()->GetURL("/empty.html"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
Attach();
const base::Value::Dict* result = SendCommandSync("Page.getAppId");
EXPECT_FALSE(result->Find("appId"));
EXPECT_FALSE(result->Find("recommendedId"));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, VisibleSecurityStateSecureState) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.ServeFilesFromSourceDirectory(GetChromeTestDataDir());
ASSERT_TRUE(https_server.Start());
ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(
browser(), https_server.GetURL("/title1.html"), 1);
content::NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
ASSERT_TRUE(entry);
// Extract SSL status data from the navigation entry.
scoped_refptr<net::X509Certificate> page_cert = entry->GetSSL().certificate;
ASSERT_TRUE(page_cert);
int ssl_version =
net::SSLConnectionStatusToVersion(entry->GetSSL().connection_status);
const char* page_protocol;
net::SSLVersionToString(&page_protocol, ssl_version);
const char* page_key_exchange_str;
const char* page_cipher;
const char* page_mac;
bool is_aead;
bool is_tls13;
uint16_t page_cipher_suite =
net::SSLConnectionStatusToCipherSuite(entry->GetSSL().connection_status);
net::SSLCipherSuiteToStrings(&page_key_exchange_str, &page_cipher, &page_mac,
&is_aead, &is_tls13, page_cipher_suite);
std::string page_key_exchange;
if (page_key_exchange_str)
page_key_exchange = page_key_exchange_str;
const char* page_key_exchange_group =
SSL_get_curve_name(entry->GetSSL().key_exchange_group);
std::string page_subject_name;
std::string page_issuer_name;
double page_valid_from = 0.0;
double page_valid_to = 0.0;
if (entry->GetSSL().certificate) {
page_subject_name = entry->GetSSL().certificate->subject().common_name;
page_issuer_name = entry->GetSSL().certificate->issuer().common_name;
page_valid_from =
entry->GetSSL().certificate->valid_start().InSecondsFSinceUnixEpoch();
page_valid_to =
entry->GetSSL().certificate->valid_expiry().InSecondsFSinceUnixEpoch();
}
std::string page_certificate_network_error;
if (net::IsCertStatusError(entry->GetSSL().cert_status)) {
page_certificate_network_error = net::ErrorToString(
net::MapCertStatusToNetError(entry->GetSSL().cert_status));
}
bool page_certificate_has_weak_signature =
(entry->GetSSL().cert_status & net::CERT_STATUS_WEAK_SIGNATURE_ALGORITHM);
bool page_certificate_has_sha1_signature_present =
(entry->GetSSL().cert_status & net::CERT_STATUS_SHA1_SIGNATURE_PRESENT);
int status = net::ObsoleteSSLStatus(entry->GetSSL().connection_status,
entry->GetSSL().peer_signature_algorithm);
bool page_modern_ssl = status == net::OBSOLETE_SSL_NONE;
bool page_obsolete_ssl_protocol = status & net::OBSOLETE_SSL_MASK_PROTOCOL;
bool page_obsolete_ssl_key_exchange =
status & net::OBSOLETE_SSL_MASK_KEY_EXCHANGE;
bool page_obsolete_ssl_cipher = status & net::OBSOLETE_SSL_MASK_CIPHER;
bool page_obsolete_ssl_signature = status & net::OBSOLETE_SSL_MASK_SIGNATURE;
Attach();
SendCommandAsync("Security.enable");
auto has_certificate = [](const base::Value::Dict& params) {
return params.FindListByDottedPath(
"visibleSecurityState.certificateSecurityState.certificate") !=
nullptr;
};
base::Value::Dict params =
WaitForMatchingNotification("Security.visibleSecurityStateChanged",
base::BindRepeating(has_certificate));
// Verify that the visibleSecurityState payload matches the SSL status data.
std::string* security_state =
params.FindStringByDottedPath("visibleSecurityState.securityState");
ASSERT_TRUE(security_state);
ASSERT_EQ(std::string("secure"), *security_state);
base::Value* certificate_security_state =
params.FindByDottedPath("visibleSecurityState.certificateSecurityState");
ASSERT_TRUE(certificate_security_state);
base::Value::Dict& dict = certificate_security_state->GetDict();
std::string* protocol = dict.FindString("protocol");
ASSERT_TRUE(protocol);
ASSERT_EQ(*protocol, page_protocol);
std::string* key_exchange = dict.FindString("keyExchange");
ASSERT_TRUE(key_exchange);
ASSERT_EQ(*key_exchange, page_key_exchange);
std::string* key_exchange_group = dict.FindString("keyExchangeGroup");
if (key_exchange_group) {
ASSERT_EQ(*key_exchange_group, page_key_exchange_group);
}
std::string* mac = dict.FindString("mac");
if (mac) {
ASSERT_EQ(*mac, page_mac);
}
std::string* cipher = dict.FindString("cipher");
ASSERT_TRUE(cipher);
ASSERT_EQ(*cipher, page_cipher);
std::string* subject_name = dict.FindString("subjectName");
ASSERT_TRUE(subject_name);
ASSERT_EQ(*subject_name, page_subject_name);
std::string* issuer = dict.FindString("issuer");
ASSERT_TRUE(issuer);
ASSERT_EQ(*issuer, page_issuer_name);
auto valid_from = dict.FindDouble("validFrom");
ASSERT_TRUE(valid_from);
ASSERT_EQ(*valid_from, page_valid_from);
auto valid_to = dict.FindDouble("validTo");
ASSERT_TRUE(valid_to);
ASSERT_EQ(*valid_to, page_valid_to);
std::string* certificate_network_error =
dict.FindString("certificateNetworkError");
if (certificate_network_error) {
ASSERT_EQ(*certificate_network_error, page_certificate_network_error);
}
auto certificate_has_weak_signature =
dict.FindBool("certificateHasWeakSignature");
ASSERT_TRUE(certificate_has_weak_signature);
ASSERT_EQ(*certificate_has_weak_signature,
page_certificate_has_weak_signature);
auto certificate_has_sha1_signature_present =
dict.FindBool("certificateHasSha1Signature");
ASSERT_TRUE(certificate_has_sha1_signature_present);
ASSERT_EQ(*certificate_has_sha1_signature_present,
page_certificate_has_sha1_signature_present);
auto modern_ssl = dict.FindBool("modernSSL");
ASSERT_TRUE(modern_ssl);
ASSERT_EQ(*modern_ssl, page_modern_ssl);
auto obsolete_ssl_protocol = dict.FindBool("obsoleteSslProtocol");
ASSERT_TRUE(obsolete_ssl_protocol);
ASSERT_EQ(*obsolete_ssl_protocol, page_obsolete_ssl_protocol);
auto obsolete_ssl_key_exchange = dict.FindBool("obsoleteSslKeyExchange");
ASSERT_TRUE(obsolete_ssl_key_exchange);
ASSERT_EQ(*obsolete_ssl_key_exchange, page_obsolete_ssl_key_exchange);
auto obsolete_ssl_cipher = dict.FindBool("obsoleteSslCipher");
ASSERT_TRUE(obsolete_ssl_cipher);
ASSERT_EQ(*obsolete_ssl_cipher, page_obsolete_ssl_cipher);
auto obsolete_ssl_signature = dict.FindBool("obsoleteSslSignature");
ASSERT_TRUE(obsolete_ssl_signature);
ASSERT_EQ(*obsolete_ssl_signature, page_obsolete_ssl_signature);
const base::Value::List* certificate_value = dict.FindList("certificate");
ASSERT_TRUE(certificate_value);
std::vector<std::string> der_certs;
for (const auto& cert : *certificate_value) {
std::string decoded;
ASSERT_TRUE(base::Base64Decode(cert.GetString(), &decoded));
der_certs.push_back(decoded);
}
std::vector<std::string_view> cert_string_piece;
for (const auto& str : der_certs) {
cert_string_piece.push_back(str);
}
// Check that the certificateSecurityState.certificate matches.
net::SHA256HashValue page_cert_chain_fingerprint =
page_cert->CalculateChainFingerprint256();
scoped_refptr<net::X509Certificate> certificate =
net::X509Certificate::CreateFromDERCertChain(cert_string_piece);
ASSERT_TRUE(certificate);
EXPECT_EQ(page_cert_chain_fingerprint,
certificate->CalculateChainFingerprint256());
const base::Value* security_state_issue_ids =
params.FindByDottedPath("visibleSecurityState.securityStateIssueIds");
ASSERT_TRUE(security_state_issue_ids->is_list());
EXPECT_EQ(security_state_issue_ids->GetList().size(), 0u);
EXPECT_FALSE(params.FindByDottedPath("visibleSecurityState.safetyTipInfo"));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
AutomationOverrideShowsAndRemovesInfoBar) {
Attach();
auto* manager = infobars::ContentInfoBarManager::FromWebContents(
chrome_test_utils::GetActiveWebContents(this));
{
base::Value::Dict params;
params.Set("enabled", true);
SendCommandSync("Emulation.setAutomationOverride", std::move(params));
}
EXPECT_EQ(manager->infobars().size(), 1u);
{
base::Value::Dict params;
params.Set("enabled", false);
SendCommandSync("Emulation.setAutomationOverride", std::move(params));
}
EXPECT_EQ(manager->infobars().size(), 0u);
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
AutomationOverrideAddsOneInfoBarOnly) {
Attach();
auto* manager = infobars::ContentInfoBarManager::FromWebContents(
chrome_test_utils::GetActiveWebContents(this));
{
base::Value::Dict params;
params.Set("enabled", true);
SendCommandSync("Emulation.setAutomationOverride", std::move(params));
}
EXPECT_EQ(manager->infobars().size(), 1u);
{
base::Value::Dict params;
params.Set("enabled", true);
SendCommandSync("Emulation.setAutomationOverride", std::move(params));
}
EXPECT_EQ(manager->infobars().size(), 1u);
}
#endif // !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, UntrustedClient) {
SetIsTrusted(false);
Attach();
EXPECT_FALSE(SendCommandSync("HeapProfiler.enable")); // Implemented in V8
EXPECT_FALSE(SendCommandSync("LayerTree.enable")); // Implemented in blink
EXPECT_FALSE(SendCommandSync(
"Memory.prepareForLeakDetection")); // Implemented in content
EXPECT_FALSE(SendCommandSync("Cast.enable")); // Implemented in content
EXPECT_TRUE(SendCommandSync("Accessibility.enable"));
}
#if !BUILDFLAG(IS_ANDROID)
class DevToolsProtocolScreenshotTest : public DevToolsProtocolTest {
protected:
void SetUp() override {
EnablePixelOutput();
DevToolsProtocolTest::SetUp();
}
SkBitmap CaptureScreenshot() {
SendCommandSync("Page.captureScreenshot");
CHECK(!error());
const std::string* base64_data = result()->FindString("data");
CHECK(base64_data);
std::optional<std::vector<uint8_t>> png_data =
base::Base64Decode(*base64_data);
SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
CHECK(!bitmap.isNull());
return bitmap;
}
};
IN_PROC_BROWSER_TEST_F(DevToolsProtocolScreenshotTest, ScreenshotInactiveTab) {
static constexpr char kBluePageURL[] =
R"(data:text/html,<body style="background-color: blue"></body>)";
static constexpr char kRedPageURL[] =
R"(data:text/html,<body style="background-color: red"></body>)";
ASSERT_TRUE(content::NavigateToURL(web_contents(), GURL(kBluePageURL)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
Attach();
constexpr int kIndex = 1;
ASSERT_TRUE(AddTabAtIndex(kIndex, GURL(kRedPageURL),
ui::PageTransition::PAGE_TRANSITION_TYPED));
SkBitmap bitmap = CaptureScreenshot();
SkColor pixel_color = bitmap.getColor(100, 100);
EXPECT_EQ(SK_ColorBLUE, pixel_color);
}
#endif // !BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(ENABLE_EXTENSIONS)
class ExtensionProtocolTest : public DevToolsProtocolTest {
protected:
void SetUpOnMainThread() override {
DevToolsProtocolTest::SetUpOnMainThread();
Profile* profile = chrome_test_utils::GetProfile(this);
extension_registrar_ = extensions::ExtensionRegistrar::Get(profile);
extension_registry_ = extensions::ExtensionRegistry::Get(profile);
}
content::WebContents* web_contents() override {
return background_web_contents_;
}
const extensions::Extension* LoadExtensionOrApp(
const base::FilePath& extension_path) {
extensions::TestExtensionRegistryObserver observer(extension_registry_);
extensions::UnpackedInstaller::Create(chrome_test_utils::GetProfile(this))
->Load(extension_path);
observer.WaitForExtensionLoaded();
const extensions::Extension* extension = nullptr;
for (const auto& enabled_extension :
extension_registry_->enabled_extensions()) {
if (enabled_extension->path() == extension_path) {
extension = enabled_extension.get();
break;
}
}
CHECK(extension) << "Failed to find loaded extension " << extension_path;
return extension;
}
const extensions::Extension* LoadExtension(
const base::FilePath& extension_path) {
ExtensionTestMessageListener activated_listener("WORKER_ACTIVATED");
const extensions::Extension* extension = LoadExtensionOrApp(extension_path);
auto* process_manager =
extensions::ProcessManager::Get(chrome_test_utils::GetProfile(this));
if (extensions::BackgroundInfo::IsServiceWorkerBased(extension)) {
EXPECT_TRUE(activated_listener.WaitUntilSatisfied());
auto worker_ids =
process_manager->GetServiceWorkersForExtension(extension->id());
CHECK_EQ(1lu, worker_ids.size());
} else {
extensions::ExtensionHost* host =
process_manager->GetBackgroundHostForExtension(extension->id());
background_web_contents_ = host->host_contents();
}
return extension;
}
void LaunchApp(const std::string& app_id) {
apps::AppLaunchParams params(
app_id, apps::LaunchContainer::kLaunchContainerNone,
WindowOpenDisposition::NEW_WINDOW, apps::LaunchSource::kFromTest);
apps::AppServiceProxyFactory::GetForProfile(
chrome_test_utils::GetProfile(this))
->BrowserAppLauncher()
->LaunchAppWithParamsForTesting(std::move(params));
}
void ReloadExtension(const std::string& extension_id) {
extensions::TestExtensionRegistryObserver observer(extension_registry_);
extension_registrar_->ReloadExtension(extension_id);
observer.WaitForExtensionLoaded();
}
private:
raw_ptr<extensions::ExtensionRegistrar, DanglingUntriaged>
extension_registrar_;
raw_ptr<extensions::ExtensionRegistry, DanglingUntriaged> extension_registry_;
raw_ptr<content::WebContents, DanglingUntriaged> background_web_contents_;
#if BUILDFLAG(IS_WIN)
// This is needed to stop ExtensionProtocolTestsfrom creating a
// shortcut in the Windows start menu. The override needs to last until the
// test is destroyed, because Windows shortcut tasks which create the shortcut
// can run after the test body returns.
base::ScopedPathOverride override_start_dir{base::DIR_START_MENU};
#endif // BUILDFLAG(IS_WIN
// TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
extensions::ScopedTestMV2Enabler mv2_enabler_;
};
IN_PROC_BROWSER_TEST_F(ExtensionProtocolTest, ReloadTracedExtension) {
base::FilePath extension_path =
base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("devtools")
.AppendASCII("extensions")
.AppendASCII("simple_background_page");
auto* extension = LoadExtension(extension_path);
ASSERT_TRUE(extension);
Attach();
ReloadExtension(extension->id());
base::Value::Dict params;
params.Set("categories", "-*");
SendCommandSync("Tracing.start", std::move(params));
SendCommandAsync("Tracing.end");
WaitForNotification("Tracing.tracingComplete", true);
}
IN_PROC_BROWSER_TEST_F(ExtensionProtocolTest,
DISABLED_ReloadServiceWorkerExtension) {
base::FilePath extension_path =
base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("devtools")
.AppendASCII("extensions")
.AppendASCII("service_worker");
std::string extension_id;
{
// `extension` is stale after reload.
auto* extension = LoadExtension(extension_path);
ASSERT_THAT(extension, testing::NotNull());
extension_id = extension->id();
}
AttachToBrowserTarget();
const base::Value::Dict* result = SendCommandSync("Target.getTargets");
base::Value::Dict ext_target;
for (const auto& target : *result->FindList("targetInfos")) {
if (*target.GetDict().FindString("type") == "service_worker") {
ext_target = target.Clone().TakeDict();
break;
}
}
{
base::Value::Dict params;
params.Set("targetId", CHECK_DEREF(ext_target.FindString("targetId")));
params.Set("waitForDebuggerOnStart", false);
SendCommandSync("Target.autoAttachRelated", std::move(params));
}
ReloadExtension(extension_id);
base::Value::Dict attached =
WaitForNotification("Target.attachedToTarget", true);
base::Value* targetInfo = attached.Find("targetInfo");
ASSERT_THAT(targetInfo, testing::NotNull());
EXPECT_THAT(*targetInfo, base::test::DictionaryHasValue(
"type", base::Value("service_worker")));
EXPECT_THAT(*targetInfo, base::test::DictionaryHasValue(
"url", CHECK_DEREF(ext_target.Find("url"))));
EXPECT_THAT(attached.FindBool("waitingForDebugger"),
testing::Optional(false));
{
base::Value::Dict params;
params.Set("targetId", *targetInfo->GetDict().FindString("targetId"));
params.Set("waitForDebuggerOnStart", false);
SendCommandSync("Target.autoAttachRelated", std::move(params));
}
auto detached = WaitForNotification("Target.detachedFromTarget", true);
EXPECT_THAT(*detached.FindString("sessionId"), Eq("sessionId"));
}
// Accepts a list of URL predicates and allows awaiting for all matching
// WebContents to load.
class WebContentsBarrier {
public:
using Predicate = base::FunctionRef<bool(const GURL& url)>;
WebContentsBarrier(std::initializer_list<Predicate> predicates)
: predicates_(predicates) {}
std::vector<raw_ptr<content::WebContents, VectorExperimental>> Await() {
if (!IsReady()) {
base::RunLoop run_loop;
ready_callback_ = run_loop.QuitClosure();
run_loop.Run();
CHECK(IsReady());
}
return std::move(ready_web_contents_);
}
private:
class LoadObserver : public content::WebContentsObserver {
public:
LoadObserver(content::WebContents& wc, WebContentsBarrier& owner)
: WebContentsObserver(&wc), owner_(owner) {}
private:
void DidFinishLoad(content::RenderFrameHost* host,
const GURL& url) override {
if (host != web_contents()->GetPrimaryMainFrame()) {
return;
}
owner_->OnWebContentsLoaded(web_contents(), url);
}
const raw_ref<WebContentsBarrier> owner_;
};
bool IsReady() const { return !pending_contents_count_; }
void OnWebContentsCreated(content::WebContents* wc) {
observers_.push_back(std::make_unique<LoadObserver>(*wc, *this));
}
void OnWebContentsLoaded(content::WebContents* wc, const GURL& url) {
CHECK(!IsReady());
for (size_t i = 0; i < predicates_.size(); ++i) {
if (!predicates_[i](url)) {
continue;
}
CHECK(!ready_web_contents_[i])
<< " predicate #" << i << " matches "
<< ready_web_contents_[i]->GetLastCommittedURL() << " and " << url;
ready_web_contents_[i] = wc;
--pending_contents_count_;
}
if (IsReady() && ready_callback_) {
std::move(ready_callback_).Run();
}
}
const std::vector<Predicate> predicates_;
std::vector<raw_ptr<content::WebContents, VectorExperimental>>
ready_web_contents_{predicates_.size(), nullptr};
size_t pending_contents_count_{predicates_.size()};
base::CallbackListSubscription creation_subscription_{
content::RegisterWebContentsCreationCallback(
base::BindRepeating(&WebContentsBarrier::OnWebContentsCreated,
base::Unretained(this)))};
std::vector<std::unique_ptr<LoadObserver>> observers_;
base::OnceClosure ready_callback_;
};
// TODO(crbug.com/40202416): Remove this when we remove the inner WebContents
// implementation for guests.
class ExtensionProtocolTestWithGuestViewInnerWebContents
: public ExtensionProtocolTest {
public:
ExtensionProtocolTestWithGuestViewInnerWebContents() {
scoped_feature_list_.InitAndDisableFeature(features::kGuestViewMPArch);
}
~ExtensionProtocolTestWithGuestViewInnerWebContents() override = default;
private:
base::test::ScopedFeatureList scoped_feature_list_;
guest_view::TestGuestViewManagerFactory guest_view_manager_factory_;
};
IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewInnerWebContents,
TabTargetWithGuestView) {
ASSERT_FALSE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
base::FilePath extension_path =
base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("devtools")
.AppendASCII("extensions")
.AppendASCII("app_with_webview");
auto* extension = LoadExtensionOrApp(extension_path);
ASSERT_THAT(extension, testing::NotNull());
WebContentsBarrier barrier(
{[](const GURL& url) -> bool {
return base::EndsWith(url.path(), "host.html");
},
[](const GURL& url) -> bool { return url.SchemeIs(url::kDataScheme); }});
LaunchApp(extension->id());
std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
barrier.Await();
ASSERT_THAT(wcs, testing::SizeIs(2));
EXPECT_NE(wcs[0], wcs[1]);
// Assure host and view have different DevTools hosts.
EXPECT_NE(content::DevToolsAgentHost::GetOrCreateForTab(wcs[0]),
content::DevToolsAgentHost::GetOrCreateForTab(wcs[1]));
// Assure host does not auto-attach view.
AttachToTabTarget(wcs[0]);
base::Value::Dict command_params;
command_params = base::Value::Dict();
command_params.Set("autoAttach", true);
command_params.Set("waitForDebuggerOnStart", false);
command_params.Set("flatten", true);
SendCommandSync("Target.setAutoAttach", std::move(command_params));
EXPECT_FALSE(HasExistingNotificationMatching(
[](const base::Value::Dict& notification) {
if (*notification.FindString("method") != "Target.attachedToTarget") {
return false;
}
const std::string* url =
notification.FindStringByDottedPath("params.targetInfo.url");
return url && base::StartsWith(*url, "data:");
}));
}
class ExtensionProtocolTestWithGuestViewMPArch : public ExtensionProtocolTest {
public:
ExtensionProtocolTestWithGuestViewMPArch() {
scoped_feature_list_.InitAndEnableFeature(features::kGuestViewMPArch);
}
~ExtensionProtocolTestWithGuestViewMPArch() override = default;
guest_view::TestGuestViewManager* GetGuestViewManager() {
return guest_view_manager_factory_.GetOrCreateTestGuestViewManager(
chrome_test_utils::GetProfile(this),
extensions::ExtensionsAPIClient::Get()
->CreateGuestViewManagerDelegate());
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
guest_view::TestGuestViewManagerFactory guest_view_manager_factory_;
};
IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewMPArch,
TabTargetDoesNotAutoAttachGuestView) {
ASSERT_TRUE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
base::FilePath extension_path =
base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("devtools")
.AppendASCII("extensions")
.AppendASCII("app_with_webview");
auto* extension = LoadExtensionOrApp(extension_path);
ASSERT_THAT(extension, testing::NotNull());
WebContentsBarrier barrier({[](const GURL& url) -> bool {
return base::EndsWith(url.path(), "host.html");
}});
LaunchApp(extension->id());
std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
barrier.Await();
ASSERT_THAT(wcs, testing::SizeIs(1));
auto* guest_view = GetGuestViewManager()->WaitForSingleGuestViewCreated();
ASSERT_TRUE(guest_view);
GetGuestViewManager()->WaitUntilAttached(guest_view);
// Assure tab-target does not auto-attach view.
AttachToTabTarget(wcs[0]);
auto command_params = base::Value::Dict()
.Set("autoAttach", true)
.Set("waitForDebuggerOnStart", false)
.Set("flatten", true);
SendCommandSync("Target.setAutoAttach", std::move(command_params));
EXPECT_FALSE(HasExistingNotificationMatching(
[](const base::Value::Dict& notification) {
if (*notification.FindString("method") != "Target.attachedToTarget") {
return false;
}
const std::string* url =
notification.FindStringByDottedPath("params.targetInfo.url");
return url && base::StartsWith(*url, "data:");
}));
}
IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewMPArch,
PrimaryMainFrameTargetAutoAttachesGuestView) {
ASSERT_TRUE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
base::FilePath extension_path =
base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("devtools")
.AppendASCII("extensions")
.AppendASCII("app_with_webview");
auto* extension = LoadExtensionOrApp(extension_path);
ASSERT_THAT(extension, testing::NotNull());
WebContentsBarrier barrier({[](const GURL& url) -> bool {
return base::EndsWith(url.path(), "host.html");
}});
LaunchApp(extension->id());
std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
barrier.Await();
ASSERT_EQ(wcs.size(), 1u);
auto* guest_view = GetGuestViewManager()->WaitForSingleGuestViewCreated();
const std::string devtools_frame_token =
guest_view->GetGuestMainFrame()->GetDevToolsFrameToken().ToString();
ASSERT_TRUE(guest_view);
GetGuestViewManager()->WaitUntilAttached(guest_view);
AttachToWebContents(wcs[0]);
auto command_params = base::Value::Dict()
.Set("autoAttach", true)
.Set("waitForDebuggerOnStart", false)
.Set("flatten", true);
SendCommand("Target.setAutoAttach", std::move(command_params));
base::Value::Dict params =
WaitForNotification("Target.attachedToTarget", /*allow_existing=*/true);
EXPECT_EQ("webview", *params.FindStringByDottedPath("targetInfo.type"));
EXPECT_EQ(devtools_frame_token,
*params.FindStringByDottedPath("targetInfo.targetId"));
EXPECT_EQ(wcs[0]->GetPrimaryMainFrame()->GetDevToolsFrameToken().ToString(),
content::DevToolsAgentHost::GetForId(devtools_frame_token)
->GetParentId());
}
IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewMPArch,
GuestViewIframeContentFrameUpdatedAfterAttach) {
ASSERT_TRUE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
base::FilePath extension_path =
base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("devtools")
.AppendASCII("extensions")
.AppendASCII("app_with_webview");
auto* extension = LoadExtensionOrApp(extension_path);
ASSERT_THAT(extension, testing::NotNull());
WebContentsBarrier barrier({[](const GURL& url) -> bool {
return base::EndsWith(url.path(), "host.html");
}});
LaunchApp(extension->id());
std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
barrier.Await();
ASSERT_EQ(wcs.size(), 1u);
auto* guest_view = GetGuestViewManager()->WaitForSingleGuestViewCreated();
ASSERT_TRUE(guest_view);
GetGuestViewManager()->WaitUntilAttached(guest_view);
AttachToWebContents(wcs[0]);
// Get the document's NodeId.
const base::Value::Dict* result = SendCommandSync("DOM.getDocument");
ASSERT_TRUE(result);
int document_node_id = result->FindIntByDottedPath("root.nodeId").value();
// Get the <webview>'s nodeId (by searching for it using querySelector).
auto params = base::Value::Dict()
.Set("nodeId", document_node_id)
.Set("selector", "webview");
result = SendCommandSync("DOM.querySelector", std::move(params));
ASSERT_TRUE(result);
int web_view_node_id = result->FindInt("nodeId").value();
// Get the <webview> shadow tree (using its nodeId), and retrieve its
// placeholder <iframe>'s frameId. The result from "DOM.describeNode" will
// look something like:
// {"node": {
// ...,
// "shadowRoots": [{
// ...,
// "children": [{
// ...,
// "frameId": "...."
// }]
// }]
// }
params = base::Value::Dict()
.Set("nodeId", web_view_node_id)
.Set("depth", 2)
.Set("pierce", true);
result = SendCommandSync("DOM.describeNode", std::move(params));
ASSERT_TRUE(result);
auto* frame_id = result->FindListByDottedPath("node.shadowRoots")
->front()
.GetDict()
.FindList("children")
->front()
.GetDict()
.FindString("frameId");
ASSERT_TRUE(frame_id);
// The frameId (i.e. the placeholder RemoteFrame's devtools_frame_token)
// should match the devtools_frame_token of the guest's main frame.
EXPECT_EQ(
*frame_id,
guest_view->GetGuestMainFrame()->GetDevToolsFrameToken().ToString());
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
class PrerenderDataSaverProtocolTest : public DevToolsProtocolTest {
public:
PrerenderDataSaverProtocolTest()
: prerender_helper_(base::BindRepeating(
&PrerenderDataSaverProtocolTest::GetActiveWebContents,
base::Unretained(this))) {}
protected:
void SetUp() override {
prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
data_saver::OverrideIsDataSaverEnabledForTesting(true);
DevToolsProtocolTest::SetUp();
}
content::test::PrerenderTestHelper* prerender_helper() {
return &prerender_helper_;
}
void TearDown() override {
data_saver::ResetIsDataSaverEnabledForTesting();
DevToolsProtocolTest::TearDown();
}
content::WebContents* GetActiveWebContents() {
return chrome_test_utils::GetActiveWebContents(this);
}
private:
content::test::PrerenderTestHelper prerender_helper_;
};
IN_PROC_BROWSER_TEST_F(PrerenderDataSaverProtocolTest,
CheckReportedDisabledByDataSaverPreloadingState) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
SendCommandSync("Runtime.enable");
const GURL initial_url = embedded_test_server()->GetURL("/empty.html");
const GURL prerendering_url =
embedded_test_server()->GetURL("/empty.html?prerender");
content::test::PrerenderHostRegistryObserver observer(
*GetActiveWebContents());
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), initial_url));
prerender_helper()->AddPrerenderAsync(prerendering_url);
observer.WaitForTrigger(prerendering_url);
content::FrameTreeNodeId host_id =
prerender_helper()->GetHostForUrl(prerendering_url);
EXPECT_TRUE(host_id.is_null());
SendCommandAsync("Preload.enable");
const base::Value::Dict result =
WaitForNotification("Preload.preloadEnabledStateUpdated", true);
EXPECT_THAT(result.FindBool("disabledByDataSaver"), true);
histogram_tester.ExpectUniqueSample(
"Prerender.Experimental.PrerenderHostFinalStatus.SpeculationRule",
/*PrerenderFinalStatus::kDataSaverEnabled=*/38, 1);
}
#if !BUILDFLAG(IS_ANDROID)
class PrivacySandboxAttestationsOverrideTest
: public InProcessBrowserTestMixinHostSupport<DevToolsProtocolTest> {
public:
PrivacySandboxAttestationsOverrideTest() = default;
private:
privacy_sandbox::PrivacySandboxAttestationsMixin
privacy_sandbox_attestations_mixin_{&mixin_host_};
};
IN_PROC_BROWSER_TEST_F(PrivacySandboxAttestationsOverrideTest,
PrivacySandboxEnrollmentOverride) {
Attach();
base::Value::Dict btm_params;
const std::string attestation_url = "https://google.com";
btm_params.Set("url", attestation_url);
SendCommand("Browser.addPrivacySandboxEnrollmentOverride",
std::move(btm_params));
EXPECT_TRUE(
privacy_sandbox::PrivacySandboxAttestations::GetInstance()->IsOverridden(
net::SchemefulSite(GURL(attestation_url))));
}
IN_PROC_BROWSER_TEST_F(PrivacySandboxAttestationsOverrideTest,
PrivacySandboxEnrollmentOverrideInvalidUrl) {
Attach();
base::Value::Dict btm_params;
const std::string attestation_url = "this is a bad url";
btm_params.Set("url", attestation_url);
SendCommand("Browser.addPrivacySandboxEnrollmentOverride",
std::move(btm_params));
EXPECT_TRUE(error());
EXPECT_FALSE(
privacy_sandbox::PrivacySandboxAttestations::GetInstance()->IsOverridden(
net::SchemefulSite(GURL(attestation_url))));
}
class DevToolsProtocolTest_RelatedWebsiteSets : public DevToolsProtocolTest {
protected:
const char* kPrimarySite = "https://a.test";
const char* kAssociatedSite = "https://b.test";
const char* kServiceSite = "https://c.test";
const char* kPrimaryCcTLD = "https://a.cctld";
void SetUpCommandLine(base::CommandLine* command_line) override {
DevToolsProtocolTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
network::switches::kUseRelatedWebsiteSet,
base::StringPrintf(R"({"primary": "%s",)"
R"("associatedSites": ["%s"],)"
R"("serviceSites": ["%s"],)"
R"("ccTLDs": {"%s": ["%s"]}})",
kPrimarySite, kAssociatedSite, kServiceSite,
kPrimarySite, kPrimaryCcTLD));
}
};
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_RelatedWebsiteSets,
GetRelatedWebsiteSets) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url(embedded_test_server()->GetURL("/empty.html"));
ASSERT_TRUE(content::NavigateToURL(
chrome_test_utils::GetActiveWebContents(this), url));
Attach();
SendCommandSync("Storage.getRelatedWebsiteSets");
if (result()) {
const base::Value::List* set_list = result()->FindList("sets");
ASSERT_TRUE(set_list);
base::Value::List expected =
base::Value::List() //
.Append(base::Value::Dict()
.Set("associatedSites",
base::Value::List().Append(kAssociatedSite))
.Set("primarySites", base::Value::List()
.Append(kPrimaryCcTLD)
.Append(kPrimarySite))
.Set("serviceSites",
base::Value::List().Append(kServiceSite)));
EXPECT_EQ(*set_list, expected);
} else if (error()) {
EXPECT_EQ(*error()->FindString("message"),
"Failed fetching RelatedWebsiteSets");
}
}
class GetAffectedUrlsForThirdPartyCookieMetadataTest
: public DevToolsProtocolTest {
protected:
tpcd::metadata::Manager* GetTpcdManager() {
return tpcd::metadata::ManagerFactory::GetForProfile(
Profile::FromBrowserContext(web_contents()->GetBrowserContext()));
}
};
IN_PROC_BROWSER_TEST_F(GetAffectedUrlsForThirdPartyCookieMetadataTest,
InvalidFirstParty) {
Attach();
base::Value::Dict params;
params.Set("firstPartyUrl", "");
params.Set("thirdPartyUrls", base::Value::List());
SendCommandSync("Storage.getAffectedUrlsForThirdPartyCookieMetadata",
std::move(params));
EXPECT_EQ(*error()->FindString("message"),
"Invalid first-party URL provided.");
}
IN_PROC_BROWSER_TEST_F(GetAffectedUrlsForThirdPartyCookieMetadataTest,
InvalidThirdParty) {
Attach();
base::Value::Dict params;
params.Set("firstPartyUrl", "https://a.test");
params.Set("thirdPartyUrls", base::Value::List().Append(""));
SendCommandSync("Storage.getAffectedUrlsForThirdPartyCookieMetadata",
std::move(params));
EXPECT_EQ(*error()->FindString("message"),
"Invalid third-party URL provided.");
}
IN_PROC_BROWSER_TEST_F(GetAffectedUrlsForThirdPartyCookieMetadataTest,
NoMatch) {
Attach();
ContentSettingsForOneType tpcd_metadata_grants;
tpcd_metadata_grants.emplace_back(
ContentSettingsPattern::FromString("*"),
ContentSettingsPattern::FromURLNoWildcard(GURL("https://a.test")),
base::Value(ContentSetting::CONTENT_SETTING_ALLOW),
content_settings::ProviderType::kNone, false);
tpcd::metadata::Manager* tpcd_metadata_manager = GetTpcdManager();
tpcd_metadata_manager->SetGrantsForTesting(tpcd_metadata_grants);
base::Value::Dict params;
params.Set("firstPartyUrl", "https://b.test");
params.Set("thirdPartyUrls", base::Value::List());
SendCommandSync("Storage.getAffectedUrlsForThirdPartyCookieMetadata",
std::move(params));
EXPECT_TRUE((*(result()->FindList("matchedUrls"))).empty());
}
IN_PROC_BROWSER_TEST_F(GetAffectedUrlsForThirdPartyCookieMetadataTest,
FirstPartyMatch) {
Attach();
const std::string first_party_url = "https://a.test";
ContentSettingsForOneType tpcd_metadata_grants;
tpcd_metadata_grants.emplace_back(
ContentSettingsPattern::FromString("*"),
ContentSettingsPattern::FromURLNoWildcard(GURL(first_party_url)),
base::Value(ContentSetting::CONTENT_SETTING_ALLOW),
content_settings::ProviderType::kNone, false);
tpcd::metadata::Manager* tpcd_metadata_manager = GetTpcdManager();
tpcd_metadata_manager->SetGrantsForTesting(tpcd_metadata_grants);
base::Value::Dict params;
params.Set("firstPartyUrl", first_party_url);
params.Set("thirdPartyUrls", base::Value::List());
SendCommandSync("Storage.getAffectedUrlsForThirdPartyCookieMetadata",
std::move(params));
EXPECT_EQ(*(result()->FindList("matchedUrls")),
base::Value::List().Append(base::Value(first_party_url)));
}
IN_PROC_BROWSER_TEST_F(GetAffectedUrlsForThirdPartyCookieMetadataTest,
ThirdPartyMatches) {
Attach();
const std::string first_party_url = "https://a.test";
const std::string third_party_url_v1 = "https://b.test";
ContentSettingsForOneType tpcd_metadata_grants;
tpcd_metadata_grants.emplace_back(
ContentSettingsPattern::FromURLNoWildcard(GURL(third_party_url_v1)),
ContentSettingsPattern::FromURLNoWildcard(GURL(first_party_url)),
base::Value(ContentSetting::CONTENT_SETTING_ALLOW),
content_settings::ProviderType::kNone, false);
const std::string third_party_url_v2 = "https://c.test";
tpcd_metadata_grants.emplace_back(
ContentSettingsPattern::FromURLNoWildcard(GURL(third_party_url_v2)),
ContentSettingsPattern::FromURLNoWildcard(GURL(first_party_url)),
base::Value(ContentSetting::CONTENT_SETTING_ALLOW),
content_settings::ProviderType::kNone, false);
tpcd::metadata::Manager* tpcd_metadata_manager = GetTpcdManager();
tpcd_metadata_manager->SetGrantsForTesting(tpcd_metadata_grants);
base::Value::Dict params;
params.Set("firstPartyUrl", first_party_url);
params.Set("thirdPartyUrls", base::Value::List()
.Append(third_party_url_v1)
.Append(third_party_url_v2)
.Append("https://d.test"));
SendCommandSync("Storage.getAffectedUrlsForThirdPartyCookieMetadata",
std::move(params));
base::Value::List expected =
base::Value::List().Append(third_party_url_v1).Append(third_party_url_v2);
EXPECT_EQ(*(result()->FindList("matchedUrls")), expected);
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
HiddenTargetIsNotVisibleInTabStrip) {
AttachToBrowserTarget();
ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
SendCommandSync("Target.getTargets");
ASSERT_EQ(1u, result()->FindList("targetInfos")->size());
base::Value::Dict params;
params.Set("url", "about:blank");
params.Set("hidden", true);
SendCommandSync("Target.createTarget", std::move(params));
// The tab strip should not contain the new tab.
EXPECT_EQ(browser()->tab_strip_model()->count(), 1);
// CDP `Target.getTargets` result should contain the new target.
SendCommandSync("Target.getTargets");
EXPECT_EQ(2u, result()->FindList("targetInfos")->size());
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
HiddenTargetClosesWhenSessionClosed) {
AttachToBrowserTarget();
ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
SendCommandSync("Target.getTargets");
ASSERT_EQ(1u, result()->FindList("targetInfos")->size());
base::Value::Dict params;
params.Set("url", "about:blank");
params.Set("hidden", true);
SendCommandSync("Target.createTarget", std::move(params));
// The tab strip should not contain the new tab.
EXPECT_EQ(browser()->tab_strip_model()->count(), 1);
// CDP `Target.getTargets` result should contain the new target.
SendCommandSync("Target.getTargets");
EXPECT_EQ(2u, result()->FindList("targetInfos")->size());
// Disconnect and connect to session.
agent_host_->DetachClient(this);
AttachToBrowserTarget();
// The hidden target should be closed.
SendCommandSync("Target.getTargets");
EXPECT_EQ(1u, result()->FindList("targetInfos")->size());
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, HiddenTargetCanBeClosed) {
AttachToBrowserTarget();
ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
SendCommandSync("Target.getTargets");
ASSERT_EQ(1u, result()->FindList("targetInfos")->size());
SendCommand("Target.setAutoAttach", base::Value::Dict()
.Set("autoAttach", true)
.Set("waitForDebuggerOnStart", false)
.Set("flatten", true));
SendCommandSync(
"Target.createTarget",
base::Value::Dict().Set("url", "about:blank").Set("hidden", true));
const std::string targetId(*result()->FindStringByDottedPath("targetId"));
// CDP `Target.getTargets` result should contain the new target.
SendCommandSync("Target.getTargets");
EXPECT_EQ(2u, result()->FindList("targetInfos")->size());
SendCommandSync("Target.closeTarget",
base::Value::Dict().Set("targetId", targetId));
WaitForNotification("Target.detachedFromTarget", true);
SendCommandSync("Target.getTargets");
EXPECT_EQ(1u, result()->FindList("targetInfos")->size());
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, HiddenTargetIsTheLastOne) {
AttachToBrowserTarget();
ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
SendCommandSync("Target.getTargets");
ASSERT_EQ(1u, result()->FindList("targetInfos")->size());
const std::string targetId(*result()
->FindList("targetInfos")
->front()
.GetDict()
.FindString("targetId"));
SendCommandSync(
"Target.createTarget",
base::Value::Dict().Set("url", "about:blank").Set("hidden", true));
SendCommandSync("Target.getTargets");
EXPECT_EQ(2u, result()->FindList("targetInfos")->size());
SendCommandSync("Target.closeTarget",
base::Value::Dict().Set("targetId", targetId));
ui_test_utils::WaitForBrowserToClose();
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
NotHiddenTargetIsVisibleInTabStrip) {
AttachToBrowserTarget();
ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
SendCommandSync("Target.getTargets");
ASSERT_EQ(1u, result()->FindList("targetInfos")->size());
base::Value::Dict params;
params.Set("url", "about:blank");
params.Set("hidden", false);
SendCommandSync("Target.createTarget", std::move(params));
// The tab strip should contain the new tab.
EXPECT_EQ(browser()->tab_strip_model()->count(), 2);
// CDP `Target.getTargets` result should contain the new target.
SendCommandSync("Target.getTargets");
EXPECT_EQ(2u, result()->FindList("targetInfos")->size());
}
#endif // !BUILDFLAG(IS_ANDROID)
class DevToolsProtocolTest_IPProtection : public DevToolsProtocolTest {
public:
DevToolsProtocolTest_IPProtection() {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{net::features::kEnableIpProtectionProxy,
{{"IpPrivacyEnableIppInDevTools", "true"}}},
{network::features::kMaskedDomainList,
{{"MaskedDomainListExperimentalVersion", "2025-05.dog.01"}}}},
{});
}
base::Value::Dict WaitForResponseNotificationWithUrl(GURL url) {
base::Value::Dict params = WaitForMatchingNotification(
"Network.responseReceived",
base::BindLambdaForTesting([url](const base::Value::Dict& params) {
return *(params.FindDict("response")->FindString("url")) ==
url.spec();
}));
return std::move(*(params.FindDict("response")));
}
protected:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->SetCertHostnames({"a.test", "b.test"});
DevToolsProtocolTest::SetUpOnMainThread();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
bool MockIppRequests(bool cached,
std::optional<std::string_view> host_to_intercept,
content::URLLoaderInterceptor::RequestParams* params) {
if (host_to_intercept.has_value() &&
host_to_intercept.value() != params->url_request.url.host_piece()) {
return false;
}
// Create the mocked URLResponseHead to represent a proxied request.
network::mojom::URLResponseHeadPtr response =
network::mojom::URLResponseHead::New();
response->mime_type = "text/html";
response->headers =
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\n\n");
CHECK_NE(response->headers.get(), nullptr);
// Mark the response as proxied. Cached responses may contain proxy_chain
// information of the original request, including if it was sent through an IP
// Protection proxy.
response->proxy_chain = net::ProxyChain::ForIpProtection({});
// Set cache state if needed.
if (cached) {
response->load_timing.request_start_time = base::Time::Now();
}
// Write the response back to the client.
mojo::ScopedDataPipeConsumerHandle consumer_handle;
mojo::ScopedDataPipeProducerHandle producer_handle;
EXPECT_EQ(mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle),
MOJO_RESULT_OK);
params->client->OnReceiveResponse(std::move(response),
std::move(consumer_handle), std::nullopt);
params->client->OnComplete(network::URLLoaderCompletionStatus());
return true;
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_IPProtection,
IsIpProtectionUsedSetInProtocol) {
// Start the test server
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
const content::URLLoaderInterceptor interceptor(
base::BindRepeating(&MockIppRequests, /*cached=*/false, "a.test"));
SendCommandSync("Network.enable");
GURL url = embedded_test_server()->GetURL("a.test", "/empty.html");
// Navigate to the test site.
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Set observer for Network.responseReceived and store the response.
base::Value::Dict response = WaitForResponseNotificationWithUrl(url);
// Then, check that "isIpProtectionUsed" is set to true within
// Network.Response.isIpProtectionUsed.
EXPECT_THAT(response.FindBool("isIpProtectionUsed"), testing::Optional(true));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_IPProtection,
CachedResponseSetsIsIpProtectionUsedToFalse) {
// Start the test server
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
const content::URLLoaderInterceptor interceptor(
base::BindRepeating(&MockIppRequests, /*cached=*/true, "a.test"));
SendCommandSync("Network.enable");
GURL url = embedded_test_server()->GetURL("a.test", "/empty.html");
// Navigate to the test site.
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Set observer for Network.responseReceived and store the response.
base::Value::Dict response = WaitForResponseNotificationWithUrl(url);
// Since this is a cached response, we expect that "isIpProtectionUsed" is
// set to false.
EXPECT_THAT(response.FindBool("isIpProtectionUsed"),
testing::Optional(false));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_IPProtection,
ProxiedThirdPartyRequestsMarkedInProtocol) {
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
SendCommandSync("Network.enable");
const content::URLLoaderInterceptor interceptor(
base::BindRepeating(&MockIppRequests, /*cached=*/false, "b.test"));
GURL first_party_url =
embedded_test_server()->GetURL("a.test", "/empty.html");
// // Navigate to the test site.
ASSERT_TRUE(content::NavigateToURL(web_contents(), first_party_url));
// Set observer for Network.responseReceived and store the response.
base::Value::Dict first_party_response =
WaitForResponseNotificationWithUrl(first_party_url);
// Ensure that the first party request is not marked as proxied.
EXPECT_THAT(first_party_response.FindBool("isIpProtectionUsed"),
testing::Optional(false));
GURL third_party_url =
embedded_test_server()->GetURL("b.test", "/empty.html");
std::string fetch_script = content::JsReplace(
"(fetch($1, { mode: 'no-cors' }).then(resp => resp.ok))",
third_party_url.spec());
EXPECT_EQ(true, content::EvalJs(web_contents(), fetch_script));
// Do the same for the third party request, but this time it should be
// marked as proxied.
base::Value::Dict third_party_response =
WaitForResponseNotificationWithUrl(third_party_url);
EXPECT_THAT(third_party_response.FindBool("isIpProtectionUsed"),
testing::Optional(true));
}
// TODO(crbug.com/440167934): add browser test for IP Proxy Status Available
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_IPProtection,
GetIpProxyStatus_MdlNotPopulated) {
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
const content::URLLoaderInterceptor interceptor(
base::BindRepeating(&MockIppRequests, /*cached=*/false, "a.test"));
SendCommandSync("Network.enable");
const base::Value::Dict* result =
SendCommandSync("Network.getIPProtectionProxyStatus");
const std::string* status_string = result->FindString("status");
ASSERT_TRUE(status_string);
// Expect MaskedDomainListNotPopulated since flags are on
EXPECT_EQ(*status_string, "MaskedDomainListNotPopulated");
}
class DevToolsProtocolTest_IPProtectionDisabled
: public DevToolsProtocolTest_IPProtection {
public:
DevToolsProtocolTest_IPProtectionDisabled() {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{net::features::kEnableIpProtectionProxy,
{{"IpPrivacyEnableIppInDevTools", "false"}}}},
{});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(
DevToolsProtocolTest_IPProtectionDisabled,
DevToolsIPPIntegrationNotEnabled_BrowserInitiatedRequest) {
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
SendCommandSync("Network.enable");
GURL url = embedded_test_server()->GetURL("a.test", "/empty.html");
// Navigate to the test site.
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Set observer for Network.responseReceived and store the response.
base::Value::Dict network_params =
WaitForNotification("Network.responseReceived", true);
base::Value::Dict* network_response = network_params.FindDict("response");
ASSERT_TRUE(network_response);
// Ensure that this request doesn't set isIpProtectionUsed.
EXPECT_THAT(network_response->FindBool("isIpProtectionUsed"), std::nullopt);
}
IN_PROC_BROWSER_TEST_F(
DevToolsProtocolTest_IPProtectionDisabled,
DevToolsIPPIntegrationNotEnabled_RendererInitiatedRequest) {
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
SendCommandSync("Network.enable");
content::URLLoaderInterceptor interceptor(
base::BindRepeating(&MockIppRequests, /*cached=*/false, "b.test"));
GURL first_party_url =
embedded_test_server()->GetURL("a.test", "/empty.html");
// Navigate to the test site.
ASSERT_TRUE(content::NavigateToURL(web_contents(), first_party_url));
// Set observer for Network.responseReceived and store the response.
base::Value::Dict first_party_response =
WaitForResponseNotificationWithUrl(first_party_url);
// Ensure that the first party request is not marked as proxied.
EXPECT_THAT(first_party_response.FindBool("isIpProtectionUsed"),
std::nullopt);
GURL third_party_url =
embedded_test_server()->GetURL("b.test", "/empty.html");
std::string fetch_script = content::JsReplace(
"(fetch($1, { mode: 'no-cors' }).then(resp => resp.ok))",
third_party_url.spec());
EXPECT_EQ(true, content::EvalJs(web_contents(), fetch_script));
// Do the same for the third party request, which should also not be set.
base::Value::Dict third_party_response =
WaitForResponseNotificationWithUrl(third_party_url);
EXPECT_THAT(third_party_response.FindBool("isIpProtectionUsed"),
std::nullopt);
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_IPProtectionDisabled,
GetIpProxyStatus_IPProtectionDisabled) {
ASSERT_TRUE(embedded_test_server()->Start());
Attach();
const content::URLLoaderInterceptor interceptor(
base::BindRepeating(&MockIppRequests, /*cached=*/false, "a.test"));
SendCommandSync("Network.enable");
const base::Value::Dict* result =
SendCommandSync("Network.getIPProtectionProxyStatus");
const std::string* status_string = result->FindString("status");
ASSERT_TRUE(status_string);
// Expect FeatureNotEnabled since IPProtectionDisabled
EXPECT_EQ(*status_string, "FeatureNotEnabled");
}
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
OpensDevTools_FailsForNonBrowserTarget) {
AttachToTabTarget(web_contents());
const base::Value::Dict* result = SendCommandSync("Target.getTargets");
const base::Value::List* list = result->FindList("targetInfos");
ASSERT_TRUE(list->size() == 1);
const std::string targetId = *list->front().GetDict().FindString("targetId");
base::Value::Dict params;
params.Set("targetId", targetId);
result = SendCommandSync("Target.openDevTools", std::move(params));
EXPECT_EQ(*error()->FindString("message"), "Not allowed");
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, OpensDevTools_OpensForPageTarget) {
AttachToBrowserTarget();
const base::Value::Dict* result = SendCommandSync("Target.getTargets");
const base::Value::List* list = result->FindList("targetInfos");
ASSERT_TRUE(list->size() == 1);
const std::string targetId = *list->front().GetDict().FindString("targetId");
base::Value::Dict params;
params.Set("targetId", targetId);
result = SendCommandSync("Target.openDevTools", std::move(params));
const std::string devtools_target_id(*result->FindString("targetId"));
// CDP `Target.getTargets` result should contain the new DevTools target.
result = SendCommandSync("Target.getTargets");
base::Value::Dict devtools_target;
for (const auto& target : *result->FindList("targetInfos")) {
if (*target.GetDict().FindString("type") == "other") {
devtools_target = target.Clone().TakeDict();
break;
}
}
EXPECT_EQ(2u, result->FindList("targetInfos")->size());
EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, OpensDevTools_OpensForTabTarget) {
AttachToBrowserTarget();
base::Value::Dict tabFilter = base::Value::Dict();
tabFilter.Set("type", "tab");
tabFilter.Set("exclude", false);
base::Value::List targetFilter =
base::Value::List().Append(std::move(tabFilter));
base::Value::Dict getTargetParams = base::Value::Dict();
getTargetParams.Set("filter", std::move(targetFilter));
const base::Value::Dict* result =
SendCommandSync("Target.getTargets", std::move(getTargetParams));
base::Value::Dict tab_target;
for (const auto& target : *result->FindList("targetInfos")) {
if (*target.GetDict().FindString("type") == "tab") {
tab_target = target.Clone().TakeDict();
break;
}
}
base::Value::Dict params;
params.Set("targetId", *tab_target.FindString("targetId"));
result = SendCommandSync("Target.openDevTools", std::move(params));
const std::string devtools_target_id(*result->FindString("targetId"));
// CDP `Target.getTargets` result should contain the new DevTools target.
result = SendCommandSync("Target.getTargets");
base::Value::Dict devtools_target;
for (const auto& target : *result->FindList("targetInfos")) {
if (*target.GetDict().FindString("type") == "other") {
devtools_target = target.Clone().TakeDict();
break;
}
}
EXPECT_EQ(2u, result->FindList("targetInfos")->size());
EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));
}
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, OpensDevTools_OpensUndocked) {
set_agent_host_can_close();
base::Value::Dict prefs;
prefs.Set("isUnderTest", "true");
prefs.Set("currentDockState", "undocked");
browser()->profile()->GetPrefs()->SetDict(prefs::kDevToolsPreferences,
std::move(prefs));
AttachToBrowserTarget();
base::Value::Dict tabFilter = base::Value::Dict();
tabFilter.Set("type", "tab");
tabFilter.Set("exclude", false);
base::Value::List targetFilter =
base::Value::List().Append(std::move(tabFilter));
base::Value::Dict getTargetParams = base::Value::Dict();
getTargetParams.Set("filter", std::move(targetFilter));
const base::Value::Dict* result =
SendCommandSync("Target.getTargets", std::move(getTargetParams));
base::Value::Dict tab_target;
for (const auto& target : *result->FindList("targetInfos")) {
if (*target.GetDict().FindString("type") == "tab") {
tab_target = target.Clone().TakeDict();
break;
}
}
base::Value::Dict params;
params.Set("targetId", *tab_target.FindString("targetId"));
result = SendCommandSync("Target.openDevTools", std::move(params));
const std::string devtools_target_id(*result->FindString("targetId"));
// CDP `Target.getTargets` result should contain the new DevTools target.
result = SendCommandSync("Target.getTargets");
base::Value::Dict devtools_target;
for (const auto& target : *result->FindList("targetInfos")) {
if (*target.GetDict().FindString("type") == "other") {
devtools_target = target.Clone().TakeDict();
break;
}
}
EXPECT_EQ(2u, result->FindList("targetInfos")->size());
EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));
}
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace