blob: b2be24a305b44f61726cdd890084394cf212c44b [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/prerender/model/prerender_browser_agent.h"
#import "base/check_deref.h"
#import "base/memory/raw_ptr.h"
#import "base/types/cxx23_to_underlying.h"
#import "components/prefs/pref_service.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/identity_manager/account_info.h"
#import "components/signin/public/identity_manager/identity_test_utils.h"
#import "components/supervised_user/test_support/supervised_user_signin_test_utils.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/identity_test_environment_browser_state_adaptor.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "net/base/mock_network_change_notifier.h"
#import "net/base/network_change_notifier.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
namespace {
// List all content world for which a TestWebFramesManager should be installed.
static const web::ContentWorld kAllContentWorlds[] = {
web::ContentWorld::kAllContentWorlds,
web::ContentWorld::kPageContentWorld,
web::ContentWorld::kIsolatedWorld,
};
// Test double implementation of NavigationManager.
class TestNavigationManager : public web::FakeNavigationManager {
public:
explicit TestNavigationManager(web::FakeWebState* web_state)
: web::FakeNavigationManager(), web_state_(CHECK_DEREF(web_state)) {
SetBrowserState(web_state_->GetBrowserState());
}
// web::NavigationManager implementation.
void LoadURLWithParams(const WebLoadParams& load_params) override {
web::FakeNavigationManager::LoadURLWithParams(load_params);
web_state_->SetVisibleURL(load_params.url);
}
private:
const raw_ref<web::FakeWebState> web_state_;
};
// Test double implementation of WebState.
class TestWebState : public web::FakeWebState {
public:
explicit TestWebState(ProfileIOS* profile)
: FakeWebState(web::WebStateID::NewUnique()) {
SetBrowserState(profile);
SetNavigationManager(std::make_unique<TestNavigationManager>(this));
SetWebFramesManager(std::make_unique<web::FakeWebFramesManager>());
for (auto content_world : kAllContentWorlds) {
SetWebFramesManager(content_world,
std::make_unique<web::FakeWebFramesManager>());
}
}
// web::WebState implementation.
std::unique_ptr<web::WebState> Clone() const override {
return std::make_unique<TestWebState>(
ProfileIOS::FromBrowserState(GetBrowserState()));
}
};
// NetworkChangeObserver that invoke a callback when the connection type
// changes.
class ScopedNetworkChangeObserver
: public net::NetworkChangeNotifier::NetworkChangeObserver {
public:
explicit ScopedNetworkChangeObserver(base::RepeatingClosure closure)
: closure_(std::move(closure)) {
net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
}
~ScopedNetworkChangeObserver() override {
net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
}
// net::NetworkChangeNotifier::NetworkChangeObserver implementation.
void OnNetworkChanged(
net::NetworkChangeNotifier::ConnectionType type) override {
closure_.Run();
}
private:
base::RepeatingClosure closure_;
};
// Converts NetworkPredictionSetting to a string.
std::string_view ToString(prerender_prefs::NetworkPredictionSetting value) {
switch (value) {
case prerender_prefs::NetworkPredictionSetting::kDisabled:
return "Disabled";
case prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly:
return "WiFi only";
case prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular:
return "Always";
}
}
// Converts net::NetworkChangeNotifier::ConnectionType to a string.
std::string_view ToString(net::NetworkChangeNotifier::ConnectionType value) {
switch (value) {
case net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI:
return "WiFi";
case net::NetworkChangeNotifier::ConnectionType::CONNECTION_2G:
case net::NetworkChangeNotifier::ConnectionType::CONNECTION_3G:
case net::NetworkChangeNotifier::ConnectionType::CONNECTION_4G:
case net::NetworkChangeNotifier::ConnectionType::CONNECTION_5G:
return "Cellular";
default:
return "Unknown";
}
}
} // namespace
class PrerenderBrowserAgentTest : public PlatformTest {
public:
PrerenderBrowserAgentTest() {
// Setup a mock NetworkChangeNotifier to simulate different network
// connection types.
network_change_notifier_ = net::test::MockNetworkChangeNotifier::Create();
TestProfileIOS::Builder builder;
builder.AddTestingFactory(
IdentityManagerFactory::GetInstance(),
base::BindRepeating(IdentityTestEnvironmentBrowserStateAdaptor::
BuildIdentityManagerForTests));
profile_ = std::move(builder).Build();
browser_ = std::make_unique<TestBrowser>(profile_.get());
PrerenderBrowserAgent::CreateForBrowser(browser_.get());
// Prerendering clones the active WebState from the Browser's WebStateList
// or fail if there is none. Insert a WebState to avoid this failure state.
browser_->GetWebStateList()->InsertWebState(
std::make_unique<TestWebState>(profile_.get()),
WebStateList::InsertionParams::Automatic().Activate());
}
// Returns the PrerenderBrowserAgent instance.
PrerenderBrowserAgent* agent() {
return PrerenderBrowserAgent::FromBrowser(browser_.get());
}
// Returns the WebStateList.
WebStateList* web_state_list() { return browser_->GetWebStateList(); }
// Returns the ProfileIOS.
ProfileIOS* profile() { return profile_.get(); }
// Helper that start prerender and wait until the PrerenderBrowserAgent
// has processed the request (even if the policy is kNoDelay, the request
// is only processed on the next run of the RunLoop).
void StartPrerender(const GURL& url, ui::PageTransition transition) {
agent()->StartPrerender(url, web::Referrer(), transition,
PrerenderBrowserAgent::PrerenderPolicy::kNoDelay);
// Give some time to PrerenderBrowserAgent to start the prerendering.
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::TimeDelta());
run_loop.Run();
}
// Set "NetworkPredictionSetting" preference.
void SetNetworkPredictionSetting(
prerender_prefs::NetworkPredictionSetting value) {
profile_->GetPrefs()->SetInteger(prefs::kNetworkPredictionSetting,
base::to_underlying(value));
}
// Set "NetworkPredictionSetting" to "Enabled on WiFi & Cellular"
void PrerenderEnabledOnWiFiAndCellular() {
SetNetworkPredictionSetting(
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular);
}
// Set "NetworkPredictionSetting" to "Enabled on WiFi Only"
void PrerenderEnabledOnWiFiOnly() {
SetNetworkPredictionSetting(
prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly);
}
// Set "NetworkPredictionSetting" to "Disabled"
void PrerenderDisabled() {
SetNetworkPredictionSetting(
prerender_prefs::NetworkPredictionSetting::kDisabled);
}
// Simulate network connection type.
void SimulateNetworkConnectionType(
net::NetworkChangeNotifier::ConnectionType type) {
if (network_change_notifier_->GetCurrentConnectionType() == type) {
return;
}
base::RunLoop run_loop;
ScopedNetworkChangeObserver observer(run_loop.QuitClosure());
network_change_notifier_->SetConnectionTypeAndNotifyObservers(type);
run_loop.Run();
}
// Simulate a "WiFi" network connection.
void SimulateWiFiConnection() {
SimulateNetworkConnectionType(
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI);
}
// Simulate a "Cellular" network connection.
void SimulateCellularConnection() {
SimulateNetworkConnectionType(
net::NetworkChangeNotifier::ConnectionType::CONNECTION_5G);
}
// Simulate an offline network connection.
void SimulateOffline() {
SimulateNetworkConnectionType(
net::NetworkChangeNotifier::ConnectionType::CONNECTION_NONE);
}
private:
web::WebTaskEnvironment task_environment_;
IOSChromeScopedTestingLocalState scoped_testing_local_state_;
std::unique_ptr<net::test::MockNetworkChangeNotifier>
network_change_notifier_;
std::unique_ptr<TestProfileIOS> profile_;
std::unique_ptr<TestBrowser> browser_;
};
// Check that StartPrerender works as expect.
TEST_F(PrerenderBrowserAgentTest, StartPrerender) {
constexpr struct TestCase {
std::string_view url;
prerender_prefs::NetworkPredictionSetting pref;
net::NetworkChangeNotifier::ConnectionType type;
bool expected;
} kTestCases[] = {
// HTTPS, Prerender enabled on WiFi & Cellular.
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI,
true,
},
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_5G,
true,
},
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_NONE,
false,
},
// HTTPS, Prerender enabled on WiFi Only.
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI,
true,
},
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_5G,
false,
},
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_NONE,
false,
},
// HTTPS, Prerender disabled.
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kDisabled,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI,
false,
},
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kDisabled,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_5G,
false,
},
{
"https://www.google.com/",
prerender_prefs::NetworkPredictionSetting::kDisabled,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_NONE,
false,
},
// Do not pre-render non-HTTP & non-HTTPS URLs.
{
"",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI,
false,
},
{
"chrome://newtab",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI,
false,
},
{
"about:flags",
prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular,
net::NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI,
false,
},
};
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
for (const auto& test_case : kTestCases) {
SetNetworkPredictionSetting(test_case.pref);
SimulateNetworkConnectionType(test_case.type);
// Replace the active WebState with a new WebState (since prerender
// is not enabled if the current WebState is already displaying the
// requested url).
WebStateList* list = web_state_list();
const int active_index = list->active_index();
list->ReplaceWebStateAt(active_index,
std::make_unique<TestWebState>(profile()));
StartPrerender(GURL(test_case.url), transition);
EXPECT_EQ(test_case.expected,
agent()->ValidatePrerender(GURL(test_case.url), transition))
<< " Where url = \"" << test_case.url << "\", pref = \""
<< ToString(test_case.pref) << "\", connection = \""
<< ToString(test_case.type) << "\"";
}
}
// Check that prerender is cancelled if enabled only on WiFi and the
// connection type changes from WiFi to Cellular.
TEST_F(PrerenderBrowserAgentTest, Cancel_ConnectionWiFiToCellular) {
PrerenderEnabledOnWiFiOnly();
SimulateWiFiConnection();
const GURL url("https://www.google.com/");
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
StartPrerender(url, transition);
// Change connection from WiFi to Cellular. This should cancel prerender.
SimulateCellularConnection();
EXPECT_FALSE(agent()->ValidatePrerender(url, transition));
}
// Check that prerender is cancelled if enabled only on WiFi and the
// connection type changes from WiFi to no connection.
TEST_F(PrerenderBrowserAgentTest, Cancel_ConnectionWiFiToOffline) {
PrerenderEnabledOnWiFiOnly();
SimulateWiFiConnection();
const GURL url("https://www.google.com/");
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
StartPrerender(url, transition);
// Change connection from WiFi to none. This should cancel prerender.
SimulateOffline();
EXPECT_FALSE(agent()->ValidatePrerender(url, transition));
}
// Check that prerender is cancelled if the connection is ceullular and the
// connection settings is changed from "WiFi & Cellular" to "WiFi only".
TEST_F(PrerenderBrowserAgentTest, Cancel_EnabledWifiAndCellularToWiFiOnly) {
PrerenderEnabledOnWiFiAndCellular();
SimulateCellularConnection();
const GURL url("https://www.google.com/");
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
StartPrerender(url, transition);
// Change setting from "Wifi & Cellular" to "WiFi Only". This should cancel
// the prerender.
PrerenderEnabledOnWiFiOnly();
EXPECT_FALSE(agent()->ValidatePrerender(url, transition));
}
// Check that prerender is cancelled if the connection is ceullular and the
// connection settings is changed from "WiFi & Cellular" to "Never".
TEST_F(PrerenderBrowserAgentTest, Cancel_EnabledWifiAndCellularToDisabled) {
PrerenderEnabledOnWiFiAndCellular();
SimulateCellularConnection();
const GURL url("https://www.google.com/");
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
StartPrerender(url, transition);
// Change setting from "Wifi & Cellular" to "Never". This should cancel
// the prerender.
PrerenderDisabled();
EXPECT_FALSE(agent()->ValidatePrerender(url, transition));
}
// Check that prerender is disabled if the WebStateList has no active WebState.
TEST_F(PrerenderBrowserAgentTest, DisabledIfNoActiveWebState) {
WebStateList* list = web_state_list();
CloseAllWebStates(CHECK_DEREF(list), WebStateList::ClosingReason::kDefault);
ASSERT_TRUE(list->empty());
PrerenderEnabledOnWiFiAndCellular();
SimulateWiFiConnection();
const GURL url("https://www.google.com/");
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
StartPrerender(url, transition);
EXPECT_FALSE(agent()->ValidatePrerender(url, transition));
}
// Check that prerender is disabled for supervised users.
TEST_F(PrerenderBrowserAgentTest, DisabledForSupervisedUsers) {
auto* identity_manager = IdentityManagerFactory::GetForProfile(profile());
AccountInfo account = signin::MakePrimaryAccountAvailable(
identity_manager, "test@gmail.com", signin::ConsentLevel::kSignin);
supervised_user::UpdateSupervisionStatusForAccount(
account, identity_manager, /*is_subject_to_parental_controls=*/true);
PrerenderEnabledOnWiFiAndCellular();
SimulateWiFiConnection();
const GURL url("https://www.google.com/");
const auto transition = ui::PageTransition::PAGE_TRANSITION_TYPED;
StartPrerender(url, transition);
EXPECT_FALSE(agent()->ValidatePrerender(url, transition));
}