blob: 8ca5431afdea3b3bf3184cc012dab40287cd2893 [file] [log] [blame]
// 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/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/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/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/test/base/android/android_browser_test.h"
#else
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.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);
test_timer_ = std::make_unique<base::ScopedMockElapsedTimersForTest>();
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:
content::test::PrerenderTestHelper prerender_helper_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
std::unique_ptr<content::test::PreloadingAttemptUkmEntryBuilder>
ukm_entry_builder_;
std::unique_ptr<base::ScopedMockElapsedTimersForTest> test_timer_;
// 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, gfx::Size(50, 50));
// Since preload setting is disabled, prerender shouldn't be triggered.
base::RunLoop().RunUntilIdle();
int host_id = prerender_helper().GetHostForUrl(prerender_url);
EXPECT_EQ(host_id, content::RenderFrameHost::kNoFrameTreeNodeId);
{
// 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, gfx::Size(50, 50));
// Since preload setting is enabled, prerender should be triggered
// successfully.
registry_observer.WaitForTrigger(prerender_url);
host_id = prerender_helper().GetHostForUrl(prerender_url);
EXPECT_NE(host_id, content::RenderFrameHost::kNoFrameTreeNodeId);
{
// 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:
PrerenderOmniboxSearchSuggestionBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
{{features::kSupportSearchSuggestionForPrerender2,
{
{"implementation_type", "use_prefetch"},
}}},
{});
}
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;
HasCanoncialPreloadingOmniboxSearchURL(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 NavigateToPrerenderedResult(const GURL& expected_prerender_url) {
content::TestNavigationObserver observer(GetActiveWebContents());
GetActiveWebContents()->OpenURL(content::OpenURLParams(
expected_prerender_url, content::Referrer(),
WindowOpenDisposition::CURRENT_TAB,
ui::PageTransitionFromInt(ui::PAGE_TRANSITION_GENERATED |
ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
/*is_renderer_initiated=*/false));
observer.Wait();
}
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);
NavigateToPrerenderedResult(prerendered_url);
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: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";
base::test::ScopedFeatureList feature_list_;
};
class PrerenderOmniboxSearchSuggestionReloadBrowserTest
: public PrerenderOmniboxSearchSuggestionBrowserTest {
public:
PrerenderOmniboxSearchSuggestionReloadBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
{{features::kSupportSearchSuggestionForPrerender2,
{
{"implementation_type", "use_prefetch"},
}},
{kSearchPrefetchServicePrefetching,
{{"device_memory_threshold_MB", "0"}}}},
// Disable BFCache, to test the HTTP Cache path.
{features::kBackForwardCache});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test back or forward navigations can use the HTTP Cache.
IN_PROC_BROWSER_TEST_F(PrerenderOmniboxSearchSuggestionReloadBrowserTest,
BackNavigationHitsHttpCache) {
base::HistogramTester histogram_tester;
const GURL kInitialUrl = embedded_test_server()->GetURL("/empty.html");
ASSERT_TRUE(GetActiveWebContents());
ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), kInitialUrl));
InitializePrerenderManager();
// 1. Prerender the first page.
std::string search_terms_1 = "prerender2222";
GURL expected_prefetched_url_1 =
GetSearchSuggestionUrl(search_terms_1, /*with_parameter=*/true);
GURL expected_activated_url_1 =
GetSearchSuggestionUrl(search_terms_1, /*with_parameter=*/false);
PrerenderAndActivate(search_terms_1);
EXPECT_EQ(0, prerender_helper().GetRequestCount(expected_activated_url_1));
EXPECT_EQ(1, prerender_helper().GetRequestCount(expected_prefetched_url_1));
// 2. Prerender and activate another page.
std::string search_terms_2 = "prefetch233";
GURL expected_prefetched_url_2 =
GetSearchSuggestionUrl(search_terms_2, /*with_parameter=*/true);
GURL expected_activated_url_2 =
GetSearchSuggestionUrl(search_terms_2, /*with_parameter=*/false);
PrerenderAndActivate(search_terms_2);
EXPECT_EQ(0, prerender_helper().GetRequestCount(expected_activated_url_2));
EXPECT_EQ(1, prerender_helper().GetRequestCount(expected_prefetched_url_2));
// 3. Navigate back. Chrome is supposed to read the response from the cache,
// instead of sending another request.
content::TestNavigationObserver back_load_observer(GetActiveWebContents());
GetActiveWebContents()->GetController().GoBack();
back_load_observer.Wait();
EXPECT_EQ(expected_activated_url_1,
GetActiveWebContents()->GetLastCommittedURL());
EXPECT_EQ(0, prerender_helper().GetRequestCount(expected_activated_url_1));
EXPECT_EQ(1, prerender_helper().GetRequestCount(expected_prefetched_url_1));
}
class PrerenderOmniboxSearchSuggestionExpiryBrowserTest
: public PrerenderOmniboxSearchSuggestionBrowserTest {
public:
PrerenderOmniboxSearchSuggestionExpiryBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
{{features::kSupportSearchSuggestionForPrerender2,
{
{"implementation_type", "use_prefetch"},
}},
{kSearchPrefetchServicePrefetching,
{{"device_memory_threshold_MB", "0"}}}},
{});
}
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);
int host_id = prerender_helper().GetHostForUrl(prerendered_url);
ASSERT_NE(host_id, content::RenderFrameHost::kNoFrameTreeNodeId);
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);
// Select the prerender hint. The prerendered result has been deleted, so
// browser loads the search result over again.
content::TestNavigationObserver observer(GetActiveWebContents());
GetActiveWebContents()->OpenURL(content::OpenURLParams(
GetSearchSuggestionUrl("prerender222", /*with_parameter=*/false),
content::Referrer(), WindowOpenDisposition::CURRENT_TAB,
ui::PageTransitionFromInt(ui::PAGE_TRANSITION_GENERATED |
ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
/*is_renderer_initiated=*/false));
observer.Wait();
// This metric is recorded only when prerendering is alive on primary page
// changed.
histogram_tester.ExpectTotalCount(
"Prerender.Experimental.Search."
"FirstCorrectPrerenderHintReceivedToRealSearchNavigationStartedDuration",
0);
}
// 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);
}