|  | // Copyright 2021 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "base/memory/weak_ptr.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/strings/strcat.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/task/sequenced_task_runner.h" | 
|  | #include "base/task/single_thread_task_runner.h" | 
|  | #include "base/test/bind.h" | 
|  | #include "base/test/scoped_feature_list.h" | 
|  | #include "base/threading/thread_restrictions.h" | 
|  | #include "components/network_session_configurator/common/network_switches.h" | 
|  | #include "content/browser/loader/navigation_early_hints_manager.h" | 
|  | #include "content/browser/renderer_host/render_frame_host_impl.h" | 
|  | #include "content/browser/web_contents/web_contents_impl.h" | 
|  | #include "content/public/test/browser_test.h" | 
|  | #include "content/public/test/content_browser_test.h" | 
|  | #include "content/public/test/content_browser_test_utils.h" | 
|  | #include "content/public/test/content_mock_cert_verifier.h" | 
|  | #include "content/public/test/fenced_frame_test_util.h" | 
|  | #include "content/public/test/prerender_test_util.h" | 
|  | #include "content/public/test/test_navigation_observer.h" | 
|  | #include "content/public/test/url_loader_interceptor.h" | 
|  | #include "content/shell/browser/shell.h" | 
|  | #include "content/test/content_browser_test_utils_internal.h" | 
|  | #include "net/base/features.h" | 
|  | #include "net/dns/mock_host_resolver.h" | 
|  | #include "net/http/http_status_code.h" | 
|  | #include "net/test/cert_test_util.h" | 
|  | #include "net/test/embedded_test_server/embedded_test_server.h" | 
|  | #include "net/test/embedded_test_server/embedded_test_server_connection_listener.h" | 
|  | #include "net/test/embedded_test_server/http_request.h" | 
|  | #include "net/test/embedded_test_server/http_response.h" | 
|  | #include "net/test/quic_simple_test_server.h" | 
|  | #include "net/test/test_data_directory.h" | 
|  | #include "services/network/public/cpp/network_switches.h" | 
|  | #include "services/network/public/mojom/early_hints.mojom.h" | 
|  | #include "services/network/public/mojom/link_header.mojom.h" | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | using PreloadedResources = NavigationEarlyHintsManager::PreloadedResources; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | struct HeaderField { | 
|  | HeaderField(const std::string& name, const std::string& value) | 
|  | : name(name), value(value) {} | 
|  |  | 
|  | std::string name; | 
|  | std::string value; | 
|  | }; | 
|  |  | 
|  | struct ResponseEntry { | 
|  | ResponseEntry(const std::string& path, net::HttpStatusCode status_code) | 
|  | : path(path) { | 
|  | headers[":path"] = path; | 
|  | headers[":status"] = base::StringPrintf("%d", status_code); | 
|  | } | 
|  |  | 
|  | void AddEarlyHints(const std::vector<HeaderField>& header_fields) { | 
|  | spdy::Http2HeaderBlock hints_headers; | 
|  | for (const auto& header : header_fields) | 
|  | hints_headers.AppendValueOrAddHeader(header.name, header.value); | 
|  | early_hints.push_back(std::move(hints_headers)); | 
|  | } | 
|  |  | 
|  | std::string path; | 
|  | spdy::Http2HeaderBlock headers; | 
|  | std::string body; | 
|  | std::vector<spdy::Http2HeaderBlock> early_hints; | 
|  | }; | 
|  |  | 
|  | const char kPageWithHintedScriptPath[] = "/page_with_hinted_js.html"; | 
|  | const char kPageWithHintedScriptBody[] = "<script src=\"/hinted.js\"></script>"; | 
|  |  | 
|  | const char kPageWithHintedCorsScriptPath[] = "/page_with_hinted_cors_js.html"; | 
|  | const char kPageWithHintedCorsScriptBody[] = | 
|  | "<script src=\"/hinted.js\" crossorigin></script>"; | 
|  |  | 
|  | const char kPageWithIframePath[] = "/page_with_iframe.html"; | 
|  | const char kPageWithIframeBody[] = | 
|  | "<iframe src=\"page_with_hinted_js.html\"></iframe>"; | 
|  |  | 
|  | const char kPageWithHintedModuleScriptPath[] = | 
|  | "/page_with_hinted_module_js.html"; | 
|  | const char kPageWithHintedModuleScriptBody[] = | 
|  | "<script src=\"/hinted.js\" type=\"module\"></script>"; | 
|  |  | 
|  | const char kHintedScriptPath[] = "/hinted.js"; | 
|  | const char kHintedScriptBody[] = "document.title = 'Done';"; | 
|  |  | 
|  | const char kHintedStylesheetPath[] = "/hinted.css"; | 
|  | const char kHintedStylesheetBody[] = "/*empty*/"; | 
|  |  | 
|  | const char kEmptyPagePath[] = "/empty.html"; | 
|  | const char kEmptyPageBody[] = "<html></html>"; | 
|  |  | 
|  | const char kRedirectedPagePath[] = "/redirected.html"; | 
|  | const char kRedirectedPageBody[] = "<script src=\"/hinted.js\"></script>"; | 
|  |  | 
|  | // Listens to sockets on an EmbeddedTestServer for preconnect tests. Created | 
|  | // on the UI thread. EmbeddedTestServerConnectionListener methods are called | 
|  | // from a different thread than the UI thread. | 
|  | class PreconnectListener | 
|  | : public net::test_server::EmbeddedTestServerConnectionListener { | 
|  | public: | 
|  | PreconnectListener() | 
|  | : task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()), | 
|  | weak_ptr_factory_(this) { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  | } | 
|  | ~PreconnectListener() override = default; | 
|  |  | 
|  | // net::test_server::EmbeddedTestServerConnectionListener implementation: | 
|  | std::unique_ptr<net::StreamSocket> AcceptedSocket( | 
|  | std::unique_ptr<net::StreamSocket> connection) override { | 
|  | task_runner_->PostTask( | 
|  | FROM_HERE, base::BindOnce(&PreconnectListener::AcceptedSocketOnUIThread, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | return connection; | 
|  | } | 
|  | void ReadFromSocket(const net::StreamSocket& connection, int rv) override {} | 
|  |  | 
|  | size_t num_accepted_sockets() { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  | return num_accepted_sockets_; | 
|  | } | 
|  |  | 
|  | private: | 
|  | void AcceptedSocketOnUIThread() { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  | ++num_accepted_sockets_; | 
|  | } | 
|  |  | 
|  | scoped_refptr<base::SequencedTaskRunner> task_runner_; | 
|  | size_t num_accepted_sockets_ = 0; | 
|  |  | 
|  | base::WeakPtrFactory<PreconnectListener> weak_ptr_factory_; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // Most tests use EmbeddedTestServer but this uses QuicSimpleTestServer because | 
|  | // Early Hints are only plumbed over HTTP/2 or HTTP/3 (QUIC). | 
|  | class NavigationEarlyHintsTest : public ContentBrowserTest { | 
|  | public: | 
|  | NavigationEarlyHintsTest() { | 
|  | feature_list_.InitWithFeatures( | 
|  | {net::features::kSplitCacheByNetworkIsolationKey}, | 
|  | {net::features::kForceIsolationInfoFrameOriginToTopLevelFrame}); | 
|  | } | 
|  | ~NavigationEarlyHintsTest() override = default; | 
|  |  | 
|  | void SetUpOnMainThread() override { | 
|  | ContentBrowserTest::SetUpOnMainThread(); | 
|  | ConfigureMockCertVerifier(); | 
|  | host_resolver()->AddRule("*", "127.0.0.1"); | 
|  |  | 
|  | cross_origin_server_.RegisterRequestHandler( | 
|  | base::BindRepeating(&NavigationEarlyHintsTest::HandleCrossOriginRequest, | 
|  | base::Unretained(this))); | 
|  | preconnect_listener_ = std::make_unique<PreconnectListener>(); | 
|  | cross_origin_server().SetConnectionListener(preconnect_listener_.get()); | 
|  | ASSERT_TRUE(cross_origin_server_.Start()); | 
|  | } | 
|  |  | 
|  | void SetUpCommandLine(base::CommandLine* command_line) override { | 
|  | command_line->AppendSwitchASCII(switches::kOriginToForceQuicOn, "*"); | 
|  | mock_cert_verifier_.SetUpCommandLine(command_line); | 
|  |  | 
|  | ASSERT_TRUE(net::QuicSimpleTestServer::Start()); | 
|  |  | 
|  | ContentBrowserTest::SetUpCommandLine(command_line); | 
|  | } | 
|  |  | 
|  | void TearDown() override { | 
|  | base::ScopedAllowBaseSyncPrimitivesForTesting allow_wait; | 
|  | net::QuicSimpleTestServer::Shutdown(); | 
|  | ContentBrowserTest::TearDown(); | 
|  | } | 
|  |  | 
|  | net::test_server::EmbeddedTestServer& cross_origin_server() { | 
|  | return cross_origin_server_; | 
|  | } | 
|  |  | 
|  | protected: | 
|  | base::test::ScopedFeatureList& feature_list() { return feature_list_; } | 
|  |  | 
|  | PreconnectListener& preconnect_listener() { return *preconnect_listener_; } | 
|  |  | 
|  | void SetUpInProcessBrowserTestFixture() override { | 
|  | mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); | 
|  | } | 
|  |  | 
|  | void TearDownInProcessBrowserTestFixture() override { | 
|  | mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); | 
|  | } | 
|  |  | 
|  | void ConfigureMockCertVerifier() { | 
|  | auto test_cert = | 
|  | net::ImportCertFromFile(net::GetTestCertsDirectory(), "quic-chain.pem"); | 
|  | net::CertVerifyResult verify_result; | 
|  | verify_result.verified_cert = test_cert; | 
|  | mock_cert_verifier_.mock_cert_verifier()->AddResultForCert( | 
|  | test_cert, verify_result, net::OK); | 
|  | mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); | 
|  | } | 
|  |  | 
|  | HeaderField CreatePreloadLinkForScript() { | 
|  | return HeaderField( | 
|  | "link", | 
|  | base::StringPrintf("<%s>; rel=preload; as=script", kHintedScriptPath)); | 
|  | } | 
|  |  | 
|  | HeaderField CreatePreloadLinkForCorsScript() { | 
|  | return HeaderField( | 
|  | "link", base::StringPrintf("<%s>; rel=preload; as=script; crossorigin", | 
|  | kHintedScriptPath)); | 
|  | } | 
|  |  | 
|  | HeaderField CreateModulePreloadLink() { | 
|  | return HeaderField("link", base::StringPrintf("<%s>; rel=modulepreload", | 
|  | kHintedScriptPath)); | 
|  | } | 
|  |  | 
|  | HeaderField CreatePreloadLinkForStylesheet() { | 
|  | return HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=style", | 
|  | kHintedStylesheetPath)); | 
|  | } | 
|  |  | 
|  | void RegisterResponse(const ResponseEntry& entry) { | 
|  | net::QuicSimpleTestServer::AddResponseWithEarlyHints( | 
|  | entry.path, entry.headers, entry.body, entry.early_hints); | 
|  | } | 
|  |  | 
|  | void RegisterHintedScriptResource() { | 
|  | ResponseEntry hinted_script_entry(kHintedScriptPath, net::HTTP_OK); | 
|  | hinted_script_entry.headers["content-type"] = "application/javascript"; | 
|  | hinted_script_entry.headers["cache-control"] = "max-age=3600"; | 
|  | hinted_script_entry.body = kHintedScriptBody; | 
|  | RegisterResponse(hinted_script_entry); | 
|  | } | 
|  |  | 
|  | void RegisterHintedStylesheetResource() { | 
|  | ResponseEntry hinted_script_entry(kHintedStylesheetPath, net::HTTP_OK); | 
|  | hinted_script_entry.headers["content-type"] = "text/css"; | 
|  | hinted_script_entry.body = kHintedStylesheetBody; | 
|  | RegisterResponse(hinted_script_entry); | 
|  | } | 
|  |  | 
|  | void RegisterRedirectedPage() { | 
|  | ResponseEntry entry(kRedirectedPagePath, net::HTTP_OK); | 
|  | entry.body = kRedirectedPageBody; | 
|  | RegisterResponse(entry); | 
|  | } | 
|  |  | 
|  | ResponseEntry CreatePageEntryWithHintedScript( | 
|  | net::HttpStatusCode status_code) { | 
|  | RegisterHintedScriptResource(); | 
|  |  | 
|  | ResponseEntry entry(kPageWithHintedScriptPath, status_code); | 
|  | entry.body = kPageWithHintedScriptBody; | 
|  | HeaderField link_header = CreatePreloadLinkForScript(); | 
|  | entry.AddEarlyHints({std::move(link_header)}); | 
|  |  | 
|  | return entry; | 
|  | } | 
|  |  | 
|  | ResponseEntry CreateEmptyPageEntryWithHintedScript() { | 
|  | RegisterHintedScriptResource(); | 
|  |  | 
|  | ResponseEntry entry(kEmptyPagePath, net::HTTP_OK); | 
|  | entry.body = kEmptyPageBody; | 
|  | HeaderField link_header = CreatePreloadLinkForScript(); | 
|  | entry.AddEarlyHints({std::move(link_header)}); | 
|  |  | 
|  | return entry; | 
|  | } | 
|  |  | 
|  | ResponseEntry CreatePageEntryWithHintedCorsScript( | 
|  | net::HttpStatusCode status_code) { | 
|  | RegisterHintedScriptResource(); | 
|  |  | 
|  | ResponseEntry entry(kPageWithHintedCorsScriptPath, status_code); | 
|  | entry.body = kPageWithHintedCorsScriptBody; | 
|  | HeaderField link_header = CreatePreloadLinkForCorsScript(); | 
|  | entry.AddEarlyHints({std::move(link_header)}); | 
|  |  | 
|  | return entry; | 
|  | } | 
|  |  | 
|  | ResponseEntry CreatePageEntryWithHintedModuleScript( | 
|  | net::HttpStatusCode status_code) { | 
|  | RegisterHintedScriptResource(); | 
|  |  | 
|  | ResponseEntry entry(kPageWithHintedModuleScriptPath, status_code); | 
|  | entry.body = kPageWithHintedModuleScriptBody; | 
|  | HeaderField link_header = CreateModulePreloadLink(); | 
|  | entry.AddEarlyHints({std::move(link_header)}); | 
|  |  | 
|  | return entry; | 
|  | } | 
|  |  | 
|  | bool NavigateToURLAndWaitTitle(const GURL& url, const std::string& title) { | 
|  | return NavigateToURLAndWaitTitleWithCommitURL(url, url, title); | 
|  | } | 
|  |  | 
|  | bool NavigateToURLAndWaitTitleWithCommitURL(const GURL& url, | 
|  | const GURL& expected_commit_url, | 
|  | const std::string& title) { | 
|  | std::u16string title16 = base::ASCIIToUTF16(title); | 
|  | TitleWatcher title_watcher(shell()->web_contents(), title16); | 
|  | if (!NavigateToURL(shell(), url, expected_commit_url)) | 
|  | return false; | 
|  | return title16 == title_watcher.WaitAndGetTitle(); | 
|  | } | 
|  |  | 
|  | NavigationEarlyHintsManager* GetEarlyHintsManager(RenderFrameHostImpl* rfh) { | 
|  | return rfh->early_hints_manager(); | 
|  | } | 
|  |  | 
|  | PreloadedResources WaitForPreloadedResources() { | 
|  | return WaitForPreloadedResources(static_cast<RenderFrameHostImpl*>( | 
|  | shell()->web_contents()->GetPrimaryMainFrame())); | 
|  | } | 
|  |  | 
|  | PreloadedResources WaitForPreloadedResources(RenderFrameHostImpl* rfh) { | 
|  | base::RunLoop loop; | 
|  | PreloadedResources result; | 
|  | if (!GetEarlyHintsManager(rfh)) | 
|  | return result; | 
|  |  | 
|  | GetEarlyHintsManager(rfh)->WaitForPreloadsFinishedForTesting( | 
|  | base::BindLambdaForTesting([&](PreloadedResources preloaded_resources) { | 
|  | result = preloaded_resources; | 
|  | loop.Quit(); | 
|  | })); | 
|  | loop.Run(); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | enum class FetchResult { | 
|  | kFetched, | 
|  | kBlocked, | 
|  | }; | 
|  | FetchResult FetchScriptOnDocument(ToRenderFrameHost target, GURL src) { | 
|  | EvalJsResult result = EvalJs(target, JsReplace(R"( | 
|  | new Promise(resolve => { | 
|  | const script = document.createElement("script"); | 
|  | script.src = $1; | 
|  | script.onerror = () => resolve("blocked"); | 
|  | script.onload = () => resolve("fetched"); | 
|  | document.body.appendChild(script); | 
|  | }); | 
|  | )", | 
|  | src)); | 
|  | return result.ExtractString() == "fetched" ? FetchResult::kFetched | 
|  | : FetchResult::kBlocked; | 
|  | } | 
|  |  | 
|  | private: | 
|  | std::unique_ptr<net::test_server::HttpResponse> HandleCrossOriginRequest( | 
|  | const net::test_server::HttpRequest& request) { | 
|  | GURL relative_url = request.base_url.Resolve(request.relative_url); | 
|  |  | 
|  | if (relative_url.path() == kEmptyPagePath) { | 
|  | auto response = std::make_unique<net::test_server::BasicHttpResponse>(); | 
|  | response->set_code(net::HTTP_OK); | 
|  | response->set_content_type("text/html"); | 
|  | response->set_content(""); | 
|  | return std::move(response); | 
|  | } | 
|  |  | 
|  | if (relative_url.path() == kRedirectedPagePath) { | 
|  | auto response = std::make_unique<net::test_server::BasicHttpResponse>(); | 
|  | response->set_code(net::HTTP_OK); | 
|  | response->set_content_type("text/html"); | 
|  | response->set_content(kRedirectedPageBody); | 
|  | return std::move(response); | 
|  | } | 
|  |  | 
|  | if (relative_url.path() != kHintedScriptPath) | 
|  | return nullptr; | 
|  |  | 
|  | auto response = std::make_unique<net::test_server::BasicHttpResponse>(); | 
|  | response->set_code(net::HTTP_OK); | 
|  | response->set_content_type("application/javascript"); | 
|  | response->set_content(kHintedScriptBody); | 
|  |  | 
|  | std::string query = relative_url.query(); | 
|  | if (query == "corp-cross-origin") { | 
|  | response->AddCustomHeader("Cross-Origin-Resource-Policy", "cross-origin"); | 
|  | } else if (query == "corp-same-origin") { | 
|  | response->AddCustomHeader("Cross-Origin-Resource-Policy", "same-origin"); | 
|  | } | 
|  |  | 
|  | return std::move(response); | 
|  | } | 
|  |  | 
|  | base::test::ScopedFeatureList feature_list_; | 
|  |  | 
|  | ContentMockCertVerifier mock_cert_verifier_; | 
|  |  | 
|  | // For tests that fetch resources from a cross origin server. | 
|  | net::EmbeddedTestServer cross_origin_server_; | 
|  | std::unique_ptr<PreconnectListener> preconnect_listener_; | 
|  | }; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, Basic) { | 
|  | ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURLAndWaitTitle( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath), | 
|  | "Done")); | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | auto it = preloads.find(preloaded_url); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::OK); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CorsAttribute) { | 
|  | ResponseEntry entry = CreatePageEntryWithHintedCorsScript(net::HTTP_OK); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURLAndWaitTitle( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedCorsScriptPath), | 
|  | "Done")); | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | auto it = preloads.find(preloaded_url); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::OK); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, ModulePreload) { | 
|  | ResponseEntry entry = CreatePageEntryWithHintedModuleScript(net::HTTP_OK); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURLAndWaitTitle( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedModuleScriptPath), | 
|  | "Done")); | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | auto it = preloads.find(preloaded_url); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::OK); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, DisallowPreloadFromIframe) { | 
|  | ResponseEntry page_entry(kPageWithIframePath, net::HTTP_OK); | 
|  | page_entry.body = kPageWithIframeBody; | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | ResponseEntry iframe_entry = CreatePageEntryWithHintedScript(net::HTTP_OK); | 
|  | RegisterResponse(iframe_entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithIframePath))); | 
|  |  | 
|  | // Find RenderFrameHost for the iframe. | 
|  | std::vector<RenderFrameHost*> all_frames = | 
|  | CollectAllRenderFrameHosts(shell()->web_contents()); | 
|  | ASSERT_EQ(all_frames.size(), 2UL); | 
|  | ASSERT_EQ(all_frames[0], all_frames[1]->GetParent()); | 
|  | RenderFrameHostImpl* iframe_host = | 
|  | static_cast<RenderFrameHostImpl*>(all_frames[1]); | 
|  |  | 
|  | EXPECT_TRUE(WaitForLoadStop(WebContents::FromRenderFrameHost(iframe_host))); | 
|  | ASSERT_EQ(iframe_host->GetLastCommittedURL(), | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath)); | 
|  |  | 
|  | // NavigationEarlyHintsManager should not be created for subframes. If it were | 
|  | // created it should have been created before navigation commit. | 
|  | EXPECT_EQ(iframe_host->early_hints_manager(), nullptr); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NavigationServerError) { | 
|  | ResponseEntry entry = | 
|  | CreatePageEntryWithHintedScript(net::HTTP_INTERNAL_SERVER_ERROR); | 
|  | entry.body = "Internal Server Error"; | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL( | 
|  | kPageWithHintedScriptPath))); | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | auto it = preloads.find(preloaded_url); | 
|  | ASSERT_NE(it, preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::OK); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, RedirectSameOrigin) { | 
|  | RegisterRedirectedPage(); | 
|  |  | 
|  | ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND); | 
|  | entry.headers["location"] = kRedirectedPagePath; | 
|  | entry.body = ""; | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURLAndWaitTitleWithCommitURL( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath), | 
|  | net::QuicSimpleTestServer::GetFileURL(kRedirectedPagePath), "Done")); | 
|  |  | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | auto it = preloads.find(preloaded_url); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::OK); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, RedirectCrossOrigin) { | 
|  | const GURL kRedirectedUrl = cross_origin_server().GetURL(kRedirectedPagePath); | 
|  |  | 
|  | ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND); | 
|  | entry.headers["location"] = kRedirectedUrl.spec(); | 
|  | entry.body = ""; | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURLAndWaitTitleWithCommitURL( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath), | 
|  | kRedirectedUrl, "Done")); | 
|  |  | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_TRUE(preloads.empty()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidPreloadLink) { | 
|  | const std::string kPath = "/hinted.html"; | 
|  |  | 
|  | RegisterHintedScriptResource(); | 
|  |  | 
|  | ResponseEntry entry(kPath, net::HTTP_OK); | 
|  | entry.body = "body"; | 
|  | entry.AddEarlyHints( | 
|  | {HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=invalid", | 
|  | kHintedScriptPath))}); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE( | 
|  | NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath))); | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_TRUE(preloads.empty()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, MultipleEarlyHints) { | 
|  | RegisterHintedScriptResource(); | 
|  | RegisterHintedStylesheetResource(); | 
|  |  | 
|  | ResponseEntry entry(kPageWithHintedScriptPath, net::HTTP_OK); | 
|  | entry.body = kPageWithHintedScriptBody; | 
|  |  | 
|  | // Set two Early Hints responses which contain duplicate preload link headers. | 
|  | // The second response should be ignored. | 
|  | HeaderField script_link_header = CreatePreloadLinkForScript(); | 
|  | HeaderField stylesheet_link_header = CreatePreloadLinkForStylesheet(); | 
|  | entry.AddEarlyHints({script_link_header}); | 
|  | entry.AddEarlyHints({script_link_header, stylesheet_link_header}); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURLAndWaitTitle( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath), | 
|  | "Done")); | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | GURL stylesheet_url = | 
|  | net::QuicSimpleTestServer::GetFileURL(kHintedStylesheetPath); | 
|  | EXPECT_TRUE(preloads.contains(script_url)); | 
|  | EXPECT_FALSE(preloads.contains(stylesheet_url)); | 
|  | } | 
|  |  | 
|  | const char kPageWithCrossOriginScriptPage[] = | 
|  | "/page_with_cross_origin_script.html"; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CORP_Pass) { | 
|  | // The server response's is a script with | 
|  | // `Cross-Origin-Resource-Policy: cross-origin`. | 
|  | const GURL kCrossOriginScriptUrl = | 
|  | cross_origin_server().GetURL("/hinted.js?corp-cross-origin"); | 
|  |  | 
|  | ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK); | 
|  | HeaderField link_header = HeaderField( | 
|  | "link", base::StringPrintf("<%s>; rel=preload; as=script", | 
|  | kCrossOriginScriptUrl.spec().c_str())); | 
|  | page_entry.AddEarlyHints({std::move(link_header)}); | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL( | 
|  | kPageWithCrossOriginScriptPage))); | 
|  | EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); | 
|  |  | 
|  | EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl), | 
|  | FetchResult::kFetched); | 
|  |  | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | auto it = preloads.find(kCrossOriginScriptUrl); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::OK); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CORP_Blocked) { | 
|  | // The server response's is a script with | 
|  | // `Cross-Origin-Resource-Policy: same-origin`. | 
|  | const GURL kCrossOriginScriptUrl = | 
|  | cross_origin_server().GetURL("/hinted.js?corp-same-origin"); | 
|  |  | 
|  | ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK); | 
|  | HeaderField link_header = HeaderField( | 
|  | "link", base::StringPrintf("<%s>; rel=preload; as=script", | 
|  | kCrossOriginScriptUrl.spec().c_str())); | 
|  | page_entry.AddEarlyHints({std::move(link_header)}); | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL( | 
|  | kPageWithCrossOriginScriptPage))); | 
|  | EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); | 
|  |  | 
|  | // The script fetch should be blocked. | 
|  | EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl), | 
|  | FetchResult::kBlocked); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, COEP_Pass) { | 
|  | // The server sends `Cross-Origin-Resource-Policy: cross-origin`. | 
|  | const GURL kCrossOriginScriptUrl = | 
|  | cross_origin_server().GetURL("/hinted.js?corp-cross-origin"); | 
|  | ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK); | 
|  | page_entry.headers["cross-origin-embedder-policy"] = "require-corp"; | 
|  | HeaderField link_header = HeaderField( | 
|  | "link", base::StringPrintf("<%s>; rel=preload; as=script", | 
|  | kCrossOriginScriptUrl.spec().c_str())); | 
|  | HeaderField coep = | 
|  | HeaderField("cross-origin-embedder-policy", "require-corp"); | 
|  | page_entry.AddEarlyHints({std::move(link_header), std::move(coep)}); | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL( | 
|  | kPageWithCrossOriginScriptPage))); | 
|  | EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); | 
|  |  | 
|  | EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl), | 
|  | FetchResult::kFetched); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, COEP_Block) { | 
|  | // The server does not send `Cross-Origin-Resource-Policy` header. | 
|  | const GURL kCrossOriginScriptUrl = cross_origin_server().GetURL("/hinted.js"); | 
|  | ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK); | 
|  | page_entry.headers["cross-origin-embedder-policy"] = "require-corp"; | 
|  | HeaderField link_header = HeaderField( | 
|  | "link", base::StringPrintf("<%s>; rel=preload; as=script", | 
|  | kCrossOriginScriptUrl.spec().c_str())); | 
|  | HeaderField coep = | 
|  | HeaderField("cross-origin-embedder-policy", "require-corp"); | 
|  | page_entry.AddEarlyHints({std::move(link_header), std::move(coep)}); | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL( | 
|  | kPageWithCrossOriginScriptPage))); | 
|  | EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); | 
|  |  | 
|  | EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl), | 
|  | FetchResult::kBlocked); | 
|  | } | 
|  |  | 
|  | // Test that network isolation key is set correctly for Early Hints preload. | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NetworkAnonymizationKey) { | 
|  | const GURL kHintedScriptUrl = | 
|  | net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  |  | 
|  | ResponseEntry entry = CreateEmptyPageEntryWithHintedScript(); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | absl::optional<bool> is_cached; | 
|  | URLLoaderInterceptor interceptor( | 
|  | base::BindLambdaForTesting( | 
|  | [&](URLLoaderInterceptor::RequestParams* params) { return false; }), | 
|  | base::BindLambdaForTesting( | 
|  | [&](const GURL& request_url, | 
|  | const network::URLLoaderCompletionStatus& status) { | 
|  | if (request_url != kHintedScriptUrl) | 
|  | return; | 
|  | is_cached = status.exists_in_cache; | 
|  | }), | 
|  | base::NullCallback()); | 
|  |  | 
|  | ASSERT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL(kEmptyPagePath))); | 
|  |  | 
|  | // Make sure the hinted resource is preloaded. | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | auto it = preloads.find(kHintedScriptUrl); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_EQ(it->second.error_code.value(), net::OK); | 
|  |  | 
|  | ASSERT_FALSE(is_cached.value()); | 
|  | is_cached = absl::nullopt; | 
|  |  | 
|  | // Fetch the hinted resource from the main frame. It should come from the | 
|  | // cache. | 
|  | FetchScriptOnDocument(shell(), kHintedScriptUrl); | 
|  | ASSERT_TRUE(is_cached.value()); | 
|  |  | 
|  | // Reset `is_cached` to make sure it is set true or false. | 
|  | is_cached = absl::nullopt; | 
|  |  | 
|  | // Create an iframe with a different origin and fetch the hinted resource from | 
|  | // the iframe. It should not come from the cache. | 
|  | auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); | 
|  | RenderFrameHost* iframe = | 
|  | CreateSubframe(web_contents, /*frame_id=*/"", | 
|  | cross_origin_server().GetURL("/empty.html"), | 
|  | /*wait_for_navigation=*/true); | 
|  | FetchScriptOnDocument(iframe, kHintedScriptUrl); | 
|  | ASSERT_FALSE(is_cached.value()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, SimplePreconnect) { | 
|  | const char kPageWithPreconnect[] = "/page_with_preconnect.html"; | 
|  | const GURL kPreconnectUrl = cross_origin_server().GetURL("/"); | 
|  | ResponseEntry page_entry(kPageWithPreconnect, net::HTTP_OK); | 
|  | HeaderField link_header = | 
|  | HeaderField("link", base::StringPrintf("<%s>; rel=preconnect", | 
|  | kPreconnectUrl.spec().c_str())); | 
|  | page_entry.AddEarlyHints({std::move(link_header)}); | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | ASSERT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithPreconnect))); | 
|  | ASSERT_TRUE(WaitForLoadStop(shell()->web_contents())); | 
|  |  | 
|  | EXPECT_EQ(preconnect_listener().num_accepted_sockets(), 1UL); | 
|  | EXPECT_TRUE( | 
|  | GetEarlyHintsManager(static_cast<RenderFrameHostImpl*>( | 
|  | shell()->web_contents()->GetPrimaryMainFrame())) | 
|  | ->WasResourceHintsReceived()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidHeader_NewLine) { | 
|  | const std::string kPath = "/header-contains-newline.html"; | 
|  | ResponseEntry entry(kPath, net::HTTP_OK); | 
|  | entry.AddEarlyHints({HeaderField("invalid-header", "foo\r\nbar")}); | 
|  | RegisterResponse(entry); | 
|  | EXPECT_FALSE( | 
|  | NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath))); | 
|  | } | 
|  |  | 
|  | class NavigationEarlyHintsAddressSpaceTest : public NavigationEarlyHintsTest { | 
|  | public: | 
|  | NavigationEarlyHintsAddressSpaceTest() = default; | 
|  | ~NavigationEarlyHintsAddressSpaceTest() override = default; | 
|  |  | 
|  | void SetUpCommandLine(base::CommandLine* command_line) override { | 
|  | NavigationEarlyHintsTest::SetUpCommandLine(command_line); | 
|  |  | 
|  | private_server_.AddDefaultHandlers(GetTestDataFilePath()); | 
|  | ASSERT_TRUE(private_server_.Start()); | 
|  |  | 
|  | // Treat the main test server as public for IPAddressSpace tests. | 
|  | command_line->AppendSwitchASCII( | 
|  | network::switches::kIpAddressSpaceOverrides, | 
|  | base::StringPrintf("127.0.0.1:%d=public", | 
|  | net::QuicSimpleTestServer::GetPort())); | 
|  | } | 
|  |  | 
|  | net::test_server::EmbeddedTestServer& private_server() { | 
|  | return private_server_; | 
|  | } | 
|  |  | 
|  | private: | 
|  | // For tests that trigger private network requests. | 
|  | net::EmbeddedTestServer private_server_; | 
|  | }; | 
|  |  | 
|  | // Tests that Early Hints preload is blocked when hints comes from the public | 
|  | // network but a hinted resource is located in a private network. | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsAddressSpaceTest, | 
|  | PublicToPrivateRequestBlocked) { | 
|  | const GURL kPrivateResourceUrl = private_server().GetURL("/blank.jpg"); | 
|  | ResponseEntry page_entry(kEmptyPagePath, net::HTTP_OK); | 
|  | HeaderField link_header = HeaderField( | 
|  | "link", base::StringPrintf("<%s>; rel=preload; as=image", | 
|  | kPrivateResourceUrl.spec().c_str())); | 
|  | page_entry.AddEarlyHints({std::move(link_header)}); | 
|  | RegisterResponse(page_entry); | 
|  |  | 
|  | EXPECT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL(kEmptyPagePath))); | 
|  | EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); | 
|  |  | 
|  | PreloadedResources preloads = WaitForPreloadedResources(); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | auto it = preloads.find(kPrivateResourceUrl); | 
|  | ASSERT_TRUE(it != preloads.end()); | 
|  | ASSERT_FALSE(it->second.was_canceled); | 
|  | ASSERT_TRUE(it->second.error_code.has_value()); | 
|  | EXPECT_EQ(it->second.error_code.value(), net::ERR_FAILED); | 
|  | EXPECT_EQ(it->second.cors_error_status->cors_error, | 
|  | network::mojom::CorsError::kInsecurePrivateNetwork); | 
|  | } | 
|  |  | 
|  | class NavigationEarlyHintsPrerenderTest : public NavigationEarlyHintsTest { | 
|  | public: | 
|  | NavigationEarlyHintsPrerenderTest() | 
|  | : prerender_helper_(base::BindRepeating( | 
|  | &NavigationEarlyHintsPrerenderTest::web_contents, | 
|  | base::Unretained(this))) {} | 
|  | ~NavigationEarlyHintsPrerenderTest() override = default; | 
|  |  | 
|  | test::PrerenderTestHelper* prerender_helper() { return &prerender_helper_; } | 
|  |  | 
|  | WebContents* web_contents() { return shell()->web_contents(); } | 
|  |  | 
|  | private: | 
|  | test::PrerenderTestHelper prerender_helper_; | 
|  | }; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsPrerenderTest, | 
|  | AllowPreloadInPrerendering) { | 
|  | EXPECT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html"))); | 
|  | ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | // Loads a page in the prerender. | 
|  | int host_id = prerender_helper()->AddPrerender( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath)); | 
|  | RenderFrameHostImpl* prerender_rfh = static_cast<RenderFrameHostImpl*>( | 
|  | prerender_helper()->GetPrerenderedMainFrameHost(host_id)); | 
|  | EXPECT_NE(prerender_rfh, nullptr); | 
|  | EXPECT_NE(prerender_rfh->early_hints_manager(), nullptr); | 
|  |  | 
|  | PreloadedResources preloads = WaitForPreloadedResources(prerender_rfh); | 
|  | EXPECT_EQ(preloads.size(), 1UL); | 
|  |  | 
|  | GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath); | 
|  | EXPECT_TRUE(preloads.contains(script_url)); | 
|  | } | 
|  |  | 
|  | class NavigationEarlyHintsFencedFrameTest : public NavigationEarlyHintsTest { | 
|  | public: | 
|  | NavigationEarlyHintsFencedFrameTest() = default; | 
|  |  | 
|  | test::FencedFrameTestHelper& fenced_frame_test_helper() { | 
|  | return fenced_frame_test_helper_; | 
|  | } | 
|  |  | 
|  | ResponseEntry CreatePageEntryWithHintedScriptInFencedFrame( | 
|  | net::HttpStatusCode status_code) { | 
|  | RegisterHintedScriptResource(); | 
|  |  | 
|  | ResponseEntry entry(kPageWithHintedScriptPath, status_code); | 
|  | entry.headers["supports-loading-mode"] = "fenced-frame"; | 
|  | entry.body = kPageWithHintedScriptBody; | 
|  | HeaderField link_header = CreatePreloadLinkForScript(); | 
|  | HeaderField fenced_frame_header = | 
|  | HeaderField("supports-loading-mode", "fenced-frame"); | 
|  | entry.AddEarlyHints( | 
|  | {std::move(link_header), std::move(fenced_frame_header)}); | 
|  | return entry; | 
|  | } | 
|  |  | 
|  | private: | 
|  | test::FencedFrameTestHelper fenced_frame_test_helper_; | 
|  | }; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsFencedFrameTest, | 
|  | DisallowPreloadInFencedFrame) { | 
|  | EXPECT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html"))); | 
|  |  | 
|  | ResponseEntry entry = | 
|  | CreatePageEntryWithHintedScriptInFencedFrame(net::HTTP_OK); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | // Create a fenced frame. | 
|  | RenderFrameHostImpl* fenced_frame_host = static_cast<RenderFrameHostImpl*>( | 
|  | fenced_frame_test_helper().CreateFencedFrame( | 
|  | shell()->web_contents()->GetPrimaryMainFrame(), | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath))); | 
|  | EXPECT_NE(fenced_frame_host, nullptr); | 
|  | EXPECT_EQ(fenced_frame_host->early_hints_manager(), nullptr); | 
|  | } | 
|  |  | 
|  | class NavigationEarlyHintsPortalTest : public NavigationEarlyHintsTest { | 
|  | public: | 
|  | NavigationEarlyHintsPortalTest() { | 
|  | scoped_feature_list_.InitWithFeatures( | 
|  | /*enabled_features=*/{blink::features::kPortals, | 
|  | blink::features::kPortalsCrossOrigin}, | 
|  | /*disabled_features=*/{}); | 
|  | } | 
|  |  | 
|  | private: | 
|  | base::test::ScopedFeatureList scoped_feature_list_; | 
|  | }; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsPortalTest, | 
|  | DisallowPreloadInPortal) { | 
|  | EXPECT_TRUE(NavigateToURL( | 
|  | shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html"))); | 
|  |  | 
|  | ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK); | 
|  | RegisterResponse(entry); | 
|  |  | 
|  | GURL portal_url( | 
|  | net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath)); | 
|  | WebContentsAddedObserver contents_observer; | 
|  | TestNavigationObserver portal_nav_observer(portal_url); | 
|  | portal_nav_observer.StartWatchingNewWebContents(); | 
|  |  | 
|  | // Create a portal. | 
|  | EXPECT_TRUE( | 
|  | ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), | 
|  | JsReplace("{" | 
|  | "  let portal = document.createElement('portal');" | 
|  | "  portal.src = $1;" | 
|  | "  document.body.appendChild(portal);" | 
|  | "}", | 
|  | portal_url), | 
|  | EXECUTE_SCRIPT_NO_USER_GESTURE)); | 
|  |  | 
|  | WebContents* portal_web_contents = contents_observer.GetWebContents(); | 
|  | EXPECT_NE(portal_web_contents, nullptr); | 
|  | portal_nav_observer.WaitForNavigationFinished(); | 
|  |  | 
|  | EXPECT_EQ(static_cast<RenderFrameHostImpl*>( | 
|  | portal_web_contents->GetPrimaryMainFrame()) | 
|  | ->early_hints_manager(), | 
|  | nullptr); | 
|  | } | 
|  |  | 
|  | }  // namespace content |