| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_features.h" |
| #include "chrome/browser/predictors/autocomplete_action_predictor.h" |
| #include "chrome/browser/predictors/autocomplete_action_predictor_factory.h" |
| #include "chrome/browser/preloading/chrome_preloading.h" |
| #include "chrome/browser/preloading/prefetch/search_prefetch/field_trial_settings.h" |
| #include "chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service.h" |
| #include "chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_factory.h" |
| #include "chrome/browser/preloading/preloading_prefs.h" |
| #include "chrome/browser/preloading/prerender/prerender_manager.h" |
| #include "chrome/browser/preloading/scoped_prewarm_feature_list.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/chrome_test_utils.h" |
| #include "chrome/test/base/platform_browser_test.h" |
| #include "chrome/test/base/search_test_utils.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/search_engines/template_url_data.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/navigation_handle_observer.h" |
| #include "content/public/test/preloading_test_util.h" |
| #include "content/public/test/prerender_test_util.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/page_transition_types.h" |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/ui/browser.h" |
| #endif |
| |
| namespace { |
| |
| using UkmEntry = ukm::TestUkmRecorder::HumanReadableUkmEntry; |
| using ukm::builders::Preloading_Attempt; |
| |
| } // namespace |
| |
| class OmniboxPrerenderBrowserTest : public PlatformBrowserTest { |
| public: |
| OmniboxPrerenderBrowserTest() |
| : prerender_helper_(base::BindRepeating( |
| &OmniboxPrerenderBrowserTest::GetActiveWebContents, |
| base::Unretained(this))) {} |
| |
| void SetUp() override { |
| prerender_helper_.RegisterServerRequestMonitor(embedded_test_server()); |
| PlatformBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_test_server()->ServeFilesFromDirectory( |
| base::PathService::CheckedGet(chrome::DIR_TEST_DATA)); |
| test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| ukm_entry_builder_ = |
| std::make_unique<content::test::PreloadingAttemptUkmEntryBuilder>( |
| chrome_preloading_predictor::kOmniboxDirectURLInput); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| void TearDownOnMainThread() override { |
| ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| } |
| |
| content::WebContents* GetActiveWebContents() { |
| return chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| content::test::PrerenderTestHelper& prerender_helper() { |
| return prerender_helper_; |
| } |
| |
| Profile* GetProfile() { |
| #if BUILDFLAG(IS_ANDROID) |
| return chrome_test_utils::GetProfile(this); |
| #else |
| return browser()->profile(); |
| #endif |
| } |
| |
| predictors::AutocompleteActionPredictor* GetAutocompleteActionPredictor() { |
| Profile* profile = GetProfile(); |
| return predictors::AutocompleteActionPredictorFactory::GetForProfile( |
| profile); |
| } |
| |
| ukm::TestAutoSetUkmRecorder* test_ukm_recorder() { |
| return test_ukm_recorder_.get(); |
| } |
| |
| const content::test::PreloadingAttemptUkmEntryBuilder& ukm_entry_builder() { |
| return *ukm_entry_builder_; |
| } |
| |
| private: |
| base::ScopedMockElapsedTimersForTest test_timer_; |
| content::test::PrerenderTestHelper prerender_helper_; |
| test::ScopedPrewarmFeatureList scoped_prewarm_feature_list_{ |
| test::ScopedPrewarmFeatureList::PrewarmState::kDisabled}; |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_; |
| std::unique_ptr<content::test::PreloadingAttemptUkmEntryBuilder> |
| ukm_entry_builder_; |
| // Disable sampling of UKM preloading logs. |
| content::test::PreloadingConfigOverride preloading_config_override_; |
| }; |
| |
| // Tests that Prerender2 cannot be triggered when preload setting is disabled. |
| IN_PROC_BROWSER_TEST_F(OmniboxPrerenderBrowserTest, DisableNetworkPrediction) { |
| const GURL kInitialUrl = embedded_test_server()->GetURL("/empty.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| // Disable network prediction. |
| PrefService* prefs = GetProfile()->GetPrefs(); |
| prefetch::SetPreloadPagesState(prefs, |
| prefetch::PreloadPagesState::kNoPreloading); |
| ASSERT_EQ(prefetch::IsSomePreloadingEnabled(*prefs), |
| content::PreloadingEligibility::kPreloadingDisabled); |
| |
| // Navigate to initial URL. |
| ASSERT_TRUE(content::NavigateToURL(web_contents, kInitialUrl)); |
| |
| // Attempt to prerender a direct URL input. |
| auto* predictor = GetAutocompleteActionPredictor(); |
| ASSERT_TRUE(predictor); |
| GURL prerender_url = embedded_test_server()->GetURL("/simple.html"); |
| predictor->StartPrerendering(prerender_url, *web_contents); |
| |
| // Since preload setting is disabled, prerender shouldn't be triggered. |
| base::RunLoop().RunUntilIdle(); |
| content::FrameTreeNodeId host_id = |
| prerender_helper().GetHostForUrl(prerender_url); |
| EXPECT_TRUE(host_id.is_null()); |
| |
| { |
| // Navigate to a different URL other than the prerender_url to flush the |
| // metrics. |
| GURL kUrl_a = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ASSERT_TRUE(content::NavigateToURL(web_contents, kUrl_a)); |
| |
| ukm::SourceId ukm_source_id = |
| web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId(); |
| auto ukm_entries = test_ukm_recorder()->GetEntries( |
| Preloading_Attempt::kEntryName, |
| content::test::kPreloadingAttemptUkmMetrics); |
| EXPECT_EQ(ukm_entries.size(), 1u); |
| |
| UkmEntry expected_entry = ukm_entry_builder().BuildEntry( |
| ukm_source_id, content::PreloadingType::kPrerender, |
| content::PreloadingEligibility::kPreloadingDisabled, |
| content::PreloadingHoldbackStatus::kUnspecified, |
| content::PreloadingTriggeringOutcome::kUnspecified, |
| content::PreloadingFailureReason::kUnspecified, |
| /*accurate=*/false); |
| EXPECT_EQ(ukm_entries[0], expected_entry) |
| << content::test::ActualVsExpectedUkmEntryToString(ukm_entries[0], |
| expected_entry); |
| } |
| |
| // Re-enable the setting. |
| prefetch::SetPreloadPagesState( |
| prefs, prefetch::PreloadPagesState::kStandardPreloading); |
| ASSERT_EQ(prefetch::IsSomePreloadingEnabled(*prefs), |
| content::PreloadingEligibility::kEligible); |
| |
| content::test::PrerenderHostRegistryObserver registry_observer(*web_contents); |
| // Attempt to trigger prerendering again. |
| predictor->StartPrerendering(prerender_url, *web_contents); |
| |
| // Since preload setting is enabled, prerender should be triggered |
| // successfully. |
| registry_observer.WaitForTrigger(prerender_url); |
| host_id = prerender_helper().GetHostForUrl(prerender_url); |
| EXPECT_TRUE(host_id); |
| |
| { |
| // Navigate to prerender_url. |
| content::NavigationHandleObserver activation_observer(web_contents, |
| prerender_url); |
| ASSERT_TRUE(content::NavigateToURL(web_contents, prerender_url)); |
| |
| ukm::SourceId ukm_source_id = activation_observer.next_page_ukm_source_id(); |
| auto ukm_entries = test_ukm_recorder()->GetEntries( |
| Preloading_Attempt::kEntryName, |
| content::test::kPreloadingAttemptUkmMetrics); |
| |
| // There should be 2 entries after we attempt Prerender again. |
| EXPECT_EQ(ukm_entries.size(), 2u); |
| |
| UkmEntry expected_entry = ukm_entry_builder().BuildEntry( |
| ukm_source_id, content::PreloadingType::kPrerender, |
| content::PreloadingEligibility::kEligible, |
| content::PreloadingHoldbackStatus::kAllowed, |
| content::PreloadingTriggeringOutcome::kSuccess, |
| content::PreloadingFailureReason::kUnspecified, |
| /*accurate=*/true, |
| /*ready_time=*/base::ScopedMockElapsedTimersForTest::kMockElapsedTime); |
| EXPECT_EQ(ukm_entries[1], expected_entry) |
| << content::test::ActualVsExpectedUkmEntryToString(ukm_entries[1], |
| expected_entry); |
| } |
| } |
| |
| class PrerenderOmniboxSearchSuggestionBrowserTest |
| : public OmniboxPrerenderBrowserTest { |
| public: |
| void SetUp() override { |
| prerender_helper().RegisterServerRequestMonitor(&search_engine_server_); |
| PlatformBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| OmniboxPrerenderBrowserTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| search_engine_server_.SetSSLConfig( |
| net::test_server::EmbeddedTestServer::CERT_TEST_NAMES); |
| search_engine_server_.ServeFilesFromDirectory( |
| base::PathService::CheckedGet(chrome::DIR_TEST_DATA)); |
| ASSERT_TRUE(search_engine_server_.Start()); |
| SetUrlTemplate(); |
| } |
| |
| void SetNewUrlTemplate(const std::string& prerender_page_target) { |
| prerender_page_target_ = prerender_page_target; |
| SetUrlTemplate(); |
| } |
| |
| protected: |
| GURL GetCanonicalSearchURL(const GURL& prefetch_url) { |
| GURL canonical_search_url; |
| HasCanonicalPreloadingOmniboxSearchURL(prefetch_url, |
| chrome_test_utils::GetProfile(this), |
| &canonical_search_url); |
| return canonical_search_url; |
| } |
| |
| void PrerenderQuery(const std::string& search_terms) { |
| auto* search_prefetch_service = |
| SearchPrefetchServiceFactory::GetForProfile(GetProfile()); |
| AutocompleteMatch match = CreateSearchSuggestionMatch(search_terms); |
| auto* template_url_service = |
| TemplateURLServiceFactory::GetForProfile(GetProfile()); |
| search_prefetch_service->CoordinatePrefetchWithPrerender( |
| match, GetActiveWebContents(), template_url_service, |
| GetCanonicalSearchURL(match.destination_url)); |
| } |
| |
| GURL GetSearchSuggestionUrl(const std::string& search_terms, |
| bool with_parameter) { |
| std::string url_template = prerender_page_target_ + "?q=$1$2&type=test"; |
| return search_engine_server_.GetURL( |
| kSearchDomain, |
| base::ReplaceStringPlaceholders( |
| url_template, {search_terms, with_parameter ? "&pf=cs" : ""}, |
| nullptr)); |
| } |
| |
| void InitializePrerenderManager() { |
| PrerenderManager::CreateForWebContents(GetActiveWebContents()); |
| |
| prerender_manager_ = |
| PrerenderManager::FromWebContents(GetActiveWebContents()); |
| ASSERT_TRUE(prerender_manager_); |
| } |
| |
| void PrerenderAndActivate(const std::string& search_terms) { |
| PrerenderQuery(search_terms); |
| GURL prerendered_url = |
| GetSearchSuggestionUrl(search_terms, /*with_parameter=*/false); |
| prerender_helper().WaitForPrerenderLoadCompletion(*GetActiveWebContents(), |
| prerendered_url); |
| prerender_helper().NavigatePrimaryPage( |
| prerendered_url, |
| ui::PageTransitionFromInt(ui::PAGE_TRANSITION_GENERATED | |
| ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)); |
| EXPECT_EQ(GetActiveWebContents()->GetLastCommittedURL(), prerendered_url); |
| } |
| |
| PrerenderManager* prerender_manager() { return prerender_manager_; } |
| |
| private: |
| AutocompleteMatch CreateSearchSuggestionMatch( |
| const std::string& search_terms) { |
| AutocompleteMatch match; |
| match.search_terms_args = std::make_unique<TemplateURLRef::SearchTermsArgs>( |
| base::UTF8ToUTF16(search_terms)); |
| match.search_terms_args->original_query = base::UTF8ToUTF16(search_terms); |
| match.destination_url = |
| GetSearchSuggestionUrl(search_terms, /*with_parameter=*/true); |
| match.keyword = base::UTF8ToUTF16(search_terms); |
| match.RecordAdditionalInfo("should_prerender", "true"); |
| return match; |
| } |
| |
| void SetUrlTemplate() { |
| TemplateURLService* model = TemplateURLServiceFactory::GetForProfile( |
| chrome_test_utils::GetProfile(this)); |
| ASSERT_TRUE(model); |
| search_test_utils::WaitForTemplateURLServiceToLoad(model); |
| ASSERT_TRUE(model->loaded()); |
| TemplateURLData data; |
| data.SetShortName(kSearchDomain16); |
| data.SetKeyword(data.short_name()); |
| data.SetURL(search_engine_server_ |
| .GetURL(kSearchDomain, |
| prerender_page_target_ + |
| "?q={searchTerms}&{google:assistedQueryStats}{" |
| "google:prefetchSource}type=test") |
| .spec()); |
| TemplateURL* template_url = model->Add(std::make_unique<TemplateURL>(data)); |
| ASSERT_TRUE(template_url); |
| model->SetUserSelectedDefaultSearchProvider(template_url); |
| } |
| |
| constexpr static char kSearchDomain[] = "a.test"; |
| constexpr static char16_t kSearchDomain16[] = u"a.test"; |
| raw_ptr<PrerenderManager, AcrossTasksDanglingUntriaged> prerender_manager_; |
| net::test_server::EmbeddedTestServer search_engine_server_{ |
| net::test_server::EmbeddedTestServer::TYPE_HTTPS}; |
| std::string prerender_page_target_ = "/title1.html"; |
| }; |
| |
| class PrerenderOmniboxSearchSuggestionExpiryBrowserTest |
| : public PrerenderOmniboxSearchSuggestionBrowserTest { |
| public: |
| PrerenderOmniboxSearchSuggestionExpiryBrowserTest() { |
| feature_list_.InitWithFeaturesAndParameters( |
| {{kSearchPrefetchServicePrefetching, |
| {{"device_memory_threshold_MB", "0"}}}}, |
| {}); |
| } |
| |
| // TODO(crbug.com/40285326): This fails with the field trial testing config. |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| PrerenderOmniboxSearchSuggestionBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch("disable-field-trial-config"); |
| } |
| |
| protected: |
| void PrerenderQueryAndWaitForExpiring(const std::string& search_terms) { |
| content::test::PrerenderHostRegistryObserver registry_observer( |
| *GetActiveWebContents()); |
| PrerenderQuery(search_terms); |
| GURL prerendered_url = |
| GetSearchSuggestionUrl(search_terms, /*with_parameter=*/false); |
| registry_observer.WaitForTrigger(prerendered_url); |
| |
| content::FrameTreeNodeId host_id = |
| prerender_helper().GetHostForUrl(prerendered_url); |
| ASSERT_TRUE(host_id); |
| content::test::PrerenderHostObserver prerender_observer( |
| *GetActiveWebContents(), host_id); |
| |
| // The prerender will be destroyed automatically for expiry. |
| auto* search_prefetch_service = |
| SearchPrefetchServiceFactory::GetForProfile(GetProfile()); |
| search_prefetch_service->FireAllExpiryTimerForTesting(); |
| prerender_observer.WaitForDestroyed(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that an ongoing prerender which loads an SRP should be canceled |
| // automatically after the expiry duration. |
| IN_PROC_BROWSER_TEST_F(PrerenderOmniboxSearchSuggestionExpiryBrowserTest, |
| SearchPrerenderExpiry) { |
| base::HistogramTester histogram_tester; |
| const GURL kInitialUrl = embedded_test_server()->GetURL("/empty.html"); |
| ASSERT_TRUE(GetActiveWebContents()); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), kInitialUrl)); |
| InitializePrerenderManager(); |
| |
| std::string search_query = "prerender2"; |
| PrerenderQueryAndWaitForExpiring("prerender222"); |
| |
| histogram_tester.ExpectUniqueSample( |
| "Prerender.Experimental.PrerenderHostFinalStatus.Embedder_" |
| "DefaultSearchEngine", |
| /*PrerenderFinalStatus::kTriggerDestroyed*/ 16, 1); |
| |
| // The prediction should be treated as cancelled. |
| histogram_tester.ExpectUniqueSample( |
| internal::kHistogramPrerenderPredictionStatusDefaultSearchEngine, |
| PrerenderPredictionStatus::kCancelled, 1); |
| } |
| |
| // Tests that kCanceled is correctly recorded in the case that PrerenderManager |
| // receives a new suggestion. Note: kCancel should only recorded when |
| // PrerenderManager receives a new suggestion or on primary-page changed. |
| // Otherwise one prediction might be recorded twice. |
| IN_PROC_BROWSER_TEST_F(PrerenderOmniboxSearchSuggestionExpiryBrowserTest, |
| DifferentSuggestionAfterPrerenderExpired) { |
| base::HistogramTester histogram_tester; |
| const GURL kInitialUrl = embedded_test_server()->GetURL("/empty.html"); |
| ASSERT_TRUE(GetActiveWebContents()); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), kInitialUrl)); |
| InitializePrerenderManager(); |
| |
| // Prerender the first query, and wait for it to be deleted. |
| PrerenderQueryAndWaitForExpiring("prerender222"); |
| |
| histogram_tester.ExpectUniqueSample( |
| "Prerender.Experimental.PrerenderHostFinalStatus.Embedder_" |
| "DefaultSearchEngine", |
| /*PrerenderFinalStatus::kTriggerDestroyed*/ 16, 1); |
| |
| // The prediction should be treated as cancelled. |
| histogram_tester.ExpectUniqueSample( |
| internal::kHistogramPrerenderPredictionStatusDefaultSearchEngine, |
| PrerenderPredictionStatus::kCancelled, 1); |
| |
| // Prerender another term and activate it. |
| PrerenderAndActivate("prerender233"); |
| |
| // The prediction is correct, so kHitFinished should be recorded. |
| histogram_tester.ExpectBucketCount( |
| internal::kHistogramPrerenderPredictionStatusDefaultSearchEngine, |
| PrerenderPredictionStatus::kHitFinished, 1); |
| |
| // Two predictions, two samples. |
| histogram_tester.ExpectTotalCount( |
| internal::kHistogramPrerenderPredictionStatusDefaultSearchEngine, 2); |
| } |