| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/predictors/loading_predictor.h" |
| |
| #include <array> |
| #include <map> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/thread_pool/thread_pool_instance.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_features.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/navigation_predictor/navigation_predictor_features.h" |
| #include "chrome/browser/navigation_predictor/navigation_predictor_preconnect_client.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h" |
| #include "chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer.h" |
| #include "chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.h" |
| #include "chrome/browser/predictors/loading_predictor_factory.h" |
| #include "chrome/browser/predictors/loading_test_util.h" |
| #include "chrome/browser/predictors/predictors_enums.h" |
| #include "chrome/browser/predictors/predictors_features.h" |
| #include "chrome/browser/predictors/predictors_switches.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/no_state_prefetch/browser/no_state_prefetch_handle.h" |
| #include "components/no_state_prefetch/browser/no_state_prefetch_manager.h" |
| #include "components/optimization_guide/core/optimization_guide_features.h" |
| #include "components/optimization_guide/proto/hints.pb.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/page_navigator.h" |
| #include "content/public/browser/preconnect_manager.h" |
| #include "content/public/browser/preconnect_request.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/referrer.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/fenced_frame_test_util.h" |
| #include "content/public/test/preconnect_test_util.h" |
| #include "content/public/test/prerender_test_util.h" |
| #include "content/public/test/simple_url_loader_test_helper.h" |
| #include "content/public/test/test_frame_navigation_observer.h" |
| #include "net/base/features.h" |
| #include "net/base/network_anonymization_key.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/connection_tracker.h" |
| #include "net/test/embedded_test_server/controllable_http_response.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/embedded_test_server/request_handler_util.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/cors/cors_error_status.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/network_quality_tracker.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/clear_data_filter.mojom.h" |
| #include "services/network/public/mojom/cors.mojom.h" |
| #include "services/network/public/mojom/ip_address_space.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/navigation/preloading_headers.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using content::BrowserThread; |
| using content::PreconnectRequest; |
| using testing::Optional; |
| using testing::SizeIs; |
| |
| namespace predictors { |
| |
| const char kChromiumUrl[] = "http://chromium.org"; |
| |
| const char kHtmlSubresourcesPath[] = "/predictors/html_subresources.html"; |
| // The embedded test server runs on test.com. |
| // kHtmlSubresourcesPath contains high priority resources from baz.com and |
| // foo.com. kHtmlSubresourcesPath also contains a low priority resource from |
| // bar.com. |
| const char* const kHtmlSubresourcesHosts[] = {"test.com", "baz.com", "foo.com"}; |
| |
| std::string GetPathWithPortReplacement(const std::string& path, uint16_t port) { |
| std::string string_port = base::StringPrintf("%d", port); |
| return net::test_server::GetFilePathWithReplacements( |
| path, {{"REPLACE_WITH_PORT", string_port}}); |
| } |
| |
| GURL GetDataURLWithContent(const std::string& content) { |
| std::string encoded_content = base::Base64Encode(content); |
| std::string data_uri_content = "data:text/html;base64," + encoded_content; |
| return GURL(data_uri_content); |
| } |
| |
| // Helper class to track and allow waiting for ResourcePrefetchPredictor |
| // initialization. WARNING: OnPredictorInitialized event will not be fired if |
| // ResourcePrefetchPredictor is initialized before the observer creation. |
| class PredictorInitializer : public TestObserver { |
| public: |
| explicit PredictorInitializer(ResourcePrefetchPredictor* predictor) |
| : TestObserver(predictor), predictor_(predictor) {} |
| |
| PredictorInitializer(const PredictorInitializer&) = delete; |
| PredictorInitializer& operator=(const PredictorInitializer&) = delete; |
| |
| void EnsurePredictorInitialized() { |
| if (predictor_->initialization_state_ == |
| ResourcePrefetchPredictor::INITIALIZED) { |
| return; |
| } |
| |
| if (predictor_->initialization_state_ == |
| ResourcePrefetchPredictor::NOT_INITIALIZED) { |
| predictor_->StartInitialization(); |
| } |
| |
| run_loop_.Run(); |
| } |
| |
| void OnPredictorInitialized() override { run_loop_.Quit(); } |
| |
| private: |
| raw_ptr<ResourcePrefetchPredictor> predictor_ = nullptr; |
| base::RunLoop run_loop_; |
| }; |
| |
| class LcpElementLearnWaiter : public TestObserver { |
| public: |
| explicit LcpElementLearnWaiter(ResourcePrefetchPredictor* predictor) |
| : TestObserver(predictor) {} |
| void Wait() { run_loop_.Run(); } |
| |
| private: |
| void OnLcppLearned() override { run_loop_.Quit(); } |
| base::RunLoop run_loop_; |
| }; |
| |
| class LcpUpdatedWaiter : public TestObserver { |
| public: |
| explicit LcpUpdatedWaiter(ResourcePrefetchPredictor* predictor, |
| size_t wait_lcp_count) |
| : TestObserver(predictor), wait_lcp_count_(wait_lcp_count) {} |
| const std::vector<std::optional<std::string>>& Wait() { |
| run_loop_.Run(); |
| return element_locators_; |
| } |
| |
| private: |
| void OnLcpUpdated( |
| const std::optional<std::string>& element_locator) override { |
| element_locators_.push_back(element_locator); |
| if (++lcp_count_ >= wait_lcp_count_) { |
| run_loop_.Quit(); |
| } |
| } |
| base::RunLoop run_loop_; |
| const size_t wait_lcp_count_; |
| size_t lcp_count_ = 0u; |
| std::vector<std::optional<std::string>> element_locators_; |
| }; |
| |
| class LcpTimingPredictedWaiter : public TestObserver { |
| public: |
| explicit LcpTimingPredictedWaiter(ResourcePrefetchPredictor* predictor) |
| : TestObserver(predictor) {} |
| std::optional<std::string>& Wait() { |
| run_loop_.Run(); |
| return lcp_element_locator_; |
| } |
| |
| private: |
| void OnLcpTimingPredicted( |
| const std::optional<std::string>& lcp_element_locator) override { |
| lcp_element_locator_ = lcp_element_locator; |
| run_loop_.Quit(); |
| } |
| base::RunLoop run_loop_; |
| std::optional<std::string> lcp_element_locator_; |
| }; |
| |
| class TestPreconnectManagerObserver |
| : public content::PreconnectManager::Observer { |
| public: |
| explicit TestPreconnectManagerObserver( |
| content::PreconnectManager* preconnect_manager) { |
| preconnect_manager->SetObserverForTesting(this); |
| } |
| |
| void OnPreconnectUrl(const GURL& url, |
| int num_sockets, |
| bool allow_credentials) override { |
| preconnect_url_attempts_.insert(url.DeprecatedGetOriginAsURL()); |
| preconnect_url_attempts_history_.push_back(url.DeprecatedGetOriginAsURL()); |
| } |
| |
| void OnPreresolveFinished( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient>& |
| observer, |
| bool success) override { |
| ResolveHostRequestInfo preconnect_info{url.host(), |
| network_anonymization_key}; |
| if (success) { |
| successful_dns_lookups_.insert(preconnect_info); |
| } else { |
| unsuccessful_dns_lookups_.insert(preconnect_info); |
| } |
| CheckForWaitingLoop(); |
| } |
| |
| void OnProxyLookupFinished( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| bool success) override { |
| ResolveProxyRequestInfo resolve_info{url::Origin::Create(url), |
| network_anonymization_key}; |
| if (success) { |
| successful_proxy_lookups_.insert(resolve_info); |
| } else { |
| unsuccessful_proxy_lookups_.insert(resolve_info); |
| } |
| CheckForWaitingLoop(); |
| } |
| |
| void WaitUntilHostLookedUp( |
| const std::string& host, |
| const net::NetworkAnonymizationKey& network_anonymization_key) { |
| wait_event_ = WaitEvent::kDns; |
| DCHECK(waiting_on_dns_.IsEmpty()); |
| waiting_on_dns_ = ResolveHostRequestInfo{host, network_anonymization_key}; |
| Wait(); |
| } |
| |
| void WaitUntilProxyLookedUp( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key) { |
| wait_event_ = WaitEvent::kProxy; |
| DCHECK(waiting_on_proxy_.IsEmpty()); |
| waiting_on_proxy_ = ResolveProxyRequestInfo{url::Origin::Create(url), |
| network_anonymization_key}; |
| Wait(); |
| } |
| |
| bool HasOriginAttemptedToPreconnect(const GURL& origin) { |
| DCHECK_EQ(origin, origin.DeprecatedGetOriginAsURL()); |
| return base::Contains(preconnect_url_attempts_, origin); |
| } |
| |
| bool HasHostBeenLookedUp( |
| const std::string& host, |
| const net::NetworkAnonymizationKey& network_anonymization_key) { |
| ResolveHostRequestInfo preconnect_info{host, network_anonymization_key}; |
| return base::Contains(successful_dns_lookups_, preconnect_info) || |
| base::Contains(unsuccessful_dns_lookups_, preconnect_info); |
| } |
| |
| bool HostFound( |
| const std::string& host, |
| const net::NetworkAnonymizationKey& network_anonymization_key) { |
| return base::Contains( |
| successful_dns_lookups_, |
| ResolveHostRequestInfo{host, network_anonymization_key}); |
| } |
| |
| bool ProxyFound( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key) { |
| return base::Contains(successful_proxy_lookups_, |
| ResolveProxyRequestInfo{url::Origin::Create(url), |
| network_anonymization_key}); |
| } |
| |
| const std::vector<GURL>& PreconnectUrlAttemptsHistory() const { |
| return preconnect_url_attempts_history_; |
| } |
| |
| private: |
| enum class WaitEvent { kNone, kDns, kProxy }; |
| |
| struct ResolveHostRequestInfo { |
| bool operator<(const ResolveHostRequestInfo& other) const { |
| return std::tie(hostname, network_anonymization_key) < |
| std::tie(other.hostname, other.network_anonymization_key); |
| } |
| |
| bool operator==(const ResolveHostRequestInfo& other) const { |
| return std::tie(hostname, network_anonymization_key) == |
| std::tie(other.hostname, other.network_anonymization_key); |
| } |
| |
| bool IsEmpty() const { |
| return hostname.empty() && network_anonymization_key.IsEmpty(); |
| } |
| |
| std::string hostname; |
| net::NetworkAnonymizationKey network_anonymization_key; |
| }; |
| |
| struct ResolveProxyRequestInfo { |
| bool operator<(const ResolveProxyRequestInfo& other) const { |
| return std::tie(origin, network_anonymization_key) < |
| std::tie(other.origin, other.network_anonymization_key); |
| } |
| |
| bool operator==(const ResolveProxyRequestInfo& other) const { |
| return std::tie(origin, network_anonymization_key) == |
| std::tie(other.origin, other.network_anonymization_key); |
| } |
| |
| bool IsEmpty() const { |
| return origin.opaque() && network_anonymization_key.IsEmpty(); |
| } |
| |
| url::Origin origin; |
| net::NetworkAnonymizationKey network_anonymization_key; |
| }; |
| |
| bool HasProxyBeenLookedUp(const ResolveProxyRequestInfo& resolve_proxy_info) { |
| return base::Contains(successful_proxy_lookups_, resolve_proxy_info) || |
| base::Contains(unsuccessful_proxy_lookups_, resolve_proxy_info); |
| } |
| |
| void Wait() { |
| base::RunLoop run_loop; |
| DCHECK(!run_loop_); |
| run_loop_ = &run_loop; |
| CheckForWaitingLoop(); |
| run_loop.Run(); |
| } |
| |
| void CheckForWaitingLoop() { |
| switch (wait_event_) { |
| case WaitEvent::kNone: |
| return; |
| case WaitEvent::kDns: |
| if (!HasHostBeenLookedUp(waiting_on_dns_.hostname, |
| waiting_on_dns_.network_anonymization_key)) { |
| return; |
| } |
| waiting_on_dns_ = ResolveHostRequestInfo(); |
| break; |
| case WaitEvent::kProxy: |
| if (!HasProxyBeenLookedUp(waiting_on_proxy_)) { |
| return; |
| } |
| waiting_on_proxy_ = ResolveProxyRequestInfo(); |
| break; |
| } |
| DCHECK(run_loop_); |
| run_loop_->Quit(); |
| run_loop_ = nullptr; |
| wait_event_ = WaitEvent::kNone; |
| } |
| |
| WaitEvent wait_event_ = WaitEvent::kNone; |
| raw_ptr<base::RunLoop> run_loop_ = nullptr; |
| |
| ResolveHostRequestInfo waiting_on_dns_; |
| std::set<ResolveHostRequestInfo> successful_dns_lookups_; |
| std::set<ResolveHostRequestInfo> unsuccessful_dns_lookups_; |
| |
| ResolveProxyRequestInfo waiting_on_proxy_; |
| std::set<ResolveProxyRequestInfo> successful_proxy_lookups_; |
| std::set<ResolveProxyRequestInfo> unsuccessful_proxy_lookups_; |
| std::set<GURL> preconnect_url_attempts_; |
| std::vector<GURL> preconnect_url_attempts_history_; |
| }; |
| |
| struct PrefetchResult { |
| PrefetchResult(const GURL& prefetch_url, |
| const network::URLLoaderCompletionStatus& status) |
| : prefetch_url(prefetch_url), status(status) {} |
| |
| GURL prefetch_url; |
| network::URLLoaderCompletionStatus status; |
| }; |
| |
| class TestPrefetchManagerObserver : public PrefetchManager::Observer { |
| public: |
| explicit TestPrefetchManagerObserver(PrefetchManager& manager) { |
| manager.set_observer_for_testing(this); |
| } |
| |
| void OnPrefetchFinished( |
| const GURL& url, |
| const GURL& prefetch_url, |
| const network::URLLoaderCompletionStatus& status) override { |
| prefetches_.emplace_back(prefetch_url, status); |
| } |
| |
| void OnAllPrefetchesFinished(const GURL& url) override { |
| done_urls_.insert(url); |
| if (waiting_url_ == url) { |
| waiting_url_ = GURL(); |
| std::move(done_callback_).Run(); |
| } |
| } |
| |
| void WaitForPrefetchesForNavigation(const GURL& url) { |
| DCHECK(waiting_url_.is_empty()); |
| DCHECK(!url.is_empty()); |
| if (done_urls_.find(url) != done_urls_.end()) { |
| return; |
| } |
| waiting_url_ = url; |
| base::RunLoop loop; |
| done_callback_ = loop.QuitClosure(); |
| loop.Run(); |
| } |
| |
| const std::vector<PrefetchResult>& results() const { return prefetches_; } |
| |
| private: |
| std::vector<PrefetchResult> prefetches_; |
| std::set<GURL> done_urls_; |
| GURL waiting_url_; |
| base::OnceClosure done_callback_; |
| }; |
| |
| class LoadingPredictorBrowserTest : public InProcessBrowserTest { |
| public: |
| LoadingPredictorBrowserTest() { |
| scoped_feature_list_.InitWithFeatures( |
| {features::kLoadingOnlyLearnHighPriorityResources, |
| features::kLoadingPreconnectToRedirectTarget, |
| features::kNavigationPredictorPreconnectHoldback}, |
| // TODO(crbug.com/40248833): Use HTTPS URLs in tests to avoid having to |
| // disable this feature. |
| {features::kHttpsUpgrades, |
| // TODO(crbug.com/354087603): Update tests when this feature has |
| // positive (or neutral) effect of loading performance. |
| features::kLoadingPredictorLimitPreconnectSocketCount}); |
| } |
| |
| LoadingPredictorBrowserTest(const LoadingPredictorBrowserTest&) = delete; |
| LoadingPredictorBrowserTest& operator=(const LoadingPredictorBrowserTest&) = |
| delete; |
| |
| ~LoadingPredictorBrowserTest() override = default; |
| |
| void SetUp() override { |
| ASSERT_TRUE(embedded_test_server()->InitializeAndListen()); |
| embedded_test_server()->RegisterRequestHandler(base::BindRepeating( |
| &LoadingPredictorBrowserTest::HandleFaviconRequest)); |
| embedded_test_server()->RegisterRequestHandler(base::BindRepeating( |
| &LoadingPredictorBrowserTest::HandleCacheRedirectRequest)); |
| |
| ASSERT_TRUE(preconnecting_test_server_.InitializeAndListen()); |
| preconnecting_test_server_.RegisterRequestHandler(base::BindRepeating( |
| &LoadingPredictorBrowserTest::HandleFaviconRequest)); |
| preconnecting_test_server_.AddDefaultHandlers(GetChromeTestDataDir()); |
| |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| connection_tracker_ = std::make_unique<net::test_server::ConnectionTracker>( |
| embedded_test_server()); |
| preconnecting_server_connection_tracker_ = |
| std::make_unique<net::test_server::ConnectionTracker>( |
| &preconnecting_test_server_); |
| |
| embedded_test_server()->StartAcceptingConnections(); |
| |
| EXPECT_TRUE(preconnecting_test_server_.Started()); |
| preconnecting_test_server_.StartAcceptingConnections(); |
| |
| loading_predictor_ = |
| LoadingPredictorFactory::GetForProfile(browser()->profile()); |
| ASSERT_TRUE(loading_predictor_); |
| preconnect_manager_observer_ = |
| std::make_unique<TestPreconnectManagerObserver>( |
| loading_predictor_->preconnect_manager()); |
| if (base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch) || |
| base::FeatureList::IsEnabled( |
| blink::features::kLCPPPrefetchSubresource)) { |
| prefetch_manager_observer_ = |
| std::make_unique<TestPrefetchManagerObserver>( |
| *loading_predictor_->prefetch_manager()); |
| } |
| PredictorInitializer initializer( |
| loading_predictor_->resource_prefetch_predictor()); |
| initializer.EnsurePredictorInitialized(); |
| } |
| |
| void TearDownOnMainThread() override { |
| ResetPredictorState(); |
| loading_predictor_ = nullptr; |
| } |
| |
| // Navigates to an URL without blocking until the navigation finishes. |
| // Returns an observer that can be used to wait for the navigation |
| // completion. |
| // This function creates a new tab for each navigation that allows multiple |
| // simultaneous navigations and avoids triggering the reload behavior. |
| std::unique_ptr<content::TestNavigationManager> NavigateToURLAsync( |
| const GURL& url) { |
| chrome::NewTab(browser()); |
| content::WebContents* tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| DCHECK(tab); |
| auto observer = std::make_unique<content::TestNavigationManager>(tab, url); |
| tab->GetController().LoadURL(url, content::Referrer(), |
| ui::PAGE_TRANSITION_TYPED, std::string()); |
| return observer; |
| } |
| |
| void ResetNetworkState() { |
| auto* network_context = |
| browser()->profile()->GetDefaultStoragePartition()->GetNetworkContext(); |
| base::RunLoop clear_host_cache_loop; |
| base::RunLoop close_all_connections_loop; |
| network_context->ClearHostCache(nullptr, |
| clear_host_cache_loop.QuitClosure()); |
| network_context->CloseAllConnections( |
| close_all_connections_loop.QuitClosure()); |
| clear_host_cache_loop.Run(); |
| close_all_connections_loop.Run(); |
| |
| connection_tracker()->ResetCounts(); |
| preconnecting_server_connection_tracker_->ResetCounts(); |
| } |
| |
| void ResetPredictorState() { |
| loading_predictor_->resource_prefetch_predictor()->DeleteAllUrls(); |
| } |
| |
| std::unique_ptr<PreconnectPrediction> GetPreconnectPrediction( |
| const GURL& url) { |
| auto prediction = std::make_unique<PreconnectPrediction>(); |
| bool has_prediction = loading_predictor_->resource_prefetch_predictor() |
| ->PredictPreconnectOrigins(url, prediction.get()); |
| if (!has_prediction) { |
| return nullptr; |
| } |
| return prediction; |
| } |
| |
| LoadingPredictor* loading_predictor() { return loading_predictor_; } |
| |
| TestPreconnectManagerObserver* preconnect_manager_observer() { |
| return preconnect_manager_observer_.get(); |
| } |
| |
| TestPrefetchManagerObserver* prefetch_manager_observer() { |
| return prefetch_manager_observer_.get(); |
| } |
| |
| net::test_server::ConnectionTracker* connection_tracker() { |
| return connection_tracker_.get(); |
| } |
| |
| net::test_server::ConnectionTracker* |
| preconnecting_server_connection_tracker() { |
| return preconnecting_server_connection_tracker_.get(); |
| } |
| |
| static std::unique_ptr<net::test_server::HttpResponse> HandleFaviconRequest( |
| const net::test_server::HttpRequest& request) { |
| if (request.relative_url != "/favicon.ico") { |
| return nullptr; |
| } |
| |
| auto http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| http_response->AddCustomHeader("Cache-Control", "max-age=6000"); |
| return http_response; |
| } |
| |
| static std::unique_ptr<net::test_server::HttpResponse> |
| HandleCacheRedirectRequest(const net::test_server::HttpRequest& request) { |
| if (!base::StartsWith(request.relative_url, "/cached-redirect?", |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return nullptr; |
| } |
| |
| GURL request_url = request.GetURL(); |
| std::string dest = |
| base::UnescapeBinaryURLComponent(request_url.query_piece()); |
| |
| auto http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_MOVED_PERMANENTLY); |
| http_response->AddCustomHeader("Location", dest); |
| http_response->set_content_type("text/html"); |
| http_response->set_content(base::StringPrintf( |
| "<html><head></head><body>Redirecting to %s</body></html>", |
| dest.c_str())); |
| http_response->AddCustomHeader("Cache-Control", "max-age=6000"); |
| return http_response; |
| } |
| |
| protected: |
| // Test server that initiates preconnect. Separate server from the one being |
| // preconnected to separate preconnected connection count. |
| net::EmbeddedTestServer preconnecting_test_server_; |
| |
| private: |
| raw_ptr<LoadingPredictor> loading_predictor_ = nullptr; |
| std::unique_ptr<net::test_server::ConnectionTracker> connection_tracker_; |
| std::unique_ptr<net::test_server::ConnectionTracker> |
| preconnecting_server_connection_tracker_; |
| std::unique_ptr<TestPreconnectManagerObserver> preconnect_manager_observer_; |
| std::unique_ptr<TestPrefetchManagerObserver> prefetch_manager_observer_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that a navigation triggers the LoadingPredictor. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, SimpleNavigation) { |
| GURL url = embedded_test_server()->GetURL("/nocontent"); |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| EXPECT_EQ(1u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| // Checking GetActiveHintsSizeForTesting() is racy since the active hint |
| // is removed after the preconnect finishes. Instead check for total |
| // hints activated. |
| EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| EXPECT_GE(2u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting()); |
| EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| EXPECT_GE(2u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| } |
| |
| // Tests that two concurrenct navigations are recorded correctly by the |
| // predictor. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, TwoConcurrentNavigations) { |
| GURL url1 = embedded_test_server()->GetURL("/echo-raw?1"); |
| GURL url2 = embedded_test_server()->GetURL("/echo-raw?2"); |
| auto observer1 = NavigateToURLAsync(url1); |
| auto observer2 = NavigateToURLAsync(url2); |
| EXPECT_TRUE(observer1->WaitForRequestStart()); |
| EXPECT_TRUE(observer2->WaitForRequestStart()); |
| EXPECT_EQ(2u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| // Checking GetActiveHintsSizeForTesting() is racy since the active hint |
| // is removed after the preconnect finishes. Instead check for total |
| // hints activated. |
| EXPECT_LE(2u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| ASSERT_TRUE(observer1->WaitForNavigationFinished()); |
| ASSERT_TRUE(observer2->WaitForNavigationFinished()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting()); |
| EXPECT_LE(2u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| } |
| |
| // Tests that two navigations to the same URL are deduplicated. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, |
| TwoNavigationsToTheSameURL) { |
| GURL url = embedded_test_server()->GetURL("/nocontent"); |
| auto observer1 = NavigateToURLAsync(url); |
| auto observer2 = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer1->WaitForRequestStart()); |
| EXPECT_TRUE(observer2->WaitForRequestStart()); |
| EXPECT_EQ(2u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| // Checking GetActiveHintsSizeForTesting() is racy since the active hint |
| // is removed after the preconnect finishes. Instead check for total |
| // hints activated. The total hints activated may be only 1 if the second |
| // navigation arrives before the first preconnect finishes. However, if the |
| // second navigation arrives later, then two hints may get activated. |
| EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| ASSERT_TRUE(observer1->WaitForNavigationFinished()); |
| ASSERT_TRUE(observer2->WaitForNavigationFinished()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting()); |
| EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| } |
| |
| // Tests that the LoadingPredictor doesn't record non-http(s) navigations. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, NonHttpNavigation) { |
| std::string content = "<body>Hello world!</body>"; |
| GURL url = GetDataURLWithContent(content); |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting()); |
| EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting()); |
| } |
| |
| // Tests that the LoadingPredictor doesn't preconnect to non-http(s) urls. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, |
| PrepareForPageLoadNonHttpScheme) { |
| std::string content = "<body>Hello world!</body>"; |
| GURL url = GetDataURLWithContent(content); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey ::CreateSameSite(site); |
| // Ensure that no backgound task would make a host lookup or attempt to |
| // preconnect. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| url.host(), network_anonymization_key)); |
| EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| "", network_anonymization_key)); |
| EXPECT_FALSE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| url.DeprecatedGetOriginAsURL())); |
| EXPECT_FALSE( |
| preconnect_manager_observer()->HasOriginAttemptedToPreconnect(GURL())); |
| } |
| |
| namespace { |
| class TestPrerenderStopObserver |
| : public prerender::NoStatePrefetchHandle::Observer { |
| public: |
| explicit TestPrerenderStopObserver(base::OnceClosure on_stop_closure) |
| : on_stop_closure_(std::move(on_stop_closure)) {} |
| ~TestPrerenderStopObserver() override = default; |
| |
| void OnPrefetchStop(prerender::NoStatePrefetchHandle* contents) override { |
| if (on_stop_closure_) { |
| std::move(on_stop_closure_).Run(); |
| } |
| } |
| |
| private: |
| base::OnceClosure on_stop_closure_; |
| }; |
| } // namespace |
| |
| // Tests that the LoadingPredictor preconnects to the main frame origin even if |
| // it doesn't have any prediction for this origin. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, |
| PrepareForPageLoadWithoutPrediction) { |
| // Navigate the first time to fill the HTTP cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey ::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| ResetPredictorState(); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| url.host(), network_anonymization_key)); |
| // We should preconnect only 2 sockets for the main frame host. |
| const size_t expected_connections = 2; |
| connection_tracker()->WaitForAcceptedConnections(expected_connections); |
| EXPECT_EQ(expected_connections, |
| connection_tracker()->GetAcceptedSocketCount()); |
| // No reads since all resources should be cached. |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Tests that the LoadingPredictor has a prediction for a host after navigating |
| // to it. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, LearnFromNavigation) { |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| std::vector<PreconnectRequest> requests; |
| for (auto* const host : kHtmlSubresourcesHosts) { |
| requests.emplace_back( |
| url::Origin::Create(embedded_test_server()->GetURL(host, "/")), 1, |
| net::NetworkAnonymizationKey::CreateSameSite(site)); |
| } |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| auto prediction = GetPreconnectPrediction(url); |
| ASSERT_TRUE(prediction); |
| EXPECT_EQ(prediction->is_redirected, false); |
| EXPECT_EQ(prediction->host, url.host()); |
| EXPECT_THAT(prediction->requests, |
| testing::UnorderedElementsAreArray(requests)); |
| } |
| |
| class LoadingPredictorBrowserTestLearnAllResources |
| : public LoadingPredictorBrowserTest { |
| public: |
| LoadingPredictorBrowserTestLearnAllResources() { |
| feature_list_.InitAndDisableFeature( |
| features::kLoadingOnlyLearnHighPriorityResources); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that the LoadingPredictor has a prediction for a host after navigating |
| // to it. Disables kLoadingOnlyLearnHighPriorityResources. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestLearnAllResources, |
| LearnFromNavigation) { |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| std::vector<PreconnectRequest> requests; |
| for (auto* const host : kHtmlSubresourcesHosts) { |
| requests.emplace_back( |
| url::Origin::Create(embedded_test_server()->GetURL(host, "/")), 1, |
| net::NetworkAnonymizationKey::CreateSameSite(site)); |
| } |
| |
| // When kLoadingOnlyLearnHighPriorityResources is disabled, loading data |
| // collector should learn the loading of low priority resources hosted on |
| // bar.com as well. |
| requests.emplace_back( |
| url::Origin::Create(embedded_test_server()->GetURL("bar.com", "/")), 1, |
| net::NetworkAnonymizationKey::CreateSameSite(site)); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| auto prediction = GetPreconnectPrediction(url); |
| ASSERT_TRUE(prediction); |
| EXPECT_EQ(prediction->is_redirected, false); |
| EXPECT_EQ(prediction->host, url.host()); |
| EXPECT_THAT(prediction->requests, |
| testing::UnorderedElementsAreArray(requests)); |
| } |
| |
| // Tests that the LoadingPredictor correctly learns from navigations with |
| // redirect. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, |
| LearnFromNavigationWithRedirect) { |
| GURL redirect_url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| GURL original_url = embedded_test_server()->GetURL( |
| "redirect.com", |
| base::StringPrintf("/server-redirect?%s", redirect_url.spec().c_str())); |
| url::Origin origin = url::Origin::Create(redirect_url); |
| net::SchemefulSite site = net::SchemefulSite(origin); |
| std::vector<PreconnectRequest> expected_requests; |
| for (auto* const host : kHtmlSubresourcesHosts) { |
| expected_requests.emplace_back( |
| url::Origin::Create(embedded_test_server()->GetURL(host, "/")), 1, |
| net::NetworkAnonymizationKey::CreateSameSite(site)); |
| } |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), original_url)); |
| // The predictor can correctly predict hosts by |redirect_url| |
| auto prediction = GetPreconnectPrediction(redirect_url); |
| ASSERT_TRUE(prediction); |
| EXPECT_EQ(prediction->is_redirected, false); |
| EXPECT_EQ(prediction->host, redirect_url.host()); |
| EXPECT_THAT(prediction->requests, |
| testing::UnorderedElementsAreArray(expected_requests)); |
| // The predictor needs minimum two redirect hits to be confident in the |
| // redirect to generate the origins for subresources. However, after the |
| // first redirect, the predictor should learn the redirect origin for |
| // preconnect. |
| prediction = GetPreconnectPrediction(original_url); |
| ASSERT_TRUE(prediction); |
| EXPECT_FALSE(prediction->is_redirected); |
| EXPECT_EQ(prediction->host, original_url.host()); |
| std::vector<PreconnectRequest> expected_requests_1; |
| url::Origin redirect_origin = url::Origin::Create( |
| embedded_test_server()->GetURL(redirect_url.host(), "/")); |
| expected_requests_1.emplace_back( |
| redirect_origin, 1, net::NetworkAnonymizationKey::CreateSameSite(site)); |
| EXPECT_THAT(prediction->requests, |
| testing::UnorderedElementsAreArray(expected_requests_1)); |
| |
| // The predictor will start predict for origins of subresources (based on |
| // redirect) after the second navigation. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), original_url)); |
| prediction = GetPreconnectPrediction(original_url); |
| expected_requests.emplace_back( |
| redirect_origin, 1, net::NetworkAnonymizationKey::CreateSameSite(site)); |
| ASSERT_TRUE(prediction); |
| EXPECT_EQ(prediction->is_redirected, true); |
| EXPECT_EQ(prediction->host, redirect_url.host()); |
| EXPECT_THAT(prediction->requests, |
| testing::UnorderedElementsAreArray(expected_requests)); |
| } |
| |
| // Tests that the LoadingPredictor performs preresolving/preconnecting for a |
| // navigation which it has a prediction for. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, |
| PrepareForPageLoadWithPrediction) { |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| for (auto* const host : kHtmlSubresourcesHosts) { |
| GURL host_url(base::StringPrintf("http://%s", host)); |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host_url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| host_url.host(), network_anonymization_key)); |
| } |
| // 2 connections to the main frame host + 1 connection per host for others. |
| const size_t expected_connections = std::size(kHtmlSubresourcesHosts) + 1; |
| connection_tracker()->WaitForAcceptedConnections(expected_connections); |
| EXPECT_EQ(expected_connections, |
| connection_tracker()->GetAcceptedSocketCount()); |
| // No reads since all resources should be cached. |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Tests that a host requested by <link rel="dns-prefetch"> is looked up. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, DnsPrefetch) { |
| GURL url = embedded_test_server()->GetURL("/predictor/dns_prefetch.html"); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| GURL(kChromiumUrl).host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| GURL(kChromiumUrl).host(), network_anonymization_key)); |
| } |
| |
| // Tests that preconnect warms up a socket connection to a test server. |
| // Note: This test uses a data URI to serve the preconnect hint, to make sure |
| // that the network stack doesn't just re-use its connection to the test server. |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, PreconnectNonCors) { |
| GURL preconnect_url = embedded_test_server()->base_url(); |
| std::string preconnect_content = |
| "<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">"; |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), GetDataURLWithContent(preconnect_content))); |
| connection_tracker()->WaitForAcceptedConnections(1u); |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| class LCPPBrowserTestBase : public LoadingPredictorBrowserTest { |
| public: |
| void NavigateAndWaitForLcpElement( |
| const GURL& url, |
| const base::Location& from_here = FROM_HERE) { |
| LcpElementLearnWaiter lcp_element_waiter( |
| loading_predictor()->resource_prefetch_predictor()); |
| page_load_metrics::PageLoadMetricsTestWaiter waiter( |
| browser()->tab_strip_model()->GetActiveWebContents()); |
| waiter.AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter:: |
| TimingField::kLargestContentfulPaint); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)) |
| << from_here.ToString(); |
| waiter.Wait(); |
| // Navigate to about:blank to force recording a LCP element. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank"))) |
| << from_here.ToString(); |
| lcp_element_waiter.Wait(); |
| } |
| }; |
| |
| // TODO(crbug.com/40063266): isolate test per feature. Currently, it has |
| // test for script observer and fonts. |
| class LCPCriticalPathPredictorBrowserTest : public LCPPBrowserTestBase { |
| public: |
| LCPCriticalPathPredictorBrowserTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{blink::features::kLCPCriticalPathPredictor, {}}, |
| {blink::features::kLCPPFontURLPredictor, |
| {{blink::features::kLCPPFontURLPredictorExcludedHosts.name, |
| "exclude.test,exclude2.test"}}}}, |
| {}); |
| } |
| |
| std::vector<std::string> ExpectLcpElementLocatorsPrediction( |
| const base::Location& from_here, |
| const GURL& url, |
| size_t expected_locator_count) { |
| auto lcpp_stat = |
| loading_predictor()->resource_prefetch_predictor()->GetLcppStat( |
| /*initiator_origin=*/std::nullopt, url); |
| std::vector<std::string> locators; |
| if (lcpp_stat) { |
| std::optional<blink::mojom::LCPCriticalPathPredictorNavigationTimeHint> |
| hint = ConvertLcppStatToLCPCriticalPathPredictorNavigationTimeHint( |
| *lcpp_stat); |
| if (hint) { |
| locators = hint->lcp_element_locators; |
| } |
| } |
| EXPECT_EQ(expected_locator_count, locators.size()) << from_here.ToString(); |
| return locators; |
| } |
| |
| |
| std::vector<std::string> GetLCPPFonts(const GURL& url) { |
| auto lcpp_stat = |
| loading_predictor()->resource_prefetch_predictor()->GetLcppStat( |
| /*initiator_origin=*/std::nullopt, url); |
| if (!lcpp_stat) { |
| return std::vector<std::string>(); |
| } |
| std::vector<std::string> fonts; |
| for (const auto& it : lcpp_stat->fetched_font_url_stat().main_buckets()) { |
| fonts.push_back(it.first); |
| } |
| return fonts; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that the LoadingPredictor has a LCP critical path predictor |
| // (LCPP) prediction after navigating to it. |
| // LCPP: |
| // https://docs.google.com/document/d/18qTNRyv_9K2CtvVrl_ancLzPxiAnfAcbvrCNegU9IBM |
| // LCP: https://web.dev/lcp/ |
| IN_PROC_BROWSER_TEST_F(LCPCriticalPathPredictorBrowserTest, |
| LearnLCPPFromNavigation) { |
| const GURL kUrlA = |
| embedded_test_server()->GetURL("p.com", "/predictors/load_image_a.html"); |
| const GURL kUrlB = |
| embedded_test_server()->GetURL("p.com", "/predictors/load_image_b.html"); |
| const GURL kUrlC = |
| embedded_test_server()->GetURL("q.com", "/predictors/load_image_a.html"); |
| |
| // There is no knowledge in the beginning. |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA, |
| /*expected_locator_count=*/0); |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB, |
| /*expected_locator_count=*/0); |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC, |
| /*expected_locator_count=*/0); |
| |
| NavigateAndWaitForLcpElement(kUrlA); |
| // The locators should contain [lcp_element_for_a]. |
| std::vector<std::string> locators_1 = |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA, |
| /*expected_locator_count=*/1); |
| std::vector<std::string> locators_2 = |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB, |
| /*expected_locator_count=*/1); |
| EXPECT_EQ(locators_1, locators_2); |
| // The locator is encoded in a binary form. So storing the locator for a LCP |
| // node for kUrlA to use later validation. |
| const std::string& locator_for_a = locators_2[0]; |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC, |
| /*expected_locator_count=*/0); |
| |
| NavigateAndWaitForLcpElement(kUrlB); |
| // The locators should contain [lcp_element_for_a, lcp_element_for_b]. |
| std::vector<std::string> locators_3 = |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA, |
| /*expected_locator_count=*/2); |
| std::vector<std::string> locators_4 = |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB, |
| /*expected_locator_count=*/2); |
| EXPECT_EQ(locators_3, locators_4); |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC, |
| /*expected_locator_count=*/0); |
| |
| NavigateAndWaitForLcpElement(kUrlB); |
| std::vector<std::string> locators_5 = |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA, |
| /*expected_locator_count=*/2); |
| std::vector<std::string> locators_6 = |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB, |
| /*expected_locator_count=*/2); |
| // The locators should contain [lcp_element_for_b, lcp_element_for_a]. |
| // lcp_element_for_b must come first because we navigated to kUrlB twice. |
| EXPECT_EQ(locators_5, locators_6); |
| ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC, |
| /*expected_locator_count=*/0); |
| EXPECT_EQ(locator_for_a, locators_6[1]); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LCPCriticalPathPredictorBrowserTest, LearnLCPPFont) { |
| const GURL kUrlA = |
| embedded_test_server()->GetURL("p.com", "/predictors/lcpp_font.html"); |
| const GURL kFontUrlA = |
| embedded_test_server()->GetURL("p.com", "/predictors/font.ttf"); |
| const GURL kUrlB = embedded_test_server()->GetURL( |
| "exclude.test", "/predictors/lcpp_font.html"); |
| const GURL kUrlC = embedded_test_server()->GetURL( |
| "exclude2.test", "/predictors/lcpp_font.html"); |
| |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlA)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB)); |
| |
| std::vector<std::string> expected; |
| expected.push_back(kFontUrlA.spec()); |
| NavigateAndWaitForLcpElement(kUrlA); |
| EXPECT_EQ(expected, GetLCPPFonts(kUrlA)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlC)); |
| |
| NavigateAndWaitForLcpElement(kUrlB); |
| EXPECT_EQ(expected, GetLCPPFonts(kUrlA)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlC)); |
| |
| NavigateAndWaitForLcpElement(kUrlC); |
| EXPECT_EQ(expected, GetLCPPFonts(kUrlA)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB)); |
| EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlC)); |
| } |
| |
| class LCPPPrefetchSubresourceTest : public LCPPBrowserTestBase { |
| public: |
| LCPPPrefetchSubresourceTest() { |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/{blink::features::kLCPPPrefetchSubresource}, |
| /*disabled_features=*/{features::kLoadingPredictorPrefetch, |
| blink::features::kHttpDiskCachePrewarming}); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| LCPPBrowserTestBase::SetUpCommandLine(command_line); |
| command_line->AppendSwitch( |
| switches::kLoadingPredictorAllowLocalRequestForTesting); |
| } |
| |
| GURL GetURL(const std::string& path) { |
| return embedded_test_server()->GetURL("a.test", path); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that prefetch uses LCP-learning subresources. |
| IN_PROC_BROWSER_TEST_F(LCPPPrefetchSubresourceTest, Base) { |
| CHECK( |
| base::FeatureList::IsEnabled(blink::features::kLCPPPrefetchSubresource)); |
| CHECK(!base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch)); |
| CHECK( |
| !base::FeatureList::IsEnabled(blink::features::kHttpDiskCachePrewarming)); |
| const GURL url = GetURL("/predictors/preload.html"); |
| NavigateAndWaitForLcpElement(url); |
| EXPECT_TRUE(prefetch_manager_observer()->results().empty()); |
| auto lcpp_stat = |
| loading_predictor()->resource_prefetch_predictor()->GetLcppStat( |
| /*initiator_origin=*/std::nullopt, url); |
| EXPECT_TRUE(lcpp_stat.has_value()); |
| const auto& subresource_urls = PredictFetchedSubresourceUrls(*lcpp_stat); |
| // Check LCP has learnt the subresources. |
| EXPECT_EQ(std::set<GURL>({GetURL("/predictors/red_rectangle.png"), |
| GetURL("/predictors/style.css"), |
| GetURL("/predictors/script.js"), |
| GetURL("/predictors/font.ttf")}), |
| std::set<GURL>(subresource_urls.begin(), subresource_urls.end())); |
| |
| EXPECT_TRUE(prefetch_manager_observer()->results().empty()); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| prefetch_manager_observer()->WaitForPrefetchesForNavigation(url); |
| const auto results = prefetch_manager_observer()->results(); |
| std::set<GURL> result_urls; |
| for (const auto& result : results) { |
| EXPECT_EQ(net::OK, result.status.error_code) << result.prefetch_url; |
| result_urls.insert(result.prefetch_url); |
| } |
| // font.tts prefetch is handled by kLCPPFontURLPredictor feature. |
| EXPECT_EQ(std::set<GURL>({GetURL("/predictors/style.css"), |
| GetURL("/predictors/script.js")}), |
| result_urls); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LCPPPrefetchSubresourceTest, UMA) { |
| NavigateAndWaitForLcpElement(GetURL("/predictors/preload.html")); |
| |
| base::HistogramTester histogram_tester; |
| NavigateAndWaitForLcpElement(GetURL("/predictors/preload2.html")); |
| // predicted subresources: |
| // style.css |
| // script.js |
| // font.ttf |
| // red_rectangle.png |
| // actual subresources: |
| // style.css |
| // script.js |
| // red_rectangle.png |
| // red_rectangle2.png |
| // red_rectangle3.png |
| // hit = 3 |
| // precision = 3/4 = 75% |
| // recall = 3/5 = 60% |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples( |
| ::internal::kHistogramLCPPSubresourceCountType), |
| base::BucketsAre( |
| base::Bucket(network::mojom::RequestDestination::kImage, 3), |
| base::Bucket(network::mojom::RequestDestination::kStyle, 1), |
| base::Bucket(network::mojom::RequestDestination::kScript, 1))); |
| histogram_tester.ExpectUniqueSample( |
| ::internal::kHistogramLCPPSubresourceCountPrecision, 75, 1); |
| histogram_tester.ExpectUniqueSample( |
| ::internal::kHistogramLCPPSubresourceCountRecall, 60, 1); |
| histogram_tester.ExpectUniqueSample( |
| ::internal::kHistogramLCPPSubresourceCountSameSiteRatio, 100, 1); |
| } |
| |
| class LCPPTimingPredictorTestBase : public InProcessBrowserTest { |
| public: |
| ~LCPPTimingPredictorTestBase() override = default; |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| slow_response_manager_ = |
| std::make_unique<net::test_server::ControllableHttpResponseManager>( |
| embedded_test_server(), "/image_slow.png"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| loading_predictor_ = |
| LoadingPredictorFactory::GetForProfile(browser()->profile()); |
| ASSERT_TRUE(loading_predictor_); |
| loading_predictor_->EnableLCPPTesting(); |
| PredictorInitializer initializer( |
| loading_predictor_->resource_prefetch_predictor()); |
| initializer.EnsurePredictorInitialized(); |
| } |
| |
| void TearDownOnMainThread() override { loading_predictor_ = nullptr; } |
| |
| void NavigateAndWaitForLcpElement( |
| const GURL& url, |
| const std::string& expected_events, |
| size_t expected_lcp_count, |
| const std::optional<size_t>& expected_lcp_index, |
| const base::Location& from_here = FROM_HERE) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| page_load_metrics::PageLoadMetricsTestWaiter onload_waiter(web_contents); |
| onload_waiter.AddPageExpectation( |
| page_load_metrics::PageLoadMetricsTestWaiter::TimingField::kLoadEvent); |
| |
| LcpTimingPredictedWaiter timing_predicted_waiter( |
| loading_predictor()->resource_prefetch_predictor()); |
| LcpUpdatedWaiter lcp_updated_waiter( |
| loading_predictor()->resource_prefetch_predictor(), expected_lcp_count); |
| |
| content::NavigationController::LoadURLParams params(url); |
| web_contents->GetController().LoadURLWithParams(params); |
| |
| const std::vector<std::optional<std::string>>& lcp_element_locators = |
| lcp_updated_waiter.Wait(); |
| |
| std::unique_ptr<net::test_server::ControllableHttpResponse> slow_response = |
| slow_response_manager_->WaitForRequest(); |
| slow_response->Send(net::HTTP_OK, "image/png", "image_body", /*cookies=*/{}, |
| {"Cache-Control: no-store"}); |
| slow_response->Done(); |
| |
| onload_waiter.Wait(); |
| |
| const std::optional<std::string>& predicted_lcp_locator = |
| timing_predicted_waiter.Wait(); |
| EXPECT_EQ(predicted_lcp_locator.has_value(), |
| expected_lcp_index.has_value()); |
| if (expected_lcp_index) { |
| EXPECT_EQ(*lcp_element_locators[*expected_lcp_index], |
| *predicted_lcp_locator); |
| } |
| |
| std::string actual_events = content::EvalJs(web_contents, R"( |
| globalThis.events.join(", ") |
| )") |
| .ExtractString(); |
| |
| std::vector<std::string> expected_vec = base::SplitString( |
| expected_events, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| std::vector<std::string> actual_vec = base::SplitString( |
| actual_events, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| EXPECT_THAT(actual_vec, testing::UnorderedElementsAreArray(expected_vec)) |
| << "Expected events: " << expected_events |
| << "\nActual events: " << actual_events << "\nTimings: " |
| << content::EvalJs(web_contents, R"( |
| globalThis.timings.join(", ") |
| )") |
| .ExtractString(); |
| |
| LcpElementLearnWaiter lcp_element_waiter( |
| loading_predictor()->resource_prefetch_predictor()); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank"))) |
| << from_here.ToString(); |
| lcp_element_waiter.Wait(); |
| } |
| |
| LoadingPredictor* loading_predictor() { return loading_predictor_; } |
| |
| private: |
| raw_ptr<LoadingPredictor> loading_predictor_ = nullptr; |
| std::unique_ptr<net::test_server::ControllableHttpResponseManager> |
| slow_response_manager_; |
| }; |
| |
| class LCPPTimingPredictorBrowserTest : public LCPPTimingPredictorTestBase { |
| public: |
| LCPPTimingPredictorBrowserTest() { |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/ |
| {blink::features::kLCPTimingPredictorPrerender2}, |
| /*disabled_features=*/{}); |
| } |
| |
| void TestPrediction(const base::Location& from_here = FROM_HERE) { |
| const GURL kUrl = embedded_test_server()->GetURL( |
| "a.test", "/predictors/lcp_occur_twice.html"); |
| |
| NavigateAndWaitForLcpElement(kUrl, |
| /*expected_events=*/"LCP@IMG, LCP@DIV, Onload", |
| /*expected_lcp_count=*/2u, |
| /*expected_lcp_index=*/std::nullopt, |
| from_here); |
| NavigateAndWaitForLcpElement(kUrl, |
| /*expected_events=*/"LCP@IMG, LCP@DIV, Onload", |
| /*expected_lcp_count=*/2u, |
| /*expected_lcp_index=*/1u, // =DIV |
| from_here); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Confirm image element of the first LCP is predicted rather than div, or the |
| // actual LCP(current implementation) |
| // TODO(crbug.com/413192370): Flaky on win-rel. |
| #if BUILDFLAG(IS_WIN) |
| #define MAYBE_Base DISABLED_Base |
| #else |
| #define MAYBE_Base Base |
| #endif |
| IN_PROC_BROWSER_TEST_F(LCPPTimingPredictorBrowserTest, MAYBE_Base) { |
| TestPrediction(); |
| } |
| |
| class LCPPTimingPredictorBrowserFlagTest |
| : public LCPPTimingPredictorBrowserTest, |
| public ::testing::WithParamInterface<std::string> { |
| public: |
| LCPPTimingPredictorBrowserFlagTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/ |
| {{blink::features::kLCPCriticalPathPredictor, |
| {{blink::features::kLCPCriticalPathPredictorRecordedLcpElementTypes |
| .name, |
| GetParam()}}}}, |
| /*disabled_features=*/{}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // kLCPCriticalPathPredictorRecordedLcpElementTypes should not affect. |
| // TODO(crbug.com/413192370): Flaky on win-rel. |
| #if BUILDFLAG(IS_WIN) |
| #define MAYBE_WithKCriticalPathFlag DISABLED_WithKCriticalPathFlag |
| #else |
| #define MAYBE_WithKCriticalPathFlag WithKCriticalPathFlag |
| #endif |
| IN_PROC_BROWSER_TEST_P(LCPPTimingPredictorBrowserFlagTest, |
| MAYBE_WithKCriticalPathFlag) { |
| TestPrediction(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(Flags, |
| LCPPTimingPredictorBrowserFlagTest, |
| ::testing::Values("all", "image_only")); |
| |
| class LCPPAutoPreconnectTest : public InProcessBrowserTest, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| LCPPAutoPreconnectTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/ |
| {{blink::features::kLCPPAutoPreconnectLcpOrigin, |
| {{blink::features::kLCPPAutoPreconnectRecordAllOrigins.name, |
| GetParam() ? "true" : "false"}}}}, |
| /*disabled_features=*/{}); |
| } |
| |
| ~LCPPAutoPreconnectTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| loading_predictor_ = |
| LoadingPredictorFactory::GetForProfile(browser()->profile()); |
| ASSERT_TRUE(loading_predictor_); |
| |
| PredictorInitializer initializer( |
| loading_predictor_->resource_prefetch_predictor()); |
| initializer.EnsurePredictorInitialized(); |
| |
| preconnect_manager_observer_ = |
| std::make_unique<TestPreconnectManagerObserver>( |
| loading_predictor_->preconnect_manager()); |
| } |
| |
| void TearDownOnMainThread() override { loading_predictor_ = nullptr; } |
| |
| void NavigateAndWaitForLcpElement( |
| const GURL& url, |
| const base::Location& from_here = FROM_HERE) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| page_load_metrics::PageLoadMetricsTestWaiter waiter(web_contents); |
| waiter.AddMinimumLargestContentfulPaintImageExpectation(1); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)) |
| << from_here.ToString(); |
| waiter.Wait(); |
| |
| waiter.AddMinimumLargestContentfulPaintImageExpectation(1); |
| EXPECT_TRUE(content::ExecJs( |
| web_contents, base::StringPrintf(R"( |
| const img_bar = document.getElementById("bar"); |
| img_bar.src = "http://bar.com:%d/predictors/lcp-100x50.png"; |
| )", |
| embedded_test_server()->port()))); |
| waiter.Wait(); |
| |
| // Navigate to about:blank to force recording a LCP element. |
| LcpElementLearnWaiter lcp_element_waiter( |
| loading_predictor()->resource_prefetch_predictor()); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank"))) |
| << from_here.ToString(); |
| lcp_element_waiter.Wait(); |
| } |
| |
| std::vector<std::string> GetPreconnectedHosts() const { |
| std::vector<std::string> hosts; |
| for (auto& url : |
| preconnect_manager_observer_->PreconnectUrlAttemptsHistory()) { |
| hosts.push_back(url.host()); |
| } |
| return hosts; |
| } |
| |
| LoadingPredictor* loading_predictor() { return loading_predictor_; } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| raw_ptr<LoadingPredictor> loading_predictor_ = nullptr; |
| std::unique_ptr<TestPreconnectManagerObserver> preconnect_manager_observer_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(LCPPAutoPreconnectTest, EnabledAllOrigins) { |
| const bool kEnabledAllOrigins = GetParam(); |
| |
| const GURL kUrl = embedded_test_server()->GetURL( |
| "a.test", GetPathWithPortReplacement("/predictors/preconnect.html", |
| embedded_test_server()->port())); |
| base::HistogramTester histogram_tester; |
| |
| NavigateAndWaitForLcpElement(kUrl); |
| EXPECT_EQ(std::vector<std::string>({"a.test"}), GetPreconnectedHosts()); |
| histogram_tester.ExpectUniqueSample("Blink.LCPP.PreconnectCount", |
| kEnabledAllOrigins ? 2 : 1, 1); |
| |
| NavigateAndWaitForLcpElement(kUrl); |
| EXPECT_EQ( |
| kEnabledAllOrigins |
| ? std::vector<std::string>({"a.test", "a.test", "foo.com", "bar.com"}) |
| : std::vector<std::string>({"a.test", "a.test", "bar.com"}), |
| GetPreconnectedHosts()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(Flags, |
| LCPPAutoPreconnectTest, |
| ::testing::Bool(), |
| ::testing::PrintToStringParamName()); |
| |
| class SuppressesLoadingPredictorOnSlowNetworkBrowserTest |
| : public LoadingPredictorBrowserTest { |
| public: |
| SuppressesLoadingPredictorOnSlowNetworkBrowserTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{features::kSuppressesLoadingPredictorOnSlowNetwork, |
| {{features::kSuppressesLoadingPredictorOnSlowNetworkThreshold.name, |
| "500ms"}}}}, |
| {}); |
| } |
| |
| network::NetworkQualityTracker& GetNetworkQualityTracker() const { |
| return *g_browser_process->network_quality_tracker(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that kSuppressesLoadingPredictorOnSlowNetwork feature suppresses |
| // LoadingPredictor on slow network. |
| IN_PROC_BROWSER_TEST_F(SuppressesLoadingPredictorOnSlowNetworkBrowserTest, |
| SuppressesOnSlowNetwork) { |
| GURL url = embedded_test_server()->GetURL("/nocontent"); |
| base::TimeDelta http_rtt = GetNetworkQualityTracker().GetHttpRTT(); |
| int32_t downstream_throughput_kbps = |
| GetNetworkQualityTracker().GetDownstreamThroughputKbps(); |
| |
| { |
| // LoadingPredictor will be suppressed on slow networks. |
| GetNetworkQualityTracker().ReportRTTsAndThroughputForTesting( |
| base::Milliseconds(501), downstream_throughput_kbps); |
| auto observer = NavigateToURLAsync(url); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| EXPECT_EQ(0u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| } |
| |
| { |
| // LoadingPredictor will not be suppressed on fast networks. |
| GetNetworkQualityTracker().ReportRTTsAndThroughputForTesting( |
| base::Milliseconds(500), downstream_throughput_kbps); |
| auto observer = NavigateToURLAsync(url); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| EXPECT_EQ(1u, loading_predictor()->GetTotalHintsActivatedForTesting()); |
| } |
| |
| // Reset to the original values. |
| GetNetworkQualityTracker().ReportRTTsAndThroughputForTesting( |
| http_rtt, downstream_throughput_kbps); |
| } |
| |
| enum class NetworkIsolationKeyMode { |
| kDisabled, |
| kEnabled, |
| }; |
| |
| class LoadingPredictorNetworkIsolationKeyBrowserTest |
| : public LoadingPredictorBrowserTest, |
| public testing::WithParamInterface<NetworkIsolationKeyMode> { |
| public: |
| LoadingPredictorNetworkIsolationKeyBrowserTest() { |
| switch (GetParam()) { |
| case NetworkIsolationKeyMode::kDisabled: |
| scoped_feature_list2_.InitWithFeatures( |
| // enabled_features |
| {features::kLoadingPreconnectToRedirectTarget}, |
| // disabled_features |
| {net::features::kPartitionConnectionsByNetworkIsolationKey, |
| net::features::kSplitCacheByNetworkIsolationKey}); |
| break; |
| case NetworkIsolationKeyMode::kEnabled: |
| scoped_feature_list2_.InitWithFeatures( |
| // enabled_features |
| {net::features::kPartitionConnectionsByNetworkIsolationKey, |
| net::features::kSplitCacheByNetworkIsolationKey, |
| features::kLoadingPreconnectToRedirectTarget}, |
| // disabled_features |
| {}); |
| break; |
| } |
| } |
| |
| ~LoadingPredictorNetworkIsolationKeyBrowserTest() override = default; |
| |
| // One server is used to initiate preconnects, and one is preconnected to. |
| // This makes tracking preconnected sockets much easier, and removes all |
| // worried about favicon fetches and other sources of preconnects. |
| net::EmbeddedTestServer* preconnecting_test_server() { |
| return &preconnecting_test_server_; |
| } |
| |
| // Load the favicon into the cache, so that favicon requests won't create any |
| // new connections. Can't just wait for it normally, because there's no easy |
| // way to be sure that the favicon request associated with a page load has |
| // completed, since it doesn't block navigation complete events. |
| void CacheFavIcon() { |
| CacheUrl(embedded_test_server()->GetURL("/favicon.ico")); |
| } |
| |
| // Drives a request for the provided URL to completion, which will then be |
| // stored in the HTTP cache, headers permitting. |
| void CacheUrl(const GURL& url) { |
| std::unique_ptr<network::ResourceRequest> request = |
| std::make_unique<network::ResourceRequest>(); |
| request->url = url; |
| content::SimpleURLLoaderTestHelper simple_loader_helper; |
| url::Origin origin = url::Origin::Create(url); |
| request->trusted_params = network::ResourceRequest::TrustedParams(); |
| request->trusted_params->isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(origin); |
| request->site_for_cookies = |
| request->trusted_params->isolation_info.site_for_cookies(); |
| std::unique_ptr<network::SimpleURLLoader> simple_loader = |
| network::SimpleURLLoader::Create(std::move(request), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| simple_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| browser()->profile()->GetURLLoaderFactory().get(), |
| simple_loader_helper.GetCallbackDeprecated()); |
| simple_loader_helper.WaitForCallback(); |
| ASSERT_TRUE(simple_loader_helper.response_body()); |
| if (url.IntPort() == embedded_test_server()->port()) { |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } else { |
| EXPECT_EQ(url.IntPort(), preconnecting_test_server_.port()); |
| EXPECT_EQ( |
| 1u, |
| preconnecting_server_connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ( |
| 1u, preconnecting_server_connection_tracker()->GetReadSocketCount()); |
| } |
| ResetNetworkState(); |
| } |
| |
| void RunCorsTest(bool use_cors_for_preconnect, |
| bool use_cors_for_resource_request) { |
| auto kCrossOriginValue = std::to_array<const char*>({ |
| "anonymous", |
| "use-credentials", |
| }); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), preconnecting_test_server()->GetURL("/title1.html"))); |
| |
| // Preconnect a socket. |
| content::WebContents* tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| GURL preconnect_url = embedded_test_server()->base_url(); |
| std::string start_preconnect = base::StringPrintf( |
| "var link = document.createElement('link');" |
| "link.rel = 'preconnect';" |
| "link.crossOrigin = '%s';" |
| "link.href = '%s';" |
| "document.head.appendChild(link);", |
| kCrossOriginValue[use_cors_for_preconnect], |
| preconnect_url.spec().c_str()); |
| content::ExecuteScriptAsync(tab, start_preconnect); |
| connection_tracker()->WaitForAcceptedConnections(1u); |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| // Load an image. |
| GURL image_url = embedded_test_server()->GetURL("/test.gif"); |
| std::string load_image = base::StringPrintf( |
| "var image = document.createElement('img');" |
| "image.crossOrigin = '%s';" |
| "image.src = '%s';" |
| "document.body.appendChild(image);", |
| kCrossOriginValue[use_cors_for_resource_request], |
| image_url.spec().c_str()); |
| content::ExecuteScriptAsync(tab, load_image); |
| connection_tracker()->WaitUntilConnectionRead(); |
| |
| // The preconnected socket should have been used by the image request if |
| // the CORS behavior of the preconnect and the request were the same. |
| if (use_cors_for_preconnect == use_cors_for_resource_request) { |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } else { |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list2_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| LoadingPredictorNetworkIsolationKeyBrowserTest, |
| ::testing::Values(NetworkIsolationKeyMode::kDisabled, |
| NetworkIsolationKeyMode::kEnabled)); |
| |
| // Make sure that the right NetworkAnonymizationKey is used by the |
| // LoadingPredictor, both when the predictor is populated and when it isn't. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| LoadingPredictorNoRedirects) { |
| // Cache resources needed by navigations, so the only sockets created |
| // during navigations should be for the two preconnects. |
| CacheFavIcon(); |
| GURL cacheable_url = embedded_test_server()->GetURL("/cachetime"); |
| CacheUrl(cacheable_url); |
| |
| // For the first loop iteration, the predictor has no state, and for the |
| // second one it does. |
| for (bool predictor_has_state : {false, true}) { |
| SCOPED_TRACE(predictor_has_state); |
| |
| auto observer = NavigateToURLAsync(cacheable_url); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| connection_tracker()->WaitForAcceptedConnections(2); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| // Have the page fetch a subresource, which should use one of the |
| // preconnects triggered by the above navigation, due to the matching |
| // NetworkAnonymizationKey. Do this instead of a navigation to a non-cached |
| // URL to avoid triggering more preconnects. |
| std::string fetch_resource = base::StringPrintf( |
| "(async () => {" |
| " var resp = (await fetch('%s'));" |
| " return resp.status; })();", |
| embedded_test_server()->GetURL("/echo").spec().c_str()); |
| EXPECT_EQ(200, EvalJs(browser() |
| ->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(), |
| fetch_resource)); |
| |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| |
| ResetNetworkState(); |
| } |
| } |
| |
| // Make sure that the right NetworkAnonymizationKey is used by the |
| // LoadingPredictor, both when the predictor is populated and when it isn't. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| LoadingPredictorWithRedirects) { |
| // Cache resources needed by navigations, so the only connections to the |
| // tracked server created during navigations should be for preconnects. |
| CacheFavIcon(); |
| GURL cacheable_url = embedded_test_server()->GetURL("/cachetime"); |
| CacheUrl(cacheable_url); |
| |
| GURL redirecting_url = preconnecting_test_server()->GetURL( |
| "/server-redirect?" + cacheable_url.spec()); |
| |
| // Learn the redirects from initial navigation. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), redirecting_url)); |
| EXPECT_EQ(0u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| // The next navigation should preconnect. It won't use the preconnected |
| // socket, since the destination resource is still in the cache. |
| auto observer = NavigateToURLAsync(redirecting_url); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| connection_tracker()->WaitForAcceptedConnections(1); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| // Have the page fetch a subresource, which should use one of the |
| // preconnects triggered by the above navigation, due to the matching |
| // NetworkAnonymizationKey. Do this instead of a navigation to a non-cached |
| // URL to avoid triggering more preconnects. |
| std::string fetch_resource = base::StringPrintf( |
| "(async () => {" |
| " var resp = (await fetch('%s'));" |
| " return resp.status; })();", |
| embedded_test_server()->GetURL("/echo").spec().c_str()); |
| EXPECT_EQ(200, EvalJs(browser() |
| ->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(), |
| fetch_resource)); |
| |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Checks the opposite of the above test - tests that even when a redirect is |
| // predicted, preconnects are still made to the original origin using the |
| // correct NetworkAnonymizationKey. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| LoadingPredictorWithRedirects2) { |
| // Cache the redirect, so the only connections to the tracked server created |
| // during navigations should be for preconnects. |
| GURL destination_url = preconnecting_test_server()->GetURL("/cachetime"); |
| GURL redirecting_url = embedded_test_server()->GetURL("/cached-redirect?" + |
| destination_url.spec()); |
| |
| // Unlike other tests, the "preconnecting" server is actually the final |
| // destination, so its favicon needs to be cached. |
| CacheUrl(preconnecting_test_server()->GetURL("/favicon.ico")); |
| |
| CacheUrl(redirecting_url); |
| |
| // The first navigation learns to preconnect based on the redirect, and the |
| // second actually preconnects to the untracked server. All navigations should |
| // preconnect twice to the tracked server. |
| for (int i = 0; i < 2; ++i) { |
| if (i == 0) { |
| // NavigateToURL waits long enough to ensure information from the |
| // navigation is learned, while WaitForNavigationFinished() does not. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), redirecting_url)); |
| } else { |
| auto observer = NavigateToURLAsync(redirecting_url); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| } |
| connection_tracker()->WaitForAcceptedConnections(2); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| // Verify that the redirect from |redirecting_url| to |destination_url| was |
| // learned and preconnected to. |
| if (i == 1) { |
| preconnecting_server_connection_tracker()->WaitForAcceptedConnections(1); |
| } |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| // Verify that the preconnects to |embedded_test_server| were made using |
| // the |redirecting_url|'s NetworkAnonymizationKey. To do this, make a |
| // request using the tracked server's NetworkAnonymizationKey, and verify it |
| // used one of the existing sockets. |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = embedded_test_server()->GetURL("/echo"); |
| content::SimpleURLLoaderTestHelper simple_loader_helper; |
| url::Origin origin = url::Origin::Create(request->url); |
| request->trusted_params = network::ResourceRequest::TrustedParams(); |
| request->trusted_params->isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(origin); |
| request->site_for_cookies = |
| request->trusted_params->isolation_info.site_for_cookies(); |
| std::unique_ptr<network::SimpleURLLoader> simple_loader = |
| network::SimpleURLLoader::Create(std::move(request), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| simple_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| browser()->profile()->GetURLLoaderFactory().get(), |
| simple_loader_helper.GetCallbackDeprecated()); |
| simple_loader_helper.WaitForCallback(); |
| ASSERT_TRUE(simple_loader_helper.response_body()); |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| |
| ResetNetworkState(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| LinkRelPreconnectMainFrame) { |
| const char kHost1[] = "host1.test"; |
| const char kHost2[] = "host2.test"; |
| GURL preconnect_url = embedded_test_server()->GetURL("/echo"); |
| |
| // Navigate two tabs, one to each host. |
| |
| content::WebContents* tab1 = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), preconnecting_test_server()->GetURL(kHost1, "/title1.html"))); |
| |
| chrome::NewTab(browser()); |
| content::WebContents* tab2 = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), preconnecting_test_server()->GetURL(kHost2, "/title1.html"))); |
| |
| std::string start_preconnect = base::StringPrintf( |
| "var link = document.createElement('link');" |
| "link.rel = 'preconnect';" |
| "link.crossOrigin = 'anonymous';" |
| "link.href = '%s';" |
| "document.head.appendChild(link);", |
| preconnect_url.spec().c_str()); |
| content::ExecuteScriptAsync(tab1->GetPrimaryMainFrame(), start_preconnect); |
| connection_tracker()->WaitForAcceptedConnections(1u); |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| std::string fetch_resource = base::StringPrintf( |
| "(async () => {" |
| " var resp = (await fetch('%s'," |
| " {credentials: 'omit'," |
| " mode: 'no-cors'}));" |
| " return resp.status; })();", |
| preconnect_url.spec().c_str()); |
| // Fetch a resource from the test server from tab 2, without CORS. |
| EXPECT_EQ(0, EvalJs(tab2->GetPrimaryMainFrame(), fetch_resource)); |
| if (GetParam() == NetworkIsolationKeyMode::kDisabled) { |
| // When not using NetworkAnonymizationKeys, the preconnected socket from a |
| // tab at one site is usable by a request from another site. |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } else { |
| // When using NetworkAnonymizationKeys, the preconnected socket cannot be |
| // used. |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Now try fetching a resource from tab 1. |
| EXPECT_EQ(0, EvalJs(tab1->GetPrimaryMainFrame(), fetch_resource)); |
| // If the preconnected socket was not used before, it should now be used. If |
| // it was used before, a new socket will be used. |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(2u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| LinkRelPreconnectSubFrame) { |
| const char kHost1[] = "host1.test"; |
| const char kHost2[] = "host2.test"; |
| GURL preconnect_url = embedded_test_server()->GetURL("/echo"); |
| |
| // Tab 1 has two iframes, one at kHost1, one at kHost2. |
| content::WebContents* tab1 = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), preconnecting_test_server()->GetURL( |
| kHost1, GetPathWithPortReplacement( |
| "/predictors/two_iframes.html", |
| preconnecting_test_server()->port())))); |
| content::RenderFrameHost* main_frame = tab1->GetPrimaryMainFrame(); |
| ASSERT_EQ(kHost1, main_frame->GetLastCommittedOrigin().host()); |
| content::RenderFrameHost* iframe_1 = ChildFrameAt(main_frame, 0); |
| ASSERT_TRUE(iframe_1); |
| ASSERT_EQ(kHost1, iframe_1->GetLastCommittedOrigin().host()); |
| content::RenderFrameHost* iframe_2 = ChildFrameAt(main_frame, 1); |
| ASSERT_TRUE(iframe_2); |
| ASSERT_EQ(kHost2, iframe_2->GetLastCommittedOrigin().host()); |
| |
| // Create another tab without an iframe, at kHost2. |
| chrome::NewTab(browser()); |
| content::WebContents* tab2 = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), preconnecting_test_server()->GetURL(kHost2, "/title1.html"))); |
| |
| // Preconnect a socket in the cross-origin iframe. |
| std::string start_preconnect = base::StringPrintf( |
| "var link = document.createElement('link');" |
| "link.rel = 'preconnect';" |
| "link.crossOrigin = 'anonymous';" |
| "link.href = '%s';" |
| "document.head.appendChild(link);", |
| preconnect_url.spec().c_str()); |
| content::ExecuteScriptAsync(iframe_2, start_preconnect); |
| connection_tracker()->WaitForAcceptedConnections(1u); |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| |
| std::string fetch_resource = base::StringPrintf( |
| "(async () => {" |
| " var resp = (await fetch('%s'," |
| " {credentials: 'omit'," |
| " mode: 'no-cors'}));" |
| " return resp.status; })();", |
| preconnect_url.spec().c_str()); |
| |
| // Fetch a resource from the test server from tab 2 iframe, without CORS. |
| EXPECT_EQ(0, EvalJs(tab2->GetPrimaryMainFrame(), fetch_resource)); |
| if (GetParam() == NetworkIsolationKeyMode::kDisabled) { |
| // When not using NetworkAnonymizationKeys, the preconnected socket from the |
| // iframe from the first tab can be used. |
| EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } else { |
| // Otherwise, the preconnected socket cannot be used. |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Fetch a resource from the test server from the same-origin iframe, without |
| // CORS. |
| EXPECT_EQ(0, EvalJs(iframe_1, fetch_resource)); |
| if (GetParam() == NetworkIsolationKeyMode::kDisabled) { |
| // When not using NetworkAnonymizationKeys, a new socket is created and |
| // used. |
| EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(2u, connection_tracker()->GetReadSocketCount()); |
| } else { |
| // Otherwise, the preconnected socket cannot be used. |
| EXPECT_EQ(3u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(2u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Now try fetching a resource the cross-site iframe. |
| EXPECT_EQ(0, EvalJs(iframe_2, fetch_resource)); |
| // If the preconnected socket was not used before, it should now be used. If |
| // it was used before, a new socket will be used. |
| EXPECT_EQ(3u, connection_tracker()->GetAcceptedSocketCount()); |
| EXPECT_EQ(3u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // Tests that preconnect warms up a non-CORS connection to a test |
| // server, and that socket is used when fetching a non-CORS resource. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| PreconnectNonCorsAndFetchNonCors) { |
| RunCorsTest(false /* use_cors_for_preconnect */, |
| false /* use_cors_for_resource_request */); |
| } |
| |
| // Tests that preconnect warms up a non-CORS connection to a test |
| // server, but that socket is not used when fetching a CORS resource. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| PreconnectNonCorsAndFetchCors) { |
| RunCorsTest(false /* use_cors_for_preconnect */, |
| true /* use_cors_for_resource_request */); |
| } |
| |
| // Tests that preconnect warms up a CORS connection to a test server, |
| // but that socket is not used when fetching a non-CORS resource. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| PreconnectCorsAndFetchNonCors) { |
| RunCorsTest(true /* use_cors_for_preconnect */, |
| false /* use_cors_for_resource_request */); |
| } |
| |
| // Tests that preconnect warms up a CORS connection to a test server, |
| // that socket is used when fetching a CORS resource. |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest, |
| PreconnectCorsAndFetchCors) { |
| RunCorsTest(true /* use_cors_for_preconnect */, |
| true /* use_cors_for_resource_request */); |
| } |
| |
| class LoadingPredictorBrowserTestWithProxy |
| : public LoadingPredictorBrowserTest { |
| public: |
| void SetUp() override { |
| pac_script_server_ = std::make_unique<net::EmbeddedTestServer>(); |
| pac_script_server_->AddDefaultHandlers(GetChromeTestDataDir()); |
| ASSERT_TRUE(pac_script_server_->InitializeAndListen()); |
| LoadingPredictorBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| LoadingPredictorBrowserTest::SetUpOnMainThread(); |
| // This will make all dns requests fail. |
| host_resolver()->ClearRules(); |
| |
| pac_script_server_->StartAcceptingConnections(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| GURL pac_url = pac_script_server_->GetURL(GetPathWithPortReplacement( |
| "/predictors/proxy.pac", embedded_test_server()->port())); |
| command_line->AppendSwitchASCII(switches::kProxyPacUrl, pac_url.spec()); |
| } |
| |
| private: |
| std::unique_ptr<net::EmbeddedTestServer> pac_script_server_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithProxy, |
| PrepareForPageLoadWithoutPrediction) { |
| // Navigate the first time to fill the HTTP cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| ResetPredictorState(); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| preconnect_manager_observer()->WaitUntilProxyLookedUp( |
| url, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->ProxyFound( |
| url, network_anonymization_key)); |
| // We should preconnect only 2 sockets for the main frame host. |
| const size_t expected_connections = 2; |
| connection_tracker()->WaitForAcceptedConnections(expected_connections); |
| EXPECT_EQ(expected_connections, |
| connection_tracker()->GetAcceptedSocketCount()); |
| // No reads since all resources should be cached. |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithProxy, |
| PrepareForPageLoadWithPrediction) { |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| for (auto* const host : kHtmlSubresourcesHosts) { |
| GURL host_url = embedded_test_server()->GetURL(host, "/"); |
| preconnect_manager_observer()->WaitUntilProxyLookedUp( |
| host_url, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->ProxyFound( |
| host_url, network_anonymization_key)); |
| } |
| // 2 connections to the main frame host + 1 connection per host for others. |
| const size_t expected_connections = std::size(kHtmlSubresourcesHosts) + 1; |
| connection_tracker()->WaitForAcceptedConnections(expected_connections); |
| EXPECT_EQ(expected_connections, |
| connection_tracker()->GetAcceptedSocketCount()); |
| // No reads since all resources should be cached. |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| class LoadingPredictorBrowserTestWithOptimizationGuide |
| : public ::testing::WithParamInterface< |
| std::tuple<bool, bool, bool, std::string>>, |
| public LoadingPredictorBrowserTest { |
| public: |
| LoadingPredictorBrowserTestWithOptimizationGuide() { |
| feature_list_.InitWithFeaturesAndParameters( |
| {{features::kLoadingPredictorUseOptimizationGuide, |
| {{"use_predictions", |
| base::ToString(ShouldUseOptimizationGuidePredictions())}, |
| {"always_retrieve_predictions", "true"}}}, |
| {optimization_guide::features::kOptimizationHints, {}}}, |
| {}); |
| |
| if (IsLocalPredictionEnabled()) { |
| local_predictions_feature_list_.InitAndEnableFeature( |
| features::kLoadingPredictorUseLocalPredictions); |
| } else { |
| local_predictions_feature_list_.InitAndDisableFeature( |
| features::kLoadingPredictorUseLocalPredictions); |
| } |
| |
| if (IsPrefetchEnabled()) { |
| prefetch_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kLoadingPredictorPrefetch, |
| {{"subresource_type", GetSubresourceTypeParam()}}); |
| } else { |
| prefetch_feature_list_.InitAndDisableFeature( |
| features::kLoadingPredictorPrefetch); |
| } |
| } |
| |
| bool IsLocalPredictionEnabled() const { return std::get<0>(GetParam()); } |
| |
| bool ShouldUseOptimizationGuidePredictions() const { |
| return std::get<1>(GetParam()); |
| } |
| |
| bool IsPrefetchEnabled() const { return std::get<2>(GetParam()); } |
| |
| std::string GetSubresourceTypeParam() const { |
| return std::string(std::get<3>(GetParam())); |
| } |
| |
| bool ShouldRetrieveOptimizationGuidePredictions() { |
| return !IsLocalPredictionEnabled() || |
| features::ShouldAlwaysRetrieveOptimizationGuidePredictions(); |
| } |
| |
| // A predicted subresource. |
| struct Subresource { |
| explicit Subresource(std::string url) |
| : url(url), |
| type(optimization_guide::proto::RESOURCE_TYPE_UNKNOWN), |
| preconnect_only(false) {} |
| Subresource(std::string url, optimization_guide::proto::ResourceType type) |
| : url(url), type(type), preconnect_only(false) {} |
| Subresource(std::string url, |
| optimization_guide::proto::ResourceType type, |
| bool preconnect_only) |
| : url(url), type(type), preconnect_only(preconnect_only) {} |
| |
| std::string url; |
| optimization_guide::proto::ResourceType type; |
| bool preconnect_only; |
| }; |
| |
| void SetUpOptimizationHint( |
| const GURL& url, |
| const std::vector<Subresource>& predicted_subresources) { |
| auto* optimization_guide_keyed_service = |
| OptimizationGuideKeyedServiceFactory::GetForProfile( |
| browser()->profile()); |
| optimization_guide::proto::LoadingPredictorMetadata |
| loading_predictor_metadata; |
| for (const auto& subresource : predicted_subresources) { |
| auto* added = loading_predictor_metadata.add_subresources(); |
| added->set_url(subresource.url); |
| added->set_resource_type(subresource.type); |
| added->set_preconnect_only(subresource.preconnect_only); |
| } |
| |
| optimization_guide::OptimizationMetadata optimization_metadata; |
| optimization_metadata.set_loading_predictor_metadata( |
| loading_predictor_metadata); |
| optimization_guide_keyed_service->AddHintForTesting( |
| url, optimization_guide::proto::LOADING_PREDICTOR, |
| optimization_metadata); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| base::test::ScopedFeatureList local_predictions_feature_list_; |
| base::test::ScopedFeatureList prefetch_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| LoadingPredictorBrowserTestWithOptimizationGuide, |
| testing::Combine( |
| /*IsLocalPredictionEnabled()=*/testing::Bool(), |
| /*ShouldUseOptimizationGuidePredictions()=*/testing::Bool(), |
| /*IsPrefetchEnabled()=*/testing::Values(false), |
| /*GetSubresourceType()=*/testing::Values(""))); |
| |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide, |
| NavigationHasLocalPredictionNoOptimizationHint) { |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| for (auto* const host : kHtmlSubresourcesHosts) { |
| if (!IsLocalPredictionEnabled() && host != url.host()) { |
| // We don't expect local predictions to be preconnected to. |
| continue; |
| } |
| |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| host, network_anonymization_key)); |
| } |
| size_t expected_connections; |
| if (IsLocalPredictionEnabled()) { |
| // 2 connections to the main frame host + 1 connection per host for others. |
| expected_connections = std::size(kHtmlSubresourcesHosts) + 1; |
| } else { |
| // There should always be 2 connections to the main frame host. |
| expected_connections = 2; |
| } |
| connection_tracker()->WaitForAcceptedConnections(expected_connections); |
| EXPECT_EQ(expected_connections, |
| connection_tracker()->GetAcceptedSocketCount()); |
| // No reads since all resources should be cached. |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide, |
| NavigationWithBothLocalPredictionAndOptimizationHint) { |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| url::Origin origin = url::Origin::Create(url); |
| net::SchemefulSite site = net::SchemefulSite(origin); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| SetUpOptimizationHint(url, {Subresource("http://subresource.com/1"), |
| Subresource("http://subresource.com/2"), |
| Subresource("http://otherresource.com/2"), |
| Subresource("skipsoverinvalidurl/////")}); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| |
| // The initial URL should be preconnected to. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| url.host(), network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| origin.GetURL())); |
| |
| // Both subresource hosts should be preconnected to. |
| std::vector<std::string> expected_subresource_hosts; |
| if (IsLocalPredictionEnabled()) { |
| // Should use subresources that were learned. |
| expected_subresource_hosts = {"baz.com", "foo.com"}; |
| } else if (ShouldUseOptimizationGuidePredictions()) { |
| // Should use subresources from optimization hint. |
| expected_subresource_hosts = {"subresource.com", "otherresource.com"}; |
| } |
| for (const auto& host : expected_subresource_hosts) { |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| host, network_anonymization_key)); |
| |
| GURL expected_origin; |
| if (IsLocalPredictionEnabled()) { |
| // The locally learned origins are expected to have a port. |
| expected_origin = embedded_test_server()->GetURL(host, "/"); |
| } else { |
| // The optimization hints learned origins do not have a port. |
| expected_origin = GURL(base::StringPrintf("http://%s", host.c_str())); |
| } |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| expected_origin)); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide, |
| NavigationWithNoLocalPredictionsButHasOptimizationHint) { |
| base::HistogramTester histogram_tester; |
| |
| GURL url = embedded_test_server()->GetURL("m.hints.com", "/simple.html"); |
| SetUpOptimizationHint(url, {Subresource("http://subresource.com/1"), |
| Subresource("http://subresource.com/2"), |
| Subresource("http://otherresource.com/2"), |
| Subresource("skipsoverinvalidurl/////")}); |
| url::Origin origin = url::Origin::Create(url); |
| net::SchemefulSite site = net::SchemefulSite(origin); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| |
| // The initial URL should be preconnected to. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| url.host(), network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| origin.GetURL())); |
| for (auto* const host : {"subresource.com", "otherresource.com"}) { |
| if (ShouldUseOptimizationGuidePredictions()) { |
| // Both subresource hosts should be preconnected to. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host, network_anonymization_key); |
| } |
| EXPECT_EQ(preconnect_manager_observer()->HostFound( |
| host, network_anonymization_key), |
| ShouldUseOptimizationGuidePredictions()); |
| |
| EXPECT_EQ(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| GURL(base::StringPrintf("http://%s/", host))), |
| ShouldUseOptimizationGuidePredictions()); |
| } |
| |
| EXPECT_TRUE(observer->WaitForResponse()); |
| observer->ResumeNavigation(); |
| content::AwaitDocumentOnLoadCompleted(observer->web_contents()); |
| ASSERT_TRUE(observer->WaitForNavigationFinished()); |
| |
| // Navigate to another URL and wait until the previous RFH is destroyed (i.e. |
| // until the optimization guide prediction is cleared and metrics are |
| // recorded). |
| content::RenderFrameHostWrapper rfh( |
| observer->web_contents()->GetPrimaryMainFrame()); |
| // Disable BFCache to ensure the navigation below unloads |rfh|. |
| content::DisableBackForwardCacheForTesting( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| content::BackForwardCache::DisableForTestingReason:: |
| TEST_REQUIRES_NO_CACHING); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), embedded_test_server()->GetURL("nohints.com", "/"))); |
| ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| LoadingPredictorBrowserTestWithOptimizationGuide, |
| OptimizationGuidePredictionsNotAppliedForAlreadyCommittedNavigation) { |
| GURL url = embedded_test_server()->GetURL("hints.com", "/simple.html"); |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| // Navigate to URL with hints but only seed hints after navigation has |
| // committed. |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForResponse()); |
| observer->ResumeNavigation(); |
| SetUpOptimizationHint(url, {Subresource("http://subresource.com/1"), |
| Subresource("http://subresource.com/2"), |
| Subresource("http://otherresource.com/2"), |
| Subresource("skipsoverinvalidurl/////")}); |
| |
| EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| "subresource.com", network_anonymization_key)); |
| EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| "otheresource.com", network_anonymization_key)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide, |
| OptimizationGuidePredictionsNotAppliedForRedirect) { |
| GURL destination_url = |
| embedded_test_server()->GetURL("otherhost.com", "/cachetime"); |
| GURL redirecting_url = embedded_test_server()->GetURL( |
| "sometimesredirects.com", "/cached-redirect?" + destination_url.spec()); |
| SetUpOptimizationHint(destination_url, |
| {Subresource("http://subresource.com/1"), |
| Subresource("http://subresource.com/2"), |
| Subresource("http://otherresource.com/2"), |
| Subresource("skipsoverinvalidurl/////")}); |
| |
| // Navigate the first time to something on redirecting origin to fill the |
| // predictor's database and the HTTP cache. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| embedded_test_server()->GetURL( |
| "sometimesredirects.com", |
| GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())))); |
| ResetNetworkState(); |
| |
| net::SchemefulSite site = net::SchemefulSite(destination_url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| // Navigate to URL with hints but is redirected, hints should not be |
| // applied. |
| auto observer = NavigateToURLAsync(redirecting_url); |
| EXPECT_TRUE(observer->WaitForResponse()); |
| SetUpOptimizationHint(redirecting_url, |
| {Subresource("http://subresourceredirect.com/1"), |
| Subresource("http://subresourceredirect.com/2"), |
| Subresource("http://otherresourceredirect.com/2"), |
| Subresource("skipsoverinvalidurl/////")}); |
| observer->ResumeNavigation(); |
| |
| std::vector<std::string> expected_opt_guide_subresource_hosts = { |
| "subresource.com", "otherresource.com"}; |
| if (ShouldRetrieveOptimizationGuidePredictions() && |
| ShouldUseOptimizationGuidePredictions()) { |
| // Should use subresources from optimization hint. |
| for (const auto& host : expected_opt_guide_subresource_hosts) { |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| host, network_anonymization_key)); |
| |
| // The origins from optimization hints do not have a port. |
| GURL expected_origin = |
| GURL(base::StringPrintf("http://%s", host.c_str())); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| expected_origin)); |
| } |
| } else { |
| for (const auto& host : expected_opt_guide_subresource_hosts) { |
| EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| host, network_anonymization_key)); |
| } |
| } |
| } |
| |
| class LoadingPredictorBrowserTestWithNoLocalPredictions |
| : public LoadingPredictorBrowserTest { |
| public: |
| LoadingPredictorBrowserTestWithNoLocalPredictions() { |
| feature_list_.InitAndDisableFeature( |
| features::kLoadingPredictorUseLocalPredictions); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithNoLocalPredictions, |
| ShouldNotActOnLocalPrediction) { |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| url::Origin origin = url::Origin::Create(url); |
| net::SchemefulSite site = net::SchemefulSite(origin); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| // The initial URL should be preconnected to. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| url.host(), network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| origin.GetURL())); |
| // 2 connections to the main frame host. |
| const size_t expected_connections = 2; |
| connection_tracker()->WaitForAcceptedConnections(expected_connections); |
| EXPECT_EQ(expected_connections, |
| connection_tracker()->GetAcceptedSocketCount()); |
| // No reads since all resources should be cached. |
| EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount()); |
| } |
| |
| // A fixture for testing prefetching with optimization guide hints. |
| class LoadingPredictorPrefetchBrowserTest |
| : public LoadingPredictorBrowserTestWithOptimizationGuide { |
| public: |
| void SetUp() override { |
| embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( |
| &LoadingPredictorPrefetchBrowserTest::MonitorRequest, |
| base::Unretained(this))); |
| |
| LoadingPredictorBrowserTestWithOptimizationGuide::SetUp(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| LoadingPredictorBrowserTestWithOptimizationGuide::SetUpCommandLine( |
| command_line); |
| command_line->AppendSwitch( |
| switches::kLoadingPredictorAllowLocalRequestForTesting); |
| } |
| |
| protected: |
| // Sets the requests to expect in WaitForRequests(). |
| void SetExpectedRequests(base::flat_set<GURL> requests) { |
| expected_requests_ = std::move(requests); |
| } |
| |
| // Returns once all expected requests have been received. |
| void WaitForRequests() { |
| if (expected_requests_.empty()) { |
| return; |
| } |
| base::RunLoop loop; |
| quit_ = loop.QuitClosure(); |
| loop.Run(); |
| } |
| |
| private: |
| void MonitorRequest(const net::test_server::HttpRequest& request) { |
| // Monitor only prefetches. |
| if (request.headers.find(blink::kSecPurposeHeaderName) == |
| request.headers.end() || |
| (request.headers.at(blink::kSecPurposeHeaderName) != |
| blink::kSecPurposePrefetchHeaderValue)) { |
| return; |
| } |
| |
| // |request.GetURL()| gives us the URL after it's already resolved to |
| // 127.0.0.1, so reconstruct the requested host via the Host header |
| // (which includes host+port). |
| GURL url = request.GetURL(); |
| auto host_iter = request.headers.find("Host"); |
| if (host_iter != request.headers.end()) { |
| url = GURL("http://" + host_iter->second + request.relative_url); |
| } |
| |
| // Remove the expected request. |
| auto it = expected_requests_.find(url); |
| ASSERT_TRUE(it != expected_requests_.end()) |
| << "Got unexpected request: " << url; |
| expected_requests_.erase(it); |
| |
| // Finish if done. |
| if (expected_requests_.empty() && quit_) { |
| std::move(quit_).Run(); |
| } |
| } |
| |
| base::flat_set<GURL> expected_requests_; |
| base::OnceClosure quit_; |
| }; |
| |
| // Tests that the LoadingPredictor performs prefetching |
| // for a navigation which it has a prediction for and there isn't a local |
| // prediction available. |
| IN_PROC_BROWSER_TEST_P( |
| LoadingPredictorPrefetchBrowserTest, |
| DISABLED_PrepareForPageLoadWithPredictionForPrefetchNoLocalHint) { |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| |
| // Set up optimization hints. |
| std::vector<Subresource> hints = { |
| {"skipsoverinvalidurl/////", |
| optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| {embedded_test_server()->GetURL("subresource.com", "/css").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| {embedded_test_server()->GetURL("subresource.com", "/image").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_UNKNOWN}, |
| {embedded_test_server()->GetURL("otherresource.com", "/js").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_SCRIPT}, |
| {embedded_test_server()->GetURL("preconnect.com", "/other").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_UNKNOWN, true}, |
| }; |
| SetUpOptimizationHint(url, hints); |
| |
| // Expect these prefetches. |
| std::vector<GURL> requests; |
| if (GetSubresourceTypeParam() == "all") { |
| requests = {embedded_test_server()->GetURL("subresource.com", "/css"), |
| embedded_test_server()->GetURL("subresource.com", "/image"), |
| embedded_test_server()->GetURL("otherresource.com", "/js")}; |
| } else if (GetSubresourceTypeParam() == "css") { |
| requests = {embedded_test_server()->GetURL("subresource.com", "/css")}; |
| } else if (GetSubresourceTypeParam() == "js_css") { |
| requests = {embedded_test_server()->GetURL("subresource.com", "/css"), |
| embedded_test_server()->GetURL("otherresource.com", "/js")}; |
| } |
| SetExpectedRequests(std::move(requests)); |
| |
| // Start a navigation and observe these prefetches. |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| WaitForRequests(); |
| |
| // preconnect.com should be preconnected to. |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| "preconnect.com", network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| "preconnect.com", network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| embedded_test_server()->GetURL("preconnect.com", "/"))); |
| } |
| |
| // Tests that the LoadingPredictor performs prefetching |
| // for a navigation which it has a prediction for and there is a local |
| // prediction available. |
| IN_PROC_BROWSER_TEST_P( |
| LoadingPredictorPrefetchBrowserTest, |
| PrepareForPageLoadWithPredictionForPrefetchHasLocalHint) { |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| // Set up optimization hints. |
| std::vector<Subresource> hints = { |
| {"skipsoverinvalidurl/////", |
| optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| {embedded_test_server()->GetURL("subresource.com", "/css").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| {embedded_test_server()->GetURL("subresource.com", "/image").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_UNKNOWN}, |
| {embedded_test_server()->GetURL("otherresource.com", "/js").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_SCRIPT}, |
| {embedded_test_server()->GetURL("preconnect.com", "/other").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_UNKNOWN, true}, |
| }; |
| SetUpOptimizationHint(url, hints); |
| |
| // Expect these prefetches. |
| std::vector<GURL> requests; |
| if (GetSubresourceTypeParam() == "all") { |
| requests = {embedded_test_server()->GetURL("subresource.com", "/css"), |
| embedded_test_server()->GetURL("subresource.com", "/image"), |
| embedded_test_server()->GetURL("otherresource.com", "/js")}; |
| } else if (GetSubresourceTypeParam() == "css") { |
| requests = {embedded_test_server()->GetURL("subresource.com", "/css")}; |
| } else if (GetSubresourceTypeParam() == "js_css") { |
| requests = {embedded_test_server()->GetURL("subresource.com", "/css"), |
| embedded_test_server()->GetURL("otherresource.com", "/js")}; |
| } |
| SetExpectedRequests(std::move(requests)); |
| |
| // Start a navigation and observe these prefetches. |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| WaitForRequests(); |
| |
| std::vector<std::string> expected_subresource_hosts; |
| if (IsLocalPredictionEnabled()) { |
| // Should use subresources that were learned. |
| expected_subresource_hosts = {"baz.com", "foo.com"}; |
| } else { |
| // Should use subresources from optimization hint. |
| expected_subresource_hosts = {"preconnect.com"}; |
| } |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| for (const auto& host : expected_subresource_hosts) { |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| host, network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| embedded_test_server()->GetURL(host, "/"))); |
| } |
| } |
| |
| // A fixture for testing prefetching with the local resource check not bypassed. |
| // The normal fixture bypasses the check so that the embedded test server can be |
| // used. |
| class LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest |
| : public LoadingPredictorPrefetchBrowserTest { |
| public: |
| LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest() = default; |
| |
| // Override to prevent adding kLoadingPredictorAllowLocalRequestForTesting |
| // here. |
| void SetUpCommandLine(base::CommandLine* command_line) override {} |
| }; |
| |
| // Test that prefetches to local resources are blocked. |
| // Disabled for being flaky. crbug.com/1116599 |
| IN_PROC_BROWSER_TEST_P( |
| LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest, |
| DISABLED_PrepareForPageLoadWithPredictionForPrefetch) { |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| |
| GURL hint_url = embedded_test_server()->GetURL("subresource.com", "/css"); |
| |
| // Set up one optimization hint. |
| std::vector<Subresource> hints = { |
| {hint_url.spec(), optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| }; |
| SetUpOptimizationHint(url, hints); |
| |
| // Start a navigation which triggers prefetch. |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| |
| // The prefetch should have failed. |
| prefetch_manager_observer()->WaitForPrefetchesForNavigation(url); |
| auto results = prefetch_manager_observer()->results(); |
| ASSERT_THAT(results, SizeIs(1)); |
| |
| const auto& status = results[0].status; |
| EXPECT_EQ(status.error_code, net::ERR_FAILED); |
| EXPECT_THAT(status.cors_error_status, |
| Optional(network::CorsErrorStatus( |
| network::mojom::CorsError::kInsecurePrivateNetwork, |
| network::mojom::IPAddressSpace::kUnknown, |
| network::mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| // This fixture is for disabling prefetching via test suite instantiation to |
| // test the counterfactual arm (|always_retrieve_predictions| is |
| // true but using the predictions is disabled). |
| class LoadingPredictorPrefetchCounterfactualBrowserTest |
| : public LoadingPredictorPrefetchBrowserTest {}; |
| |
| IN_PROC_BROWSER_TEST_P( |
| LoadingPredictorPrefetchCounterfactualBrowserTest, |
| PrepareForPageLoadWithPredictionForPrefetchHasLocalHint) { |
| // Assert that this tests the counterfactual arm. |
| ASSERT_TRUE(features::ShouldAlwaysRetrieveOptimizationGuidePredictions()); |
| ASSERT_FALSE(features::ShouldUseOptimizationGuidePredictions()); |
| |
| // Navigate the first time to fill the predictor's database and the HTTP |
| // cache. |
| GURL url = embedded_test_server()->GetURL( |
| "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath, |
| embedded_test_server()->port())); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ResetNetworkState(); |
| |
| // Set up optimization hints. |
| std::vector<Subresource> hints = { |
| {"skipsoverinvalidurl/////", |
| optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| {embedded_test_server()->GetURL("subresource.com", "/css").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_CSS}, |
| {embedded_test_server()->GetURL("subresource.com", "/image").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_UNKNOWN}, |
| {embedded_test_server()->GetURL("otherresource.com", "/js").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_SCRIPT}, |
| {embedded_test_server()->GetURL("preconnect.com", "/other").spec(), |
| optimization_guide::proto::RESOURCE_TYPE_UNKNOWN, true}, |
| }; |
| SetUpOptimizationHint(url, hints); |
| |
| // Expect no prefetches. The test will fail if any prefetch requests are |
| // issued. |
| SetExpectedRequests({}); |
| |
| // Start a navigation. |
| auto observer = NavigateToURLAsync(url); |
| EXPECT_TRUE(observer->WaitForRequestStart()); |
| |
| std::vector<std::string> expected_subresource_hosts; |
| if (IsLocalPredictionEnabled()) { |
| // Should use subresources that were learned. |
| expected_subresource_hosts = {"baz.com", "foo.com"}; |
| } else { |
| // Should not use subresources from optimization hint since |
| // use_predictions is disabled. |
| } |
| net::SchemefulSite site = net::SchemefulSite(url); |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(site); |
| for (const auto& host : expected_subresource_hosts) { |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| host, network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| host, network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect( |
| embedded_test_server()->GetURL(host, "/"))); |
| } |
| |
| // Run the run loop to give the test a chance to fail by issuing a prefetch. |
| // We don't have an explicit signal for the prefetch manager *not* starting. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(prefetch_manager_observer()->results().empty()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| LoadingPredictorPrefetchBrowserTest, |
| testing::Combine( |
| /*IsLocalPredictionEnabled()=*/testing::Values(true, false), |
| /*ShouldUseOptimizationGuidePredictions()=*/ |
| testing::Values(true), |
| /*IsPrefetchEnabled()=*/testing::Values(true), |
| /*GetSubresourceType()=*/testing::Values("all", "css", "js_css"))); |
| |
| // For the "BlockedLocalRequest" test, the params largely don't matter. We just |
| // need to enable prefetching and test one configuration, since the test passes |
| // if the prefetch is blocked. |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest, |
| testing::Combine( |
| /*IsLocalPredictionEnabled()=*/testing::Values(false), |
| /*ShouldUseOptimizationGuidePredictions()=*/ |
| testing::Values(true), |
| /*IsPrefetchEnabled()=*/testing::Values(true), |
| /*GetSubresourceType()=*/testing::Values("all"))); |
| |
| // For the "prefetch counterfactual" test, we want to retrieve the optimization |
| // guide hints but not use them, so set ShouldUseOptimizationGuidePredictions() |
| // to false. It doesn't matter if IsPrefetchEnabled() is true or not, since |
| // prefetching only uses optimization guide predictions. |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| LoadingPredictorPrefetchCounterfactualBrowserTest, |
| testing::Combine( |
| /*IsLocalPredictionEnabled()=*/testing::Values(true, false), |
| /*ShouldUseOptimizationGuidePredictions()=*/ |
| testing::Values(false), |
| /*IsPrefetchEnabled()=*/testing::Values(true), |
| /*GetSubresourceType()=*/testing::Values("all"))); |
| |
| // Tests that LoadingPredictorTabHelper ignores prerender navigations and |
| // page activations. |
| class LoadingPredictorMultiplePageBrowserTest |
| : public LoadingPredictorBrowserTest { |
| public: |
| LoadingPredictorMultiplePageBrowserTest() |
| : prerender_test_helper_(base::BindRepeating( |
| &LoadingPredictorMultiplePageBrowserTest::GetWebContents, |
| base::Unretained(this))) {} |
| |
| content::test::PrerenderTestHelper& prerender_test_helper() { |
| return prerender_test_helper_; |
| } |
| |
| content::WebContents* GetWebContents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| protected: |
| void SetUp() override { |
| prerender_test_helper_.RegisterServerRequestMonitor(embedded_test_server()); |
| LoadingPredictorBrowserTest::SetUp(); |
| } |
| |
| private: |
| content::test::PrerenderTestHelper prerender_test_helper_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorMultiplePageBrowserTest, |
| PrerenderNavigationNotObserved) { |
| GURL first_main = embedded_test_server()->GetURL("/title1.html"); |
| GURL prerender = embedded_test_server()->GetURL("/title2.html"); |
| GURL second_main = embedded_test_server()->GetURL("/title3.html"); |
| auto* loading_predictor = |
| predictors::LoadingPredictorFactory::GetForProfile(browser()->profile()); |
| |
| // Start navigation in the primary main frame. |
| auto first_main_observer = std::make_unique<content::TestNavigationManager>( |
| GetWebContents(), first_main); |
| GetWebContents()->GetController().LoadURL(first_main, content::Referrer(), |
| ui::PAGE_TRANSITION_TYPED, |
| std::string()); |
| ASSERT_TRUE(first_main_observer->WaitForRequestStart()); |
| EXPECT_EQ(1u, loading_predictor->GetActiveNavigationsSizeForTesting()); |
| ASSERT_TRUE(first_main_observer->WaitForNavigationFinished()); |
| EXPECT_EQ(0u, loading_predictor->GetActiveNavigationsSizeForTesting()); |
| content::WaitForLoadStop(GetWebContents()); |
| EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| |
| // Start a prerender and a navigation in the primary main frame so we have 2 |
| // concurrent navigations. |
| auto prerender_observer = std::make_unique<content::TestNavigationManager>( |
| GetWebContents(), prerender); |
| auto second_main_observer = std::make_unique<content::TestNavigationManager>( |
| GetWebContents(), second_main); |
| prerender_test_helper().AddPrerenderAsync(prerender); |
| GetWebContents()->GetController().LoadURL(second_main, content::Referrer(), |
| ui::PAGE_TRANSITION_TYPED, |
| std::string()); |
| ASSERT_TRUE(prerender_observer->WaitForRequestStart()); |
| ASSERT_TRUE(second_main_observer->WaitForRequestStart()); |
| EXPECT_EQ(1u, loading_predictor->GetActiveNavigationsSizeForTesting()); |
| ASSERT_TRUE(prerender_observer->WaitForNavigationFinished()); |
| EXPECT_EQ(1u, loading_predictor->GetActiveNavigationsSizeForTesting()); |
| ASSERT_TRUE(second_main_observer->WaitForNavigationFinished()); |
| EXPECT_EQ(0u, loading_predictor->GetActiveNavigationsSizeForTesting()); |
| |
| content::WaitForLoadStop(GetWebContents()); |
| EXPECT_EQ(2u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorMultiplePageBrowserTest, |
| PrerenderActivationNotObserved) { |
| GURL main_url = embedded_test_server()->GetURL("/title1.html"); |
| GURL prerender_url = embedded_test_server()->GetURL("/title2.html"); |
| auto* loading_predictor = |
| predictors::LoadingPredictorFactory::GetForProfile(browser()->profile()); |
| |
| // Navigate primary main frame. |
| GetWebContents()->GetController().LoadURL( |
| main_url, content::Referrer(), ui::PAGE_TRANSITION_TYPED, std::string()); |
| content::WaitForLoadStop(GetWebContents()); |
| EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| |
| // Start a prerender. |
| prerender_test_helper().AddPrerender(prerender_url); |
| EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| |
| // Activate the prerender. |
| prerender_test_helper().NavigatePrimaryPage(prerender_url); |
| EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| } |
| |
| // TODO(crbug.com/325336071): Re-enable this test |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_BackForwardCacheNavigationNotObserved \ |
| DISABLED_BackForwardCacheNavigationNotObserved |
| #else |
| #define MAYBE_BackForwardCacheNavigationNotObserved \ |
| BackForwardCacheNavigationNotObserved |
| #endif |
| IN_PROC_BROWSER_TEST_F(LoadingPredictorMultiplePageBrowserTest, |
| MAYBE_BackForwardCacheNavigationNotObserved) { |
| GURL url_1 = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| GURL url_2 = embedded_test_server()->GetURL("b.com", "/title2.html"); |
| auto* loading_predictor = |
| predictors::LoadingPredictorFactory::GetForProfile(browser()->profile()); |
| |
| // Navigate primary main frame twice. |
| ASSERT_TRUE(content::NavigateToURL(GetWebContents(), url_1)); |
| content::RenderFrameHostWrapper rfh_1( |
| GetWebContents()->GetPrimaryMainFrame()); |
| ASSERT_TRUE(content::NavigateToURL(GetWebContents(), url_2)); |
| ASSERT_EQ(rfh_1->GetLifecycleState(), |
| content::RenderFrameHost::LifecycleState::kInBackForwardCache); |
| EXPECT_EQ(2u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| |
| // Go back (using BackForwardCache). |
| ASSERT_TRUE(content::HistoryGoBack(GetWebContents())); |
| ASSERT_EQ(GetWebContents()->GetPrimaryMainFrame(), rfh_1.get()); |
| EXPECT_EQ(2u, loading_predictor->GetTotalHintsActivatedForTesting()); |
| } |
| |
| // Test interaction with fenced frame `window.fence.disableUntrustedNetwork()` |
| // API. See: |
| // https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frames_with_local_unpartitioned_data_access.md#revoking-network-access |
| class FencedFrameLoadingPredictorBrowserTest |
| : public LoadingPredictorBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| LoadingPredictorBrowserTest::SetUpOnMainThread(); |
| |
| // Set up the embedded https test server for fenced frame which requires a |
| // secure context to load. |
| embedded_https_test_server().SetSSLConfig( |
| net::EmbeddedTestServer::CERT_TEST_NAMES); |
| |
| // Add content/test/data for cross_site_iframe_factory.html. |
| embedded_https_test_server().ServeFilesFromSourceDirectory( |
| "content/test/data"); |
| embedded_https_test_server().ServeFilesFromDirectory( |
| GetChromeTestDataDir()); |
| } |
| |
| content::test::FencedFrameTestHelper& fenced_frame_test_helper() { |
| return fenced_frame_test_helper_; |
| } |
| |
| content::WebContents* GetWebContents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| private: |
| content::test::FencedFrameTestHelper fenced_frame_test_helper_; |
| }; |
| |
| // Verify DNS prefetch is working in fenced frame. |
| IN_PROC_BROWSER_TEST_F(FencedFrameLoadingPredictorBrowserTest, DnsPrefetch) { |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = embedded_https_test_server().GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})"); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url)); |
| |
| // Get fenced frame render frame host. |
| std::vector<content::RenderFrameHost*> child_frames = |
| fenced_frame_test_helper().GetChildFencedFrameHosts( |
| GetWebContents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| content::RenderFrameHost* fenced_frame_rfh = child_frames[0]; |
| |
| // Get fenced frame NetworkAnonymizationKey. |
| const net::NetworkAnonymizationKey& network_anonymization_key = |
| fenced_frame_rfh->GetIsolationInfoForSubresources() |
| .network_anonymization_key(); |
| |
| GURL dns_prefetch_url("https://chromium.org"); |
| |
| // Add a link element in fenced frame that does a DNS prefetch. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, content::JsReplace(R"( |
| var link_element = document.createElement('link'); |
| link_element.href = $1; |
| link_element.rel = 'dns-prefetch'; |
| document.body.appendChild(link_element); |
| )", |
| dns_prefetch_url))); |
| |
| // The observer should observe a DNS prefetch which succeeds. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| dns_prefetch_url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| } |
| |
| // Verify DNS prefetch is disabled after fenced frame untrusted network cutoff. |
| IN_PROC_BROWSER_TEST_F(FencedFrameLoadingPredictorBrowserTest, |
| NetworkCutoffDisablesDnsPrefetch) { |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = embedded_https_test_server().GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})"); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url)); |
| |
| // Get fenced frame render frame host. |
| std::vector<content::RenderFrameHost*> child_frames = |
| fenced_frame_test_helper().GetChildFencedFrameHosts( |
| GetWebContents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| content::RenderFrameHost* fenced_frame_rfh = child_frames[0]; |
| |
| // Get fenced frame NetworkAnonymizationKey. |
| const net::NetworkAnonymizationKey& network_anonymization_key = |
| fenced_frame_rfh->GetIsolationInfoForSubresources() |
| .network_anonymization_key(); |
| |
| GURL dns_prefetch_url("https://chromium.org"); |
| |
| // Disable fenced frame untrusted network access, then add a link element |
| // that does a DNS prefetch. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, content::JsReplace(R"( |
| (async () => { |
| await window.fence.disableUntrustedNetwork().then( |
| () => { |
| var link_element = document.createElement('link'); |
| link_element.href = $1; |
| link_element.rel = 'dns-prefetch'; |
| document.body.appendChild(link_element); |
| } |
| ); |
| })(); |
| )", |
| dns_prefetch_url))); |
| |
| // The observer should observe a DNS prefetch which is cancelled. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| dns_prefetch_url.host(), network_anonymization_key); |
| |
| // The host is looked up, but the lookup is eventually cancelled because the |
| // fenced frame untrusted network access has been disabled. |
| EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| EXPECT_FALSE(preconnect_manager_observer()->HostFound( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| } |
| |
| // Verify DNS prefetch triggered by link response header is working in fenced |
| // frame. |
| // TODO(crbug.com/360154073): Disabled for flakiness. |
| IN_PROC_BROWSER_TEST_F(FencedFrameLoadingPredictorBrowserTest, |
| DISABLED_DnsPrefetchFromLinkHeader) { |
| std::string relative_url = "/title1.html"; |
| net::test_server::ControllableHttpResponse response( |
| &embedded_https_test_server(), relative_url); |
| |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = embedded_https_test_server().GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})"); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url)); |
| |
| // Get fenced frame render frame host. |
| std::vector<content::RenderFrameHost*> child_frames = |
| fenced_frame_test_helper().GetChildFencedFrameHosts( |
| GetWebContents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| content::RenderFrameHost* fenced_frame_rfh = child_frames[0]; |
| |
| GURL dns_prefetch_url("https://chromium.org"); |
| GURL navigation_url = |
| embedded_https_test_server().GetURL("a.test", relative_url); |
| |
| // Navigate the fenced frame. |
| content::TestFrameNavigationObserver observer(fenced_frame_rfh); |
| |
| EXPECT_TRUE( |
| ExecJs(GetWebContents()->GetPrimaryMainFrame(), |
| content::JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigation_url))); |
| |
| // Send a response header with link dns-prefetch field. |
| response.WaitForRequest(); |
| ResetNetworkState(); |
| ResetPredictorState(); |
| response.Send( |
| base::StringPrintf("HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Supports-Loading-Mode: fenced-frame\r\n" |
| "Link: <%s>; rel=dns-prefetch\r\n" |
| "\r\n", |
| dns_prefetch_url.spec().c_str())); |
| response.Done(); |
| |
| // Wait until navigation commits. |
| observer.WaitForCommit(); |
| |
| // Get the fenced frame render frame host again as it has changed after |
| // navigation. |
| child_frames = fenced_frame_test_helper().GetChildFencedFrameHosts( |
| GetWebContents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| fenced_frame_rfh = child_frames[0]; |
| |
| // Get fenced frame NetworkAnonymizationKey after navigation commits. This |
| // is because DNS prefetch uses the NetworkAnonymizationKey from the |
| // IsolationInfo of the pending navigation. So the NetworkAnonymizationKey |
| // used for the checks below needs to be obtained from the new fenced frame |
| // render frame host. |
| const net::NetworkAnonymizationKey& network_anonymization_key = |
| fenced_frame_rfh->GetIsolationInfoForSubresources() |
| .network_anonymization_key(); |
| |
| // The observer should observe a DNS prefetch which succeeds. |
| preconnect_manager_observer()->WaitUntilHostLookedUp( |
| dns_prefetch_url.host(), network_anonymization_key); |
| EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| EXPECT_TRUE(preconnect_manager_observer()->HostFound( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| } |
| |
| // Verify DNS prefetch triggered by link response header is disabled after |
| // fenced frame untrusted network cutoff. |
| IN_PROC_BROWSER_TEST_F(FencedFrameLoadingPredictorBrowserTest, |
| NetworkCutoffDisablesDnsPrefetchFromLinkHeader) { |
| std::string relative_url = "/title1.html"; |
| net::test_server::ControllableHttpResponse dns_prefetch_response( |
| &embedded_https_test_server(), relative_url); |
| |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| |
| // Navigate to a page that contains a fenced frame and a nested iframe. |
| const GURL main_url = embedded_https_test_server().GetURL( |
| "a.test", |
| "/cross_site_iframe_factory.html?a.test(a.test{fenced}(a.test))"); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url)); |
| |
| // Get fenced frame render frame host. |
| std::vector<content::RenderFrameHost*> child_frames = |
| fenced_frame_test_helper().GetChildFencedFrameHosts( |
| GetWebContents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| content::RenderFrameHost* fenced_frame_rfh = child_frames[0]; |
| |
| // Get nested iframe render frame host. |
| content::RenderFrameHost* nested_iframe_rfh = |
| content::ChildFrameAt(fenced_frame_rfh, 0); |
| |
| // Get fenced frame NetworkAnonymizationKey. |
| const net::NetworkAnonymizationKey& network_anonymization_key = |
| fenced_frame_rfh->GetIsolationInfoForSubresources() |
| .network_anonymization_key(); |
| |
| GURL dns_prefetch_url("https://chromium.org"); |
| GURL navigation_url = |
| embedded_https_test_server().GetURL("a.test", relative_url); |
| |
| // Disable fenced frame untrusted network access. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, R"( |
| (async () => { |
| await window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| |
| // Exempt `navigation_url` from fenced frame network revocation. |
| content::test::ExemptUrlsFromFencedFrameNetworkRevocation(fenced_frame_rfh, |
| {navigation_url}); |
| |
| // Navigate the nested iframe. The navigation is allowed because the url has |
| // been exempted from network revocation. |
| content::TestFrameNavigationObserver observer(nested_iframe_rfh); |
| |
| EXPECT_TRUE(ExecJs( |
| fenced_frame_rfh, |
| content::JsReplace("document.getElementsByTagName('iframe')[0].src = $1;", |
| navigation_url))); |
| |
| // Send a response header with link dns-prefetch field. |
| dns_prefetch_response.WaitForRequest(); |
| ResetNetworkState(); |
| ResetPredictorState(); |
| dns_prefetch_response.Send( |
| base::StringPrintf("HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Supports-Loading-Mode: fenced-frame\r\n" |
| "Link: <%s>; rel=dns-prefetch\r\n" |
| "\r\n", |
| dns_prefetch_url.spec().c_str())); |
| dns_prefetch_response.Done(); |
| |
| // Wait until navigation commits. |
| observer.WaitForCommit(); |
| ASSERT_TRUE(WaitForLoadStop(GetWebContents())); |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| // In rare cases, the NetworkHintsHandler will not receive the dns prefetch |
| // IPC call. Then there is no dns prefetch request initiated at all. |
| // `HasHostBeenLookedUp()` is not checked here to avoid flakiness. |
| EXPECT_FALSE(preconnect_manager_observer()->HostFound( |
| dns_prefetch_url.host(), network_anonymization_key)); |
| } |
| |
| } // namespace predictors |