0
Files
src/content/browser/back_forward_cache_browsertest.cc
Sandor Major a9a29ad57e Move Permissions Policy switch and flags to the Network service
Permissions Policy code is being migrated out of Blink, to the Network
service. The code depends on these flags so it needs to be moved.

Bug: 382291442
Change-Id: I4e2bbb4d4c49392f0e845096cc8c62ae9ae9fb20
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6282334
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Reviewed-by: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Owen Min <zmin@chromium.org>
Commit-Queue: Sandor «Alex» Major <sandormajor@chromium.org>
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1422574}
2025-02-20 08:00:14 -08:00

3704 lines
151 KiB
C++

// 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 "content/browser/back_forward_cache_browsertest.h"
#include <climits>
#include <optional>
#include <string_view>
#include <unordered_map>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/metrics_hashes.h"
#include "base/run_loop.h"
#include "base/system/sys_info.h"
#include "base/task/common/task_annotator.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_log.h"
#include "build/build_config.h"
#include "build/chromecast_buildflags.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/bad_message.h"
#include "content/browser/renderer_host/back_forward_cache_can_store_document_result.h"
#include "content/browser/renderer_host/back_forward_cache_disable.h"
#include "content/browser/renderer_host/back_forward_cache_impl.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/should_swap_browsing_instance.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/document_service.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/result_codes.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/commit_message_delayer.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_navigation_throttle.h"
#include "content/public/test/test_navigation_throttle_inserter.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/text_input_test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/public/test/web_contents_observer_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_javascript_dialog_manager.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "media/base/media_switches.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/filename_util.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/cpp/features.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/device_memory/approximated_device_memory.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/blink/public/mojom/back_forward_cache_not_restored_reasons.mojom.h"
#include "third_party/blink/public/mojom/frame/back_forward_cache_controller.mojom.h"
#include "third_party/blink/public/mojom/frame/frame.mojom.h"
#include "third_party/blink/public/mojom/render_accessibility.mojom.h"
#include "third_party/blink/public/mojom/script_source_location.mojom.h"
// This file has too many tests.
//
// Before adding new tests to this file, consider if they will fit better into
// one of the other back_forward_cache_*_browsertest.cc files or if there are
// enough new tests to justify a new file.
using testing::_;
using testing::Each;
using testing::ElementsAre;
using testing::Not;
using testing::UnorderedElementsAreArray;
namespace content {
using NotRestoredReasons =
BackForwardCacheCanStoreDocumentResult::NotRestoredReasons;
using NotRestoredReason = BackForwardCacheMetrics::NotRestoredReason;
namespace {
class DOMContentLoadedObserver : public WebContentsObserver {
public:
explicit DOMContentLoadedObserver(RenderFrameHostImpl* render_frame_host)
: WebContentsObserver(
WebContents::FromRenderFrameHost(render_frame_host)),
render_frame_host_(render_frame_host) {}
void DOMContentLoaded(RenderFrameHost* render_frame_host) override {
if (render_frame_host_ == render_frame_host)
run_loop_.Quit();
}
[[nodiscard]] bool Wait() {
if (render_frame_host_->IsDOMContentLoaded())
run_loop_.Quit();
run_loop_.Run();
return render_frame_host_->IsDOMContentLoaded();
}
private:
raw_ptr<RenderFrameHostImpl> render_frame_host_;
base::RunLoop run_loop_;
};
} // namespace
bool WaitForDOMContentLoaded(RenderFrameHostImpl* rfh) {
DOMContentLoadedObserver observer(rfh);
return observer.Wait();
}
EvalJsResult GetLocalStorage(RenderFrameHostImpl* rfh, std::string key) {
return EvalJs(rfh, JsReplace("localStorage.getItem($1)", key));
}
[[nodiscard]] bool WaitForLocalStorage(RenderFrameHostImpl* rfh,
std::string key,
std::string expected_value) {
auto value = EvalJs(rfh, JsReplace(R"(
new Promise((resolve) => {
let key = $1;
let expected_value = $2;
if (localStorage.getItem(key) == expected_value) {
resolve(localStorage.getItem(key));
return;
}
let listener = window.addEventListener("storage", e => {
if (e.storageArea == localStorage && e.key == key
&& e.newValue == expected_value) {
resolve(localStorage.getItem(key));
removeEventListener("storage", listener);
return;
}
});
});
)",
key, expected_value));
return value == expected_value;
}
BackForwardCacheBrowserTest::BackForwardCacheBrowserTest() = default;
BackForwardCacheBrowserTest::~BackForwardCacheBrowserTest() {
if (fail_for_unexpected_messages_while_cached_) {
// If this is triggered, see MojoInterfaceName in
// tools/metrics/histograms/metadata/navigation/enums.xml for which values
// correspond which messages.
std::vector<base::Bucket> samples = histogram_tester().GetAllSamples(
"BackForwardCache.UnexpectedRendererToBrowserMessage."
"InterfaceName");
// TODO(crbug.com/40244391): Remove this.
// This bucket corresponds to the LocalFrameHost interface. It is known to
// be flaky due calls to `LocalFrameHost::DidFocusFrame()` after entering
// BFCache. So we ignore it for now by removing it if it's present until we
// can fix the root cause.
// TODO(crbug.com/40925798): Remove this.
// As above but `LocalMainFrameHost::DidFirstVisuallyNonEmptyPaint()`.
std::erase_if(samples, [](base::Bucket bucket) {
return bucket.min ==
static_cast<base::HistogramBase::Sample32>(base::HashMetricName(
blink::mojom::LocalFrameHost::Name_)) ||
bucket.min ==
static_cast<base::HistogramBase::Sample32>(base::HashMetricName(
blink::mojom::LocalMainFrameHost::Name_));
});
EXPECT_THAT(samples, testing::ElementsAre());
}
}
void BackForwardCacheBrowserTest::NotifyNotRestoredReasons(
std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree_result) {
tree_result_ = std::move(tree_result);
}
void BackForwardCacheBrowserTest::SetUpCommandLine(
base::CommandLine* command_line) {
mock_cert_verifier_.SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kUseFakeUIForMediaStream);
command_line->AppendSwitch(switches::kEnableExperimentalWebPlatformFeatures);
// TODO(sreejakshetty): Initialize ScopedFeatureLists from test constructor.
EnableFeatureAndSetParams(features::kBackForwardCacheTimeToLiveControl,
"time_to_live_seconds", "3600");
// Entry to the cache can be slow during testing and cause flakiness.
DisableFeature(features::kBackForwardCacheEntryTimeout);
EnableFeatureAndSetParams(features::kBackForwardCache,
"message_handling_when_cached", "log");
EnableFeatureAndSetParams(
blink::features::kLogUnexpectedIPCPostedToBackForwardCachedDocuments,
"delay_before_tracking_ms", "0");
// Allow unlimited network during tests. Override this if you want to test the
// network limiting.
EnableFeatureAndSetParams(blink::features::kLoadingTasksUnfreezable,
"max_buffered_bytes_per_process",
base::NumberToString(INT_MAX));
EnableFeatureAndSetParams(blink::features::kLoadingTasksUnfreezable,
"grace_period_to_finish_loading_in_seconds",
base::NumberToString(INT_MAX));
// Enable capturing not-restored-reasons tree.
EnableFeatureAndSetParams(
blink::features::kBackForwardCacheSendNotRestoredReasons, "", "");
// Do not trigger DumpWithoutCrashing() for JavaScript execution.
DisableFeature(blink::features::kBackForwardCacheDWCOnJavaScriptExecution);
#if BUILDFLAG(IS_ANDROID)
EnableFeatureAndSetParams(features::kBackForwardCache,
"process_binding_strength", "NORMAL");
#endif
// Allow BackForwardCache for all devices regardless of their memory.
DisableFeature(features::kBackForwardCacheMemoryControls);
// Disables BackForwardCache cache size overwritten by
// `content::kBackForwardCacheSize`, as many browser tests here assume
// specific or smaller cache size (e.g. 1) rather than 6.
DisableFeature(kBackForwardCacheSize);
// WebSQL is disabled by default as of M119 (crbug/695592). Enable feature
// in tests during deprecation trial and enterprise policy support.
EnableFeatureAndSetParams(blink::features::kWebSQLAccess, "", "");
SetupFeaturesAndParameters();
command_line->AppendSwitchASCII(
switches::kAutoplayPolicy,
switches::autoplay::kNoUserGestureRequiredPolicy);
// Unfortunately needed for one test on slow bots, TextInputStateUpdated,
// where deferred commits delays input too much.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
void BackForwardCacheBrowserTest::SetUpInProcessBrowserTestFixture() {
ContentBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void BackForwardCacheBrowserTest::TearDownInProcessBrowserTestFixture() {
ContentBrowserTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void BackForwardCacheBrowserTest::SetupFeaturesAndParameters() {
std::vector<base::test::FeatureRefAndParams> enabled_features;
for (const auto& [feature_ref, params] : features_with_params_) {
enabled_features.emplace_back(*feature_ref, params);
}
feature_list_.InitWithFeaturesAndParameters(enabled_features,
disabled_features_);
vmodule_switches_.InitWithSwitches("back_forward_cache_impl=1");
}
void BackForwardCacheBrowserTest::EnableFeatureAndSetParams(
const base::Feature& feature,
std::string param_name,
std::string param_value) {
features_with_params_[feature][param_name] = param_value;
}
void BackForwardCacheBrowserTest::DisableFeature(const base::Feature& feature) {
disabled_features_.push_back(feature);
}
void BackForwardCacheBrowserTest::SetUpOnMainThread() {
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
host_resolver()->AddRule("*", "127.0.0.1");
// TestAutoSetUkmRecorder's constructor requires a sequenced context.
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
histogram_tester_ = std::make_unique<base::HistogramTester>();
ContentBrowserTest::SetUpOnMainThread();
}
void BackForwardCacheBrowserTest::TearDownOnMainThread() {
ukm_recorder_.reset();
ContentBrowserTest::TearDownOnMainThread();
}
WebContentsImpl* BackForwardCacheBrowserTest::web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
RenderFrameHostImpl* BackForwardCacheBrowserTest::current_frame_host() {
return web_contents()->GetPrimaryFrameTree().root()->current_frame_host();
}
RenderFrameHostManager*
BackForwardCacheBrowserTest::render_frame_host_manager() {
return web_contents()->GetPrimaryFrameTree().root()->render_manager();
}
std::string BackForwardCacheBrowserTest::DepictFrameTree(FrameTreeNode* node) {
return visualizer_.DepictFrameTree(node);
}
bool BackForwardCacheBrowserTest::HistogramContainsIntValue(
base::HistogramBase::Sample32 sample,
std::vector<base::Bucket> histogram_values) {
return base::Contains(histogram_values, static_cast<int>(sample),
&base::Bucket::min);
}
void BackForwardCacheBrowserTest::EvictByJavaScript(RenderFrameHostImpl* rfh) {
// Run JavaScript on a page in the back-forward cache. The page should be
// evicted. As the frame is deleted, ExecJs returns false without executing.
// Run without user gesture to prevent UpdateUserActivationState message
// being sent back to browser.
EXPECT_FALSE(
ExecJs(rfh, "console.log('hi');", EXECUTE_SCRIPT_NO_USER_GESTURE));
}
void BackForwardCacheBrowserTest::StartRecordingEvents(
RenderFrameHostImpl* rfh) {
EXPECT_TRUE(ExecJs(rfh, R"(
window.testObservedEvents = [];
let event_list = [
'visibilitychange',
'pagehide',
'pageshow',
'freeze',
'resume',
];
for (event_name of event_list) {
let result = event_name;
window.addEventListener(event_name, event => {
if (event.persisted)
result += '.persisted';
window.testObservedEvents.push('window.' + result);
});
document.addEventListener(event_name,
() => window.testObservedEvents.push('document.' + result));
}
)"));
}
void BackForwardCacheBrowserTest::MatchEventList(RenderFrameHostImpl* rfh,
base::Value list,
base::Location location) {
EXPECT_EQ(list, EvalJs(rfh, "window.testObservedEvents"))
<< location.ToString();
}
// Creates a minimal HTTPS server, accessible through https_server().
// Returns a pointer to the server.
net::EmbeddedTestServer* BackForwardCacheBrowserTest::CreateHttpsServer() {
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_->AddDefaultHandlers(GetTestDataFilePath());
https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
return https_server();
}
net::EmbeddedTestServer* BackForwardCacheBrowserTest::https_server() {
return https_server_.get();
}
// Do not fail this test if a message from a renderer arrives at the browser
// for a cached page.
void BackForwardCacheBrowserTest::DoNotFailForUnexpectedMessagesWhileCached() {
fail_for_unexpected_messages_while_cached_ = false;
}
// Navigates to a page at |page_url| with an img element with src set to
// "image.png".
RenderFrameHostImpl* BackForwardCacheBrowserTest::NavigateToPageWithImage(
const GURL& page_url) {
EXPECT_TRUE(NavigateToURL(shell(), page_url));
RenderFrameHostImpl* rfh = current_frame_host();
// Wait for the document to load DOM to ensure that kLoading is not
// one of the reasons why the document wasn't cached.
EXPECT_TRUE(WaitForDOMContentLoaded(rfh));
EXPECT_TRUE(ExecJs(rfh, R"(
var image = document.createElement("img");
image.src = "image.png";
document.body.appendChild(image);
var image_load_status = new Promise((resolve, reject) => {
image.onload = () => { resolve("loaded"); }
image.onerror = () => { resolve("error"); }
});
)"));
return rfh;
}
void BackForwardCacheBrowserTest::AcquireKeyboardLock(
RenderFrameHostImpl* rfh) {
EXPECT_EQ(42, EvalJs(rfh, R"(
new Promise(resolve => {
navigator.keyboard.lock();
resolve(42);
});
)"));
}
void BackForwardCacheBrowserTest::ReleaseKeyboardLock(
RenderFrameHostImpl* rfh) {
EXPECT_EQ(42, EvalJs(rfh, R"(
new Promise(resolve => {
navigator.keyboard.unlock();
resolve(42);
});
)"));
}
void BackForwardCacheBrowserTest::NavigateAndBlock(GURL url,
int history_offset) {
// Block the navigation with an error.
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
URLLoaderInterceptor::SetupRequestFailForURL(url,
net::ERR_BLOCKED_BY_CLIENT);
if (history_offset) {
shell()->GoBackOrForward(history_offset);
} else {
shell()->LoadURL(url);
}
WaitForLoadStop(web_contents());
ASSERT_EQ(current_frame_host()->GetLastCommittedURL(), url);
ASSERT_TRUE(current_frame_host()->IsErrorDocument());
}
ReasonsMatcher BackForwardCacheBrowserTest::MatchesNotRestoredReasons(
const std::optional<testing::Matcher<std::string>>& id,
const std::optional<testing::Matcher<std::string>>& name,
const std::optional<testing::Matcher<std::string>>& src,
const std::vector<BlockingDetailsReasonsMatcher>& reasons,
const std::optional<SameOriginMatcher>& same_origin_details) {
// TODO(crbug.com/41496143) Make this matcher display human-friendly messages.
return testing::Pointee(testing::AllOf(
id.has_value()
? testing::Field(
"id", &blink::mojom::BackForwardCacheNotRestoredReasons::id,
testing::Optional(id.value()))
: testing::Field(
"id", &blink::mojom::BackForwardCacheNotRestoredReasons::id,
std::optional<std::string>(std::nullopt)),
name.has_value()
? testing::Field(
"name", &blink::mojom::BackForwardCacheNotRestoredReasons::name,
testing::Optional(name.value()))
: testing::Field(
"name", &blink::mojom::BackForwardCacheNotRestoredReasons::name,
std::optional<std::string>(std::nullopt)),
src.has_value()
? testing::Field(
"src", &blink::mojom::BackForwardCacheNotRestoredReasons::src,
testing::Optional(src.value()))
: testing::Field(
"src", &blink::mojom::BackForwardCacheNotRestoredReasons::src,
std::optional<std::string>(std::nullopt)),
testing::Field("reasons",
&blink::mojom::BackForwardCacheNotRestoredReasons::reasons,
testing::UnorderedElementsAreArray(reasons)),
testing::Field(
"same_origin_details",
&blink::mojom::BackForwardCacheNotRestoredReasons::
same_origin_details,
same_origin_details.has_value()
? same_origin_details.value()
: testing::Property(
"is_null",
&blink::mojom::SameOriginBfcacheNotRestoredDetailsPtr::
is_null,
true))));
}
SameOriginMatcher BackForwardCacheBrowserTest::MatchesSameOriginDetails(
const testing::Matcher<GURL>& url,
const std::vector<ReasonsMatcher>& children) {
// TODO(crbug.com/41496143) Make this matcher display human-friendly messages.
return testing::Pointee(testing::AllOf(
testing::Field(
"url", &blink::mojom::SameOriginBfcacheNotRestoredDetails::url, url),
testing::Field(
"children",
&blink::mojom::SameOriginBfcacheNotRestoredDetails::children,
testing::ElementsAreArray(children))));
}
BlockingDetailsReasonsMatcher
BackForwardCacheBrowserTest::MatchesDetailedReason(
const testing::Matcher<std::string>& name,
const std::optional<SourceLocationMatcher>& source) {
// TODO(crbug.com/41496143) Make this matcher display human-friendly
// messages.
return testing::Pointee(testing::AllOf(
testing::Field("name", &blink::mojom::BFCacheBlockingDetailedReason::name,
name),
testing::Field(
"source", &blink::mojom::BFCacheBlockingDetailedReason::source,
source.has_value()
? source.value()
: testing::Property(
"is_null", &blink::mojom::ScriptSourceLocationPtr::is_null,
true))));
}
BlockingDetailsMatcher BackForwardCacheBrowserTest::MatchesBlockingDetails(
const std::optional<SourceLocationMatcher>& source) {
// TODO(crbug.com/41496143) Make this matcher display human-friendly messages.
return testing::Pointee(testing::Field(
"source", &blink::mojom::BlockingDetails::source,
source.has_value()
? source.value()
: testing::Property("is_null",
&blink::mojom::ScriptSourceLocationPtr::is_null,
true)));
}
SourceLocationMatcher BackForwardCacheBrowserTest::MatchesSourceLocation(
const testing::Matcher<GURL>& url,
const testing::Matcher<std::string>& function_name,
const testing::Matcher<uint64_t>& line_number,
const testing::Matcher<uint64_t>& column_number) {
// TODO(crbug.com/41496143) Make this matcher display human-friendly
// messages.
return testing::Pointee(testing::AllOf(
testing::Field("url", &blink::mojom::ScriptSourceLocation::url, url),
testing::Field("function_name",
&blink::mojom::ScriptSourceLocation::function_name,
function_name),
testing::Field("line_number",
&blink::mojom::ScriptSourceLocation::line_number,
line_number),
testing::Field("column_number",
&blink::mojom::ScriptSourceLocation::column_number,
column_number)));
}
void BackForwardCacheUnloadBrowserTest::SetUpCommandLine(
base::CommandLine* command_line) {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
scoped_feature_list_.InitAndEnableFeature(kBackForwardCacheUnloadAllowed);
}
std::initializer_list<RenderFrameHostImpl*> Elements(
std::initializer_list<RenderFrameHostImpl*> t) {
return t;
}
// Execute a custom callback when navigation is ready to commit. This is
// useful for simulating race conditions happening when a page enters the
// BackForwardCache and receive inflight messages sent when it wasn't frozen
// yet.
class ReadyToCommitNavigationCallback : public WebContentsObserver {
public:
ReadyToCommitNavigationCallback(
WebContents* content,
base::OnceCallback<void(NavigationHandle*)> callback)
: WebContentsObserver(content), callback_(std::move(callback)) {}
ReadyToCommitNavigationCallback(const ReadyToCommitNavigationCallback&) =
delete;
ReadyToCommitNavigationCallback& operator=(
const ReadyToCommitNavigationCallback&) = delete;
private:
// WebContentsObserver:
void ReadyToCommitNavigation(NavigationHandle* navigation_handle) override {
if (callback_)
std::move(callback_).Run(navigation_handle);
}
base::OnceCallback<void(NavigationHandle*)> callback_;
};
class FirstVisuallyNonEmptyPaintObserver : public WebContentsObserver {
public:
explicit FirstVisuallyNonEmptyPaintObserver(WebContents* contents)
: WebContentsObserver(contents) {}
void DidFirstVisuallyNonEmptyPaint() override {
if (observed_)
return;
observed_ = true;
run_loop_.Quit();
}
bool did_fire() const { return observed_; }
void Wait() { run_loop_.Run(); }
private:
bool observed_ = false;
base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
};
void WaitForFirstVisuallyNonEmptyPaint(WebContents* contents) {
if (contents->CompletedFirstVisuallyNonEmptyPaint())
return;
FirstVisuallyNonEmptyPaintObserver observer(contents);
observer.Wait();
}
class ThemeColorObserver : public WebContentsObserver {
public:
explicit ThemeColorObserver(WebContents* contents)
: WebContentsObserver(contents) {}
// Can only be called once.
[[nodiscard]] bool WaitUntilThemeColorChange() {
CHECK(!loop_);
loop_ = std::make_unique<base::RunLoop>();
if (observed_) {
return true;
}
loop_->Run();
return observed_;
}
void DidChangeThemeColor() override {
observed_ = true;
if (loop_) {
loop_->Quit();
}
}
bool did_fire() const { return observed_; }
private:
std::unique_ptr<base::RunLoop> loop_;
bool observed_ = false;
};
PageLifecycleStateManagerTestDelegate::PageLifecycleStateManagerTestDelegate(
PageLifecycleStateManager* manager)
: manager_(manager) {
manager->SetDelegateForTesting(this);
}
PageLifecycleStateManagerTestDelegate::
~PageLifecycleStateManagerTestDelegate() {
if (manager_)
manager_->SetDelegateForTesting(nullptr);
}
bool PageLifecycleStateManagerTestDelegate::WaitForInBackForwardCacheAck() {
DCHECK(manager_);
if (manager_->last_acknowledged_state().is_in_back_forward_cache) {
return true;
}
base::RunLoop loop;
store_in_back_forward_cache_ack_received_ = loop.QuitClosure();
loop.Run();
return manager_->last_acknowledged_state().is_in_back_forward_cache;
}
void PageLifecycleStateManagerTestDelegate::OnStoreInBackForwardCacheSent(
base::OnceClosure cb) {
store_in_back_forward_cache_sent_ = std::move(cb);
}
void PageLifecycleStateManagerTestDelegate::OnDisableJsEvictionSent(
base::OnceClosure cb) {
disable_eviction_sent_ = std::move(cb);
}
void PageLifecycleStateManagerTestDelegate::OnRestoreFromBackForwardCacheSent(
base::OnceClosure cb) {
restore_from_back_forward_cache_sent_ = std::move(cb);
}
void PageLifecycleStateManagerTestDelegate::OnLastAcknowledgedStateChanged(
const blink::mojom::PageLifecycleState& old_state,
const blink::mojom::PageLifecycleState& new_state) {
if (store_in_back_forward_cache_ack_received_ &&
new_state.is_in_back_forward_cache)
std::move(store_in_back_forward_cache_ack_received_).Run();
}
void PageLifecycleStateManagerTestDelegate::OnUpdateSentToRenderer(
const blink::mojom::PageLifecycleState& new_state) {
if (store_in_back_forward_cache_sent_ && new_state.is_in_back_forward_cache) {
std::move(store_in_back_forward_cache_sent_).Run();
}
if (disable_eviction_sent_ && new_state.eviction_enabled == false) {
std::move(disable_eviction_sent_).Run();
}
if (restore_from_back_forward_cache_sent_ &&
!new_state.is_in_back_forward_cache) {
std::move(restore_from_back_forward_cache_sent_).Run();
}
}
void PageLifecycleStateManagerTestDelegate::OnDeleted() {
manager_ = nullptr;
}
// Check the visible URL in the omnibox is properly updated when restoring a
// document from the BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, VisibleURL) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Go to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// 2) Go to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(url_a, web_contents()->GetVisibleURL());
// 4) Go forward to B.
ASSERT_TRUE(HistoryGoForward(web_contents()));
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
}
// Test only 1 document is kept in the at a time BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheSizeLimitedToOneDocumentPerTab) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// BackForwardCache is empty.
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// BackForwardCache contains only rfh_a.
RenderFrameHostImpl* rfh_b = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b);
EXPECT_TRUE(NavigateToURL(shell(), url_c));
// BackForwardCache contains only rfh_b.
delete_observer_rfh_a.WaitUntilDeleted();
EXPECT_FALSE(delete_observer_rfh_b.deleted());
// If/when the cache size is increased, this can be tested iteratively, see
// deleted code in: https://crrev.com/c/1782902.
ASSERT_TRUE(HistoryGoToOffset(web_contents(), -2));
ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason::kCacheLimit},
{}, {}, {}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, ResponseHeaders) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/set-header?X-Foo: bar"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
NavigationHandleObserver observer1(web_contents(), url_a);
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
EXPECT_TRUE(observer1.has_committed());
EXPECT_EQ("bar", observer1.GetNormalizedResponseHeader("x-foo"));
// 2) Navigate to B.
NavigationHandleObserver observer2(web_contents(), url_b);
EXPECT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImpl* rfh_b = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b);
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_FALSE(rfh_b->IsInBackForwardCache());
EXPECT_TRUE(observer2.has_committed());
// 3) Go back to A.
NavigationHandleObserver observer3(web_contents(), url_a);
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_FALSE(delete_observer_rfh_b.deleted());
EXPECT_EQ(rfh_a, current_frame_host());
EXPECT_FALSE(rfh_a->IsInBackForwardCache());
EXPECT_TRUE(rfh_b->IsInBackForwardCache());
EXPECT_TRUE(observer3.has_committed());
EXPECT_EQ("bar", observer3.GetNormalizedResponseHeader("x-foo"));
ExpectRestored(FROM_HERE);
}
void HighCacheSizeBackForwardCacheBrowserTest::SetUpCommandLine(
base::CommandLine* command_line) {
EnableFeatureAndSetParams(features::kBackForwardCache, "cache_size",
base::NumberToString(kBackForwardCacheSize));
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
// Test documents are evicted from the BackForwardCache at some point.
IN_PROC_BROWSER_TEST_F(HighCacheSizeBackForwardCacheBrowserTest,
CacheEvictionWithIncreasedCacheSize) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a)); // BackForwardCache size is 0.
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
EXPECT_TRUE(NavigateToURL(shell(), url_b)); // BackForwardCache size is 1.
RenderFrameHostImpl* rfh_b = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b);
for (size_t i = 2; i < kBackForwardCacheSize; ++i) {
EXPECT_TRUE(NavigateToURL(shell(), i % 2 ? url_b : url_a));
// After |i+1| navigations, |i| documents went into the BackForwardCache.
// When |i| is greater than the BackForwardCache size limit, they are
// evicted:
EXPECT_EQ(i >= kBackForwardCacheSize + 1, delete_observer_rfh_a.deleted());
EXPECT_EQ(i >= kBackForwardCacheSize + 2, delete_observer_rfh_b.deleted());
}
}
// Tests that evicting a page in between the time the back/forward cache
// NavigationRequest restore was created and when the NavigationRequest actually
// starts after finishing beforeunload won't result in a crash.
// See https://crbug.com/1218114.
IN_PROC_BROWSER_TEST_F(HighCacheSizeBackForwardCacheBrowserTest,
EvictedWhileWaitingForBeforeUnload) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
GURL url_c(embedded_test_server()->GetURL("c.com", "/title3.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Navigate to C, which has a beforeunload handler that never finishes.
EXPECT_TRUE(NavigateToURL(shell(), url_c));
RenderFrameHostImplWrapper rfh_c(current_frame_host());
EXPECT_TRUE(ExecJs(rfh_c.get(), R"(
window.onbeforeunload = () => {
while (true) {}
}
)"));
// Both A & B are in the back/forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_TRUE(rfh_b->IsInBackForwardCache());
// 4) Evict entry A. This will post a task that destroys all evicted entries
// when it runs (task #1).
DisableBFCacheForRFHForTesting(rfh_a->GetGlobalId());
EXPECT_FALSE(rfh_a.IsDestroyed());
EXPECT_TRUE(rfh_a->is_evicted_from_back_forward_cache());
// 5) Trigger a back navigation to B. This will create a BFCache restore
// navigation to B, but will wait for C's beforeunload handler to finish
// running before continuing.
// The BFCache entry will be evicted before the back navigation completes, so
// the old navigation will be reset and a new navigation will be restarted.
// This observer is waiting for the two navigation requests to complete.
TestNavigationObserver observer(web_contents(),
/* expected_number_of_navigations= */ 2,
MessageLoopRunner::QuitMode::IMMEDIATE,
/* ignore_uncommitted_navigations= */ false);
web_contents()->GetController().GoBack();
// 6) Post a task to run BeforeUnloadCompleted (task #2). This will continue
// the BFCache restore navigation to B from step 5, which is currently waiting
// for a BeforeUnloadCompleted call.
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([&]() {
root->navigator().BeforeUnloadCompleted(
root, /*proceed=*/true, base::TimeTicks::Now(),
/*for_legacy=*/false, /*showed_dialog=*/false);
}));
// 7) Evict entry B. This will post a task (task #3) to restart the navigation
// to B, and also another task (task #4) to destroy all evicted entries.
DisableBFCacheForRFHForTesting(rfh_b->GetGlobalId());
EXPECT_FALSE(rfh_b.IsDestroyed());
EXPECT_TRUE(rfh_b->is_evicted_from_back_forward_cache());
// 8) Wait until the back navigation to B finishes. This will run posted tasks
// in order. So:
// - Task #1 from step 4 will run and destroy all evicted entries. As both the
// entries for A & B have been evicted, they are both destroyed.
// - Task #2 from step 6 will run and continue the back/forward cache restore
// NavigationRequest to B. However, it would notice that the entry for B is
// now gone, and should handle it gracefully.
// - Task #3 from step 7 to restart navigation to B runs, and should create a
// NavigationRequest to replace the previous NavigationRequest to B.
// - Task #4 from step 7 to destroy evicted entries runs and won't destroy
// any entry since there's no longer any entry in the back/forward cache.
observer.Wait();
EXPECT_EQ(web_contents()->GetLastCommittedURL(), url_b);
ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason::
kDisableForRenderFrameHostCalled},
{}, {}, {RenderFrameHostDisabledForTestingReason()}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
SubframeWithOngoingNavigationNotCached) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/hung");
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to a page with an iframe.
TestNavigationObserver navigation_observer1(web_contents());
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_hung_iframe.html"));
shell()->LoadURL(main_url);
navigation_observer1.WaitForNavigationFinished();
RenderFrameHostImpl* main_frame = current_frame_host();
RenderFrameDeletedObserver frame_deleted_observer(main_frame);
response.WaitForRequest();
// Navigate away.
TestNavigationObserver navigation_observer2(web_contents());
shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title1.html"));
navigation_observer2.WaitForNavigationFinished();
// The page with the unsupported feature should be deleted (not cached).
frame_deleted_observer.WaitUntilDeleted();
}
// Only HTTP/HTTPS main document can enter the BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, CacheHTTPDocumentOnly) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL http_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
GURL https_url(https_server()->GetURL("a.test", "/title1.html"));
GURL file_url = net::FilePathToFileURL(GetTestFilePath("", "title1.html"));
GURL data_url = GURL("data:text/html,");
GURL blank_url = GURL(url::kAboutBlankURL);
GURL webui_url = GetWebUIURL("gpu");
enum { STORED, DELETED };
struct {
int expectation;
GURL url;
} test_cases[] = {
// Only document with HTTP/HTTPS URLs are allowed to enter the
// BackForwardCache.
{STORED, http_url},
{STORED, https_url},
// Others aren't allowed.
{DELETED, file_url},
{DELETED, data_url},
{DELETED, webui_url},
{DELETED, blank_url},
};
char hostname[] = "a.unique";
for (auto& test_case : test_cases) {
SCOPED_TRACE(testing::Message()
<< std::endl
<< "expectation = " << test_case.expectation << std::endl
<< "url = " << test_case.url << std::endl);
// 1) Navigate to.
EXPECT_TRUE(NavigateToURL(shell(), test_case.url));
RenderFrameHostImplWrapper rfh(current_frame_host());
// 2) Navigate away.
hostname[0]++;
GURL reset_url(embedded_test_server()->GetURL(hostname, "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), reset_url));
if (test_case.expectation == STORED) {
EXPECT_FALSE(rfh.IsRenderFrameDeleted());
EXPECT_TRUE(rfh->IsInBackForwardCache());
continue;
}
if (rfh.get() == current_frame_host()) {
// If the RenderFrameHost is reused, it won't be deleted, so don't wait
// for deletion. Just check that it's not saved in the back-forward cache.
EXPECT_FALSE(rfh.IsRenderFrameDeleted());
EXPECT_FALSE(rfh->IsInBackForwardCache());
continue;
}
// When the RenderFrameHost is not reused and it's not stored in the
// back-forward cache, it will eventually be deleted.
ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
}
}
// Regression test for https://crbug.com/993337.
//
// A note about sharing BrowsingInstances and the BackForwardCache:
//
// We should never keep around more than one main frame that belongs to the same
// BrowsingInstance. When swapping two pages, when one is stored in the
// back-forward cache or one is restored from it, the current code expects the
// two to live in different BrowsingInstances.
//
// History navigation can recreate a page with the same BrowsingInstance as the
// one stored in the back-forward cache. This case must to be handled. When it
// happens, the back-forward cache page is evicted.
//
// Since cache eviction is asynchronous, it's is possible for two main frames
// belonging to the same BrowsingInstance to be alive for a brief period of time
// (the new page being navigated to, and a page in the cache, until it is
// destroyed asynchronously via eviction).
//
// The test below tests that the brief period of time where two main frames are
// alive in the same BrowsingInstance does not cause anything to blow up.
// TODO(crbug.com/1127979, crbug.com/1446206): Flaky on Linux, Windows and
// ChromeOS, iOS, and Mac.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) || \
BUILDFLAG(IS_MAC) || BUILDFLAG(IS_IOS)
#define MAYBE_NavigateToTwoPagesOnSameSite DISABLED_NavigateToTwoPagesOnSameSite
#else
#define MAYBE_NavigateToTwoPagesOnSameSite NavigateToTwoPagesOnSameSite
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_NavigateToTwoPagesOnSameSite) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a1(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_a2(embedded_test_server()->GetURL("a.com", "/title2.html"));
GURL url_b3(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A1.
EXPECT_TRUE(NavigateToURL(shell(), url_a1));
// 2) Navigate to A2.
EXPECT_TRUE(NavigateToURL(shell(), url_a2));
RenderFrameHostImpl* rfh_a2 = current_frame_host();
RenderFrameDeletedObserver delete_rfh_a2(current_frame_host());
// 3) Navigate to B3.
EXPECT_TRUE(NavigateToURL(shell(), url_b3));
EXPECT_TRUE(rfh_a2->IsInBackForwardCache());
RenderFrameHostImpl* rfh_b3 = current_frame_host();
// 4) Do a history navigation back to A1.
ASSERT_TRUE(HistoryGoToIndex(web_contents(), 0));
EXPECT_TRUE(rfh_b3->IsInBackForwardCache());
// Note that the frame for A1 gets created before A2 is deleted from the
// cache, so there will be a brief period where two the main frames (A1 and
// A2) are alive in the same BrowsingInstance/SiteInstance, at the same time.
// That is the scenario this test is covering. This used to cause a CHECK,
// because the two main frames shared a single RenderViewHost (no longer the
// case after https://crrev.com/c/1833616).
// A2 should be evicted from the cache and asynchronously deleted, due to the
// cache size limit (B3 took its place in the cache).
delete_rfh_a2.WaitUntilDeleted();
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
NavigateToTwoPagesOnSameSiteWithSubframes) {
ASSERT_TRUE(embedded_test_server()->Start());
// This test covers the same scenario as NavigateToTwoPagesOnSameSite, except
// the pages contain subframes:
// A1(B) -> A2(B(C)) -> D3 -> A1(B)
//
// The subframes shouldn't make a difference, so the expected behavior is the
// same as NavigateToTwoPagesOnSameSite.
GURL url_a1(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
GURL url_a2(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
GURL url_d3(embedded_test_server()->GetURL("d.com", "/title1.html"));
// 1) Navigate to A1(B).
EXPECT_TRUE(NavigateToURL(shell(), url_a1));
// 2) Navigate to A2(B(C)).
EXPECT_TRUE(NavigateToURL(shell(), url_a2));
RenderFrameHostImpl* rfh_a2 = current_frame_host();
RenderFrameDeletedObserver delete_rfh_a2(current_frame_host());
// 3) Navigate to D3.
EXPECT_TRUE(NavigateToURL(shell(), url_d3));
EXPECT_TRUE(rfh_a2->IsInBackForwardCache());
RenderFrameHostImpl* rfh_d3 = current_frame_host();
// 4) Do a history navigation back to A1(B).
ASSERT_TRUE(HistoryGoToIndex(web_contents(), 0));
// D3 takes A2(B(C))'s place in the cache.
EXPECT_TRUE(rfh_d3->IsInBackForwardCache());
delete_rfh_a2.WaitUntilDeleted();
}
// Sub-frame doesn't transition from LifecycleStateImpl::kInBackForwardCache to
// LifecycleStateImpl::kRunningUnloadHandlers even when the sub-frame having
// unload handlers is being evicted from BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheUnloadBrowserTest,
SubframeWithUnloadHandler) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a.com(a.com)"));
GURL child_url = embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a.com()");
GURL url_2(embedded_test_server()->GetURL("a.com", "/title1.html"));
// 1) Navigate to |main_url|.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImpl* main_rfh = current_frame_host();
ASSERT_EQ(1U, main_rfh->child_count());
RenderFrameHostImpl* child_rfh = main_rfh->child_at(0)->current_frame_host();
RenderFrameDeletedObserver main_rfh_observer(main_rfh),
child_rfh_observer(child_rfh);
// 2) Add an unload handler to the child RFH.
EXPECT_TRUE(ExecJs(child_rfh, "window.onunload = () => {} "));
// 3) Navigate to |url_2|.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// 4) The previous main RFH and child RFH should be in the back-forward
// cache.
EXPECT_FALSE(main_rfh_observer.deleted());
EXPECT_FALSE(child_rfh_observer.deleted());
EXPECT_TRUE(main_rfh->IsInBackForwardCache());
EXPECT_TRUE(child_rfh->IsInBackForwardCache());
// Destruction of bfcached page happens after shutdown and it should not
// trigger unload handlers and be destroyed directly.
}
// Do a same document navigation and make sure we do not fire the
// DidFirstVisuallyNonEmptyPaint again
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
DoesNotFireDidFirstVisuallyNonEmptyPaintForSameDocumentNavigation) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a_1(embedded_test_server()->GetURL(
"a.com", "/accessibility/html/a-name.html"));
GURL url_a_2(embedded_test_server()->GetURL(
"a.com", "/accessibility/html/a-name.html#id"));
EXPECT_TRUE(NavigateToURL(shell(), url_a_1));
WaitForFirstVisuallyNonEmptyPaint(shell()->web_contents());
FirstVisuallyNonEmptyPaintObserver observer(web_contents());
EXPECT_TRUE(NavigateToURL(shell(), url_a_2));
// Make sure the bfcache restore code does not fire the event during commit
// navigation.
EXPECT_FALSE(observer.did_fire());
EXPECT_TRUE(web_contents()->CompletedFirstVisuallyNonEmptyPaint());
}
// Make sure we fire DidFirstVisuallyNonEmptyPaint when restoring from bf-cache.
// TODO(crbug.com/327195951): Re-enable this test
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache \
DISABLED_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache
#else
#define MAYBE_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache \
FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache
#endif
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
MAYBE_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
WaitForFirstVisuallyNonEmptyPaint(shell()->web_contents());
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_FALSE(delete_observer_rfh_a.deleted());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
WaitForFirstVisuallyNonEmptyPaint(shell()->web_contents());
// 3) Navigate to back to A.
FirstVisuallyNonEmptyPaintObserver observer(web_contents());
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Make sure the bfcache restore code does fire the event during commit
// navigation.
EXPECT_TRUE(web_contents()->CompletedFirstVisuallyNonEmptyPaint());
EXPECT_TRUE(observer.did_fire());
}
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_SetsThemeColorWhenRestoredFromCache \
DISABLED_SetsThemeColorWhenRestoredFromCache
#else
#define MAYBE_SetsThemeColorWhenRestoredFromCache \
SetsThemeColorWhenRestoredFromCache
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_SetsThemeColorWhenRestoredFromCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/theme_color.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_a));
WaitForFirstVisuallyNonEmptyPaint(web_contents());
RenderFrameHostImplWrapper rfh_a(current_frame_host());
EXPECT_EQ(web_contents()->GetThemeColor(), 0xFFFF0000u);
ASSERT_TRUE(NavigateToURL(shell(), url_b));
WaitForFirstVisuallyNonEmptyPaint(web_contents());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_EQ(web_contents()->GetThemeColor(), std::nullopt);
ThemeColorObserver observer(web_contents());
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_TRUE(observer.WaitUntilThemeColorChange());
EXPECT_EQ(web_contents()->GetThemeColor(), 0xFFFF0000u);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
ContentsMimeTypeWhenRestoredFromCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
EXPECT_EQ(web_contents()->GetContentsMimeType(), "text/html");
// Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Go back to A, which restores A from bfcache. ContentsMimeType should be
// restored as well.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
ExpectRestored(FROM_HERE);
EXPECT_EQ(web_contents()->GetContentsMimeType(), "text/html");
}
// Check BackForwardCache is enabled and works for devices with very low memory.
// Navigate from A -> B and go back.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
BackForwardCacheEnabledOnLowMemoryDevices) {
// Set device physical memory to 10 MB.
blink::ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(10);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameDeletedObserver delete_observer_rfh_a(current_frame_host());
RenderFrameHostImpl* rfh_a = current_frame_host();
// 2) Navigate to B. A should be in BackForwardCache.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImpl* rfh_b = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b);
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to A. B should be in BackForwardCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_FALSE(delete_observer_rfh_b.deleted());
EXPECT_TRUE(rfh_b->IsInBackForwardCache());
}
// Test for functionality of memory controls in back-forward cache for low
// memory devices.
class BackForwardCacheBrowserTestForLowMemoryDevices
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
// Set the value of memory threshold more than the physical memory and check
// if back-forward cache is disabled or not.
std::string memory_threshold =
base::NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB() + 1);
scoped_feature_list_.InitWithFeaturesAndParameters(
{{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb",
memory_threshold}}},
{blink::features::kLoadingTasksUnfreezable, {}}},
{});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Ensure that the BackForwardCache trial is not activated as expected on
// low-memory devices.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForLowMemoryDevices,
DisableBFCacheForLowEndDevices) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Ensure that the BackForwardCache trial starts inactive.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
EXPECT_FALSE(IsBackForwardCacheEnabled());
// Ensure that we do not activate the BackForwardCache trial when querying
// bfcache status.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) A shouldn't be stored in back-forward cache because the physical
// memory is less than the memory threshold.
delete_observer_rfh_a.WaitUntilDeleted();
// 4) Go back to check the
// NotRestoredReasons.kBackForwardCacheDisabledByLowMemory is recorded when
// the memory is less than the threshold value.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{
BackForwardCacheMetrics::NotRestoredReason::kBackForwardCacheDisabled,
BackForwardCacheMetrics::NotRestoredReason::
kBackForwardCacheDisabledByLowMemory,
},
{}, {}, {}, {}, FROM_HERE);
// Ensure that the BackForwardCache trial still hasn't been activated.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
}
// Trigger network reqeuests, then navigate from A to B, then go back.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForLowMemoryDevices,
DisableBFCacheForLowEndDevices_NetworkRequests) {
net::test_server::ControllableHttpResponse image_response(
embedded_test_server(), "/image.png");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Ensure that the trials starts inactive.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(
blink::features::kLoadingTasksUnfreezable)
->trial_name()));
EXPECT_FALSE(IsBackForwardCacheEnabled());
// Ensure that we do not activate the trials for kBackForwardCache and
// kLoadingTasksUnfreezable when querying bfcache or unfreezable loading tasks
// status.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(
blink::features::kLoadingTasksUnfreezable)
->trial_name()));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Request for an image and send a response to trigger loading code. This is
// to ensure kLoadingTasksUnfreezable won't trigger bfcache activation.
EXPECT_TRUE(ExecJs(rfh_a, R"(
var image = document.createElement("img");
image.src = "image.png";
document.body.appendChild(image);
)"));
image_response.WaitForRequest();
image_response.Send(net::HTTP_OK, "image/png");
image_response.Send("image_body");
image_response.Done();
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) A shouldn't be stored in back-forward cache because the physical
// memory is less than the memory threshold.
delete_observer_rfh_a.WaitUntilDeleted();
// Nothing is recorded when the memory is less than the threshold value.
ExpectOutcomeDidNotChange(FROM_HERE);
ExpectNotRestoredDidNotChange(FROM_HERE);
// Ensure that the trials still haven't been activated.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(
blink::features::kLoadingTasksUnfreezable)
->trial_name()));
}
// Test for functionality of memory controls in back-forward cache for high
// memory devices.
class BackForwardCacheBrowserTestForHighMemoryDevices
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
// Set the value of memory threshold less than the physical memory and check
// if back-forward cache is enabled or not.
std::string memory_threshold =
base::NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB() - 1);
scoped_feature_list_.InitWithFeaturesAndParameters(
{{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb",
memory_threshold}}},
{blink::features::kLoadingTasksUnfreezable, {}}},
{});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Ensure that the BackForwardCache trial got activated as expected on
// high-memory devices when the BackForwardCache feature is enabled.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForHighMemoryDevices,
EnableBFCacheForHighMemoryDevices) {
// Ensure that the BackForwardCache trial starts active on high-memory devices
// when the BackForwardCache feature is enabled, because
// IsBackForwardCacheEnabled() got queried already before the test starts.
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
EXPECT_TRUE(IsBackForwardCacheEnabled());
// Ensure that the BackForwardCache trial stays active after querying
// IsBackForwardCacheEnabled().
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) A should be stored in back-forward cache because the physical memory is
// greater than the memory threshold.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Ensure that the BackForwardCache trial stays active.
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
}
// Trigger network reqeuests, then navigate from A to B, then go back.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForHighMemoryDevices,
EnableBFCacheForHighMemoryDevices_NetworkRequests) {
net::test_server::ControllableHttpResponse image_response(
embedded_test_server(), "/image.png");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Ensure that back-forward cache flag is enabled and the trial is active.
EXPECT_TRUE(IsBackForwardCacheEnabled());
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
// Ensure that the LoadingTasksUnfreezable trials starts as inactive.
EXPECT_FALSE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(
blink::features::kLoadingTasksUnfreezable)
->trial_name()));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Request for an image and send a response to trigger loading code.
EXPECT_TRUE(ExecJs(rfh_a, R"(
var image = document.createElement("img");
image.src = "image.png";
document.body.appendChild(image);
)"));
image_response.WaitForRequest();
image_response.Send(net::HTTP_OK, "image/png");
image_response.Send("image_body");
image_response.Done();
// The loading code activates the LoadingTasksUnfreezable trial.
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(
blink::features::kLoadingTasksUnfreezable)
->trial_name()));
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) A should be stored in back-forward cache because the physical memory is
// greater than the memory threshold.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Ensure that the trials stay activated.
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(features::kBackForwardCache)
->trial_name()));
EXPECT_TRUE(base::FieldTrialList::IsTrialActive(
base::FeatureList::GetFieldTrial(
blink::features::kLoadingTasksUnfreezable)
->trial_name()));
}
// Tests for high memory devices that have the BackForwardCache feature flag
// disabled.
class BackForwardCacheBrowserTestForHighMemoryDevicesWithBFCacheDisabled
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
// Set the value of memory threshold less than the physical memory and check
// if back-forward cache is enabled or not.
std::string memory_threshold =
base::NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB() - 1);
scoped_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb",
memory_threshold}}},
{blink::features::kLoadingTasksUnfreezable, {}}},
/*disabled_features=*/
{features::kBackForwardCache});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTestForHighMemoryDevicesWithBFCacheDisabled,
HighMemoryDevicesWithBFacheDisabled) {
// Ensure that IsBackForwardCacheEnabled() returns false, because the
// BackForwardCache feature is disabled.
EXPECT_FALSE(IsBackForwardCacheEnabled());
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) A shouldn't be stored in back-forward cache because the BackForwardCache
// feature is disabled.
delete_observer_rfh_a.WaitUntilDeleted();
// 4) Go back to check that only kBackForwardCacheDisabled is recorded.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{
BackForwardCacheMetrics::NotRestoredReason::kBackForwardCacheDisabled,
},
{}, {}, {}, {}, FROM_HERE);
}
// Start an inifite dialogs in JS, yielding after each. The first dialog should
// be dismissed by navigation. The later dialogs should be handled gracefully
// and not appear while in BFCache. Finally, when the page comes out of BFCache,
// dialogs should appear again.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CanUseCacheWhenPageAlertsInTimeoutLoop) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
AppModalDialogWaiter dialog_waiter(shell());
EXPECT_TRUE(ExecJs(rfh_a, R"(
function alertLoop() {
setTimeout(alertLoop, 0);
window.alert("alert");
}
// Don't block this script.
setTimeout(alertLoop, 0);
)"));
dialog_waiter.Wait();
// Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImpl* rfh_b = current_frame_host();
ASSERT_FALSE(delete_observer_rfh_a.deleted());
ASSERT_THAT(rfh_a, InBackForwardCache());
ASSERT_NE(rfh_a, rfh_b);
dialog_waiter.Restart();
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
EXPECT_FALSE(rfh_a->IsInBackForwardCache());
// The page should still be requesting dialogs in a loop. Wait for one to be
// requested.
dialog_waiter.Wait();
}
// UnloadOldFrame will clear all dialogs. We test that further requests for
// dialogs coming from JS do not result in the creation of a dialog. This test
// posts some dialog creation JS to the render from inside the
// CommitNavigationCallback task. This JS is then able to post a task back to
// the renders to show a dialog. By the time this task runs, we the
// RenderFrameHostImpl's is_active() should be false.
//
// This test is not perfect, it can pass simply because the renderer thread does
// not run the JS in time. Ideally it would block until the renderer posts the
// request for a dialog but it's possible to do that without creating a nested
// message loop and if we do that, we risk processing the dialog request.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DialogsCancelledAndSuppressedWhenCached) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Let's us know whether the following callback ran. Not strictly necessary
// since it really should run.
bool posted_dialog_js = false;
// Create a callback that will be called during the DidCommitNavigation task.
WillEnterBackForwardCacheCallbackForTesting
will_enter_back_forward_cache_callback =
base::BindLambdaForTesting([&]() {
// Post a dialog, it should not result in a dialog being created.
ExecuteScriptAsync(rfh_a, R"(window.alert("alert");)");
posted_dialog_js = true;
});
rfh_a->render_view_host()->SetWillEnterBackForwardCacheCallbackForTesting(
will_enter_back_forward_cache_callback);
AppModalDialogWaiter dialog_waiter(shell());
// Try show another dialog. It should work.
ExecuteScriptAsync(rfh_a, R"(window.alert("alert");)");
dialog_waiter.Wait();
dialog_waiter.Restart();
// Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImpl* rfh_b = current_frame_host();
ASSERT_FALSE(delete_observer_rfh_a.deleted());
ASSERT_THAT(rfh_a, InBackForwardCache());
ASSERT_NE(rfh_a, rfh_b);
// Test that the JS was run and that it didn't result in a dialog.
ASSERT_TRUE(posted_dialog_js);
ASSERT_FALSE(dialog_waiter.WasDialogRequestedCallbackCalled());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
EXPECT_FALSE(rfh_a->IsInBackForwardCache());
// Try show another dialog. It should work.
ExecuteScriptAsync(rfh_a, R"(window.alert("alert");)");
dialog_waiter.Wait();
}
// Tests that pagehide handlers of the old RFH are run for bfcached pages even
// if the page is already hidden (and visibilitychange won't run).
// Disabled on Linux and Win because of flakiness, see crbug.com/40165901.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
#define MAYBE_PagehideRunsWhenPageIsHidden DISABLED_PagehideRunsWhenPageIsHidden
#else
#define MAYBE_PagehideRunsWhenPageIsHidden PagehideRunsWhenPageIsHidden
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_PagehideRunsWhenPageIsHidden) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
GURL url_3(embedded_test_server()->GetURL("a.com", "/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to |url_1| and hide the tab.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImplWrapper main_frame_1(web_contents->GetPrimaryMainFrame());
// We need to set it to Visibility::VISIBLE first in case this is the first
// time the visibility is updated.
web_contents->UpdateWebContentsVisibility(Visibility::VISIBLE);
web_contents->UpdateWebContentsVisibility(Visibility::HIDDEN);
EXPECT_EQ(Visibility::HIDDEN, web_contents->GetVisibility());
// Create a pagehide handler that sets item "pagehide_storage" and a
// visibilitychange handler that sets item "visibilitychange_storage" in
// localStorage.
EXPECT_TRUE(ExecJs(main_frame_1.get(),
R"(
localStorage.setItem('pagehide_storage', 'not_dispatched');
var dispatched_pagehide = false;
window.onpagehide = function(e) {
if (dispatched_pagehide) {
// We shouldn't dispatch pagehide more than once.
localStorage.setItem('pagehide_storage', 'dispatched_more_than_once');
} else if (!e.persisted) {
localStorage.setItem('pagehide_storage', 'wrong_persisted');
} else {
localStorage.setItem('pagehide_storage', 'dispatched_once');
}
dispatched_pagehide = true;
}
localStorage.setItem('visibilitychange_storage', 'not_dispatched');
document.onvisibilitychange = function(e) {
localStorage.setItem('visibilitychange_storage',
'should_not_be_dispatched');
}
)"));
// |visibilitychange_storage| should be set to its initial correct value.
EXPECT_EQ("not_dispatched",
GetLocalStorage(main_frame_1.get(), "visibilitychange_storage"));
// 2) Navigate cross-site to |url_2|. We need to navigate cross-site to make
// sure we won't run pagehide and visibilitychange during new page's commit,
// which is tested in ProactivelySwapBrowsingInstancesSameSiteTest.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// |main_frame_1| should be in the back-forward cache.
EXPECT_TRUE(main_frame_1->IsInBackForwardCache());
// 3) Navigate to |url_3| which is same-origin with |url_1|, so we can check
// the localStorage values.
EXPECT_TRUE(NavigateToURL(shell(), url_3));
RenderFrameHostImpl* main_frame_3 = web_contents->GetPrimaryMainFrame();
// Check that the value for 'pagehide_storage' and 'visibilitychange_storage'
// are set correctly.
EXPECT_TRUE(
WaitForLocalStorage(main_frame_3, "pagehide_storage", "dispatched_once"));
EXPECT_TRUE(WaitForLocalStorage(main_frame_3, "visibilitychange_storage",
"not_dispatched"));
}
// Tests that we're getting the correct TextInputState and focus updates when a
// page enters the back-forward cache and when it gets restored.
// TODO(b/324570785): Re-enable the test for Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_TextInputStateUpdated DISABLED_TextInputStateUpdated
#else
#define MAYBE_TextInputStateUpdated TextInputStateUpdated
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_TextInputStateUpdated) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to |url_1| and add a text input with "foo" as the value.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImpl* rfh_1 = current_frame_host();
EXPECT_TRUE(ExecJs(rfh_1,
"document.title='bfcached';"
"var input = document.createElement('input');"
"input.setAttribute('type', 'text');"
"input.setAttribute('value', 'foo');"
"document.body.appendChild(input);"
"var focusCount = 0;"
"var blurCount = 0;"
"input.onfocus = () => { focusCount++;};"
"input.onblur = () => { blurCount++; };"));
{
TextInputManagerTypeObserver type_observer(web_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
TextInputManagerValueObserver value_observer(web_contents(), "foo");
// 2) Press tab key to focus the <input>, and verify the type & value.
SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer.Wait();
value_observer.Wait();
EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 1);
EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 0);
}
{
TextInputManagerTester tester(web_contents());
TextInputManagerValueObserver value_observer(web_contents(), "A");
// 3) Press the "A" key to change the text input value. This should notify
// the browser that the text input value has changed.
SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('A'),
ui::DomCode::US_A, ui::VKEY_A, false, false, false, false);
value_observer.Wait();
EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 1);
EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 0);
}
{
TextInputManagerTypeObserver type_observer(web_contents(),
ui::TEXT_INPUT_TYPE_NONE);
// 4) Navigating to |url_2| should reset type to TEXT_INPUT_TYPE_NONE.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
type_observer.Wait();
// |rfh_1| should get into the back-forward cache.
EXPECT_TRUE(rfh_1->IsInBackForwardCache());
EXPECT_EQ(current_frame_host(), web_contents()->GetFocusedFrame());
EXPECT_NE(rfh_1, web_contents()->GetFocusedFrame());
}
{
// 5) Navigating back to |url_1|, we shouldn't restore the focus to the
// text input, but |rfh_1| will be focused again as we will restore focus
// to main frame after navigation.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 1);
EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 1);
}
{
TextInputManagerTypeObserver type_observer(web_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
TextInputManagerValueObserver value_observer(web_contents(), "A");
// 6) Press tab key to focus the <input> again. Note that we need to press
// the tab key twice here, because the last "tab focus" point was the
// <input> element. The first tab key press would focus on the UI/url bar,
// then the second tab key would go back to the <input>.
SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer.Wait();
value_observer.Wait();
EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 2);
EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 1);
}
}
#if (BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID))
#define MAYBE_SubframeTextInputStateUpdated DISABLED_SubframeTextInputStateUpdated
#else
#define MAYBE_SubframeTextInputStateUpdated SubframeTextInputStateUpdated
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_SubframeTextInputStateUpdated) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(a))"));
GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to |url_1| and add a text input with "foo" as the value in the
// a.com subframe.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameHostImpl* rfh_b = rfh_a->child_at(0)->current_frame_host();
RenderFrameHostImpl* rfh_subframe_a =
rfh_b->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(rfh_subframe_a,
"var input = document.createElement('input');"
"input.setAttribute('type', 'text');"
"input.setAttribute('value', 'foo');"
"document.body.appendChild(input);"
"var focusCount = 0;"
"var blurCount = 0;"
"input.onfocus = () => { focusCount++;};"
"input.onblur = () => { blurCount++; };"));
{
TextInputManagerTypeObserver type_observer(web_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
TextInputManagerValueObserver value_observer(web_contents(), "foo");
// 2) Press tab key to focus the <input>, and verify the type & value.
SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer.Wait();
value_observer.Wait();
EXPECT_EQ(rfh_subframe_a, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 1);
EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 0);
}
{
TextInputManagerTester tester(web_contents());
TextInputManagerValueObserver value_observer(web_contents(), "A");
// 3) Press the "A" key to change the text input value. This should notify
// the browser that the text input value has changed.
SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('A'),
ui::DomCode::US_A, ui::VKEY_A, false, false, false, false);
value_observer.Wait();
EXPECT_EQ(rfh_subframe_a, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 1);
EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 0);
}
{
TextInputManagerTypeObserver type_observer(web_contents(),
ui::TEXT_INPUT_TYPE_NONE);
// 4) Navigating to |url_2| should reset type to TEXT_INPUT_TYPE_NONE and
// changed focus to the new page's main frame.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
type_observer.Wait();
// |rfh_a| and its subframes should get into the back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_TRUE(rfh_b->IsInBackForwardCache());
EXPECT_TRUE(rfh_subframe_a->IsInBackForwardCache());
EXPECT_NE(rfh_subframe_a, web_contents()->GetFocusedFrame());
}
{
// 5) Navigating back to |url_1|, we shouldn't restore the focus to the
// text input in the subframe (we will focus on the main frame |rfh_a|
// instead).
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 1);
EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 1);
}
{
TextInputManagerTypeObserver type_observer(web_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
TextInputManagerValueObserver value_observer(web_contents(), "A");
// 6) Press tab key to focus the <input> again.
SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer.Wait();
value_observer.Wait();
EXPECT_EQ(rfh_subframe_a, web_contents()->GetFocusedFrame());
EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 2);
EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 1);
}
}
// Tests that trying to focus on a BFCached cross-site iframe won't crash.
// See https://crbug.com/1250218.
// TODO(crbug.com/40856039): Flaky on linux tsan
#if BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER)
#define MAYBE_FocusSameSiteSubframeOnPagehide \
DISABLED_FocusSameSiteSubframeOnPagehide
#else
#define MAYBE_FocusSameSiteSubframeOnPagehide FocusSameSiteSubframeOnPagehide
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_FocusSameSiteSubframeOnPagehide) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL main_url(
embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
GURL main_url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to a page with a same-site iframe.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper rfh_1(current_frame_host());
EXPECT_EQ(rfh_1.get(), web_contents()->GetFocusedFrame());
// 2) Navigate away from the page while trying to focus the subframe on
// pagehide. The DidFocusFrame IPC should arrive after the page gets into
// BFCache and should be ignored by the browser. The focus after navigation
// should go to the new main frame.
EXPECT_TRUE(ExecJs(rfh_1.get(), R"(
window.onpagehide = function(e) {
document.getElementById("test_iframe").focus();
})"));
EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
EXPECT_TRUE(rfh_1->IsInBackForwardCache());
EXPECT_NE(rfh_1.get(), web_contents()->GetFocusedFrame());
EXPECT_EQ(current_frame_host(), web_contents()->GetFocusedFrame());
// 3) Navigate back to the page. The focus should be on the main frame.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_1.get(), web_contents()->GetFocusedFrame());
ExpectRestored(FROM_HERE);
}
// Tests that trying to focus on a BFCached cross-site iframe won't crash.
// See https://crbug.com/1250218.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
FocusCrossSiteSubframeOnPagehide) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
GURL main_url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to a page with a cross-site iframe.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper rfh_1(current_frame_host());
EXPECT_EQ(rfh_1.get(), web_contents()->GetFocusedFrame());
// 2) Navigate away from the page while trying to focus the subframe on
// pagehide. The DidFocusFrame IPC should arrive after the page gets into
// BFCache and should be ignored by the browser. The focus after navigation
// should go to the new main frame.
EXPECT_TRUE(ExecJs(rfh_1.get(), R"(
window.onpagehide = function(e) {
document.getElementById("child-0").focus();
})"));
EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
EXPECT_TRUE(rfh_1->IsInBackForwardCache());
EXPECT_NE(rfh_1.get(), web_contents()->GetFocusedFrame());
// 3) Navigate back to the page. The focus should be on the original page's
// main frame.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_1.get(), current_frame_host());
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MainDocumentCSPHeadersAreRestored) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL(
"a.com",
"/set-header?"
"Content-Security-Policy: frame-src 'none'"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A, which should set CSP.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
// Check that CSP was set.
{
const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp =
current_frame_host()
->policy_container_host()
->policies()
.content_security_policies;
EXPECT_EQ(1u, root_csp.size());
EXPECT_EQ("frame-src 'none'", root_csp[0]->header->header_value);
}
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) Navigate back and expect that the CSP headers are present on the main
// frame.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
ExpectRestored(FROM_HERE);
// Check that CSP was restored.
{
const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp =
current_frame_host()
->policy_container_host()
->policies()
.content_security_policies;
EXPECT_EQ(1u, root_csp.size());
EXPECT_EQ("frame-src 'none'", root_csp[0]->header->header_value);
}
}
// Check that sandboxed documents are cached and won't lose their sandbox flags
// after restoration.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, CspSandbox) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(
embedded_test_server()->GetURL("a.com",
"/set-header?"
"Content-Security-Policy: sandbox"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A, which should set CSP.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
{
const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp =
current_frame_host()
->policy_container_host()
->policies()
.content_security_policies;
ASSERT_EQ(1u, root_csp.size());
ASSERT_EQ("sandbox", root_csp[0]->header->header_value);
ASSERT_EQ(network::mojom::WebSandboxFlags::kAll,
current_frame_host()->active_sandbox_flags());
}
// 2) Navigate to B. Expect the previous RenderFrameHost to enter the bfcache.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
{
const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp =
current_frame_host()
->policy_container_host()
->policies()
.content_security_policies;
ASSERT_EQ(0u, root_csp.size());
ASSERT_EQ(network::mojom::WebSandboxFlags::kNone,
current_frame_host()->active_sandbox_flags());
}
// 3) Navigate back and expect the page to be restored, with the correct
// CSP and sandbox flags.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_EQ(current_frame_host(), rfh_a);
{
const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp =
current_frame_host()
->policy_container_host()
->policies()
.content_security_policies;
ASSERT_EQ(1u, root_csp.size());
ASSERT_EQ("sandbox", root_csp[0]->header->header_value);
ASSERT_EQ(network::mojom::WebSandboxFlags::kAll,
current_frame_host()->active_sandbox_flags());
}
}
// Check that about:blank is not cached.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, AboutBlankWillNotBeCached) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to about:blank.
GURL blank_url(url::kAboutBlankURL);
EXPECT_TRUE(NavigateToURL(shell(), blank_url));
RenderFrameHostImplWrapper rfh_blank(current_frame_host());
// 2) Navigate to a.com.
GURL url_a(embedded_test_server()->GetURL("a.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// 3) Navigate back to about:blank.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// This about:blank document does not have a SiteInstance and then loading a
// page on it doesn't swap the browsing instance.
if (ShouldCreateNewHostForAllFrames()) {
EXPECT_TRUE(rfh_blank.WaitUntilRenderFrameDeleted());
ExpectNotRestored(
{
BackForwardCacheMetrics::NotRestoredReason::kHTTPStatusNotOK,
BackForwardCacheMetrics::NotRestoredReason::kSchemeNotHTTPOrHTTPS,
BackForwardCacheMetrics::NotRestoredReason::
kBrowsingInstanceNotSwapped,
},
{}, {ShouldSwapBrowsingInstance::kNo_DoesNotHaveSite}, {}, {},
FROM_HERE);
} else {
EXPECT_FALSE(rfh_blank->IsInBackForwardCache());
ExpectNotRestored(
{
BackForwardCacheMetrics::NotRestoredReason::
kBrowsingInstanceNotSwapped,
},
{}, {ShouldSwapBrowsingInstance::kNo_DoesNotHaveSite}, {}, {},
FROM_HERE);
}
}
// Check that browsing instances are not swapped when a navigation redirects
// toward the last committed URL and the reasons are recorded correctly.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, RedirectToSelf) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigationControllerImpl& controller = web_contents()->GetController();
// 1) Navigate to a.com/empty.html.
GURL url_a(embedded_test_server()->GetURL("a.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
// 2) Navigate to the same page by redirection.
GURL url_a2(embedded_test_server()->GetURL(
"a.com", "/server-redirect-301?" + url_a.spec()));
EXPECT_TRUE(NavigateToURL(shell(), url_a2, url_a));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
EXPECT_EQ(2, controller.GetEntryCount());
if (ShouldCreateNewHostForAllFrames()) {
EXPECT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
} else {
EXPECT_FALSE(rfh_a->IsInBackForwardCache());
EXPECT_TRUE(rfh_a->GetSiteInstance()->IsRelatedSiteInstance(
rfh_b->GetSiteInstance()));
}
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
// 3) Navigate back to the previous page.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
// TODO(crbug.com/40760515): Investigate whether these navigation results are
// expected.
ExpectNotRestored(
{
BackForwardCacheMetrics::NotRestoredReason::
kBrowsingInstanceNotSwapped,
},
{}, {ShouldSwapBrowsingInstance::kNo_SameUrlNavigation}, {}, {},
FROM_HERE);
}
// Check that reloading doesn't affect the back-forward cache usage.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, ReloadDoesntAffectCache) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigationControllerImpl& controller = web_contents()->GetController();
// 1) Navigate to a.com.
GURL url_a(embedded_test_server()->GetURL("a.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
// 2) Navigate to b.com.
GURL url_b(embedded_test_server()->GetURL("b.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(url_b, controller.GetLastCommittedEntry()->GetURL());
// 3) Go back to a.com and reload.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
ExpectRestored(FROM_HERE);
// 4) Reload the tab.
web_contents()->GetController().Reload(content::ReloadType::NORMAL, false);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
// By reloading the tab, ShouldSwapBrowsingInstance::
// kNo_AlreadyHasMatchingBrowsingInstance is set once. This should be reset
// when the navigation 4)'s commit finishes and should not prevent putting the
// page into the back-forward cache.
//
// Note that SetBrowsingInstanceSwapResult might not be called for every
// navigation because we might not get to this point for some navigations,
// e.g. if the navigation uses a pre-existing RenderFrameHost and SiteInstance
// for navigation.
//
// TODO(crbug.com/40747698): Tie BrowsingInstanceSwapResult to
// NavigationRequest instead and move the SetBrowsingInstanceSwapResult call
// for navigations to happen at commit time instead.
// 5) Go forward to b.com and reload.
ASSERT_TRUE(HistoryGoForward(web_contents()));
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(url_b, controller.GetLastCommittedEntry()->GetURL());
// The page loaded at B) is correctly cached and restored. Reloading doesn't
// affect the cache usage.
ExpectRestored(FROM_HERE);
// 6) Go back to a.com.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL());
// The page loaded at 3) is correctly cached and restored. Reloading doesn't
// affect the cache usage.
ExpectRestored(FROM_HERE);
}
// Regression test for crbug.com/1183313. Checks that CommitNavigationParam's
// |has_user_gesture| value reflects the gesture from the latest navigation
// after the commit finished.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
SameDocumentNavAfterRestoringDocumentLoadedWithUserGesture) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_a_foo(embedded_test_server()->GetURL("a.com", "/title1.html#foo"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
NavigationControllerImpl& controller = web_contents()->GetController();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Initial navigation (so that we can initiate a navigation from renderer).
EXPECT_TRUE(NavigateToURL(shell(), start_url));
// 1) Navigate to A with user gesture.
{
FrameNavigateParamsCapturer params_capturer(root);
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_a));
params_capturer.Wait();
EXPECT_TRUE(params_capturer.has_user_gesture());
EXPECT_TRUE(root->current_frame_host()
->last_committed_common_params_has_user_gesture());
}
RenderFrameHostImpl* rfh_a = current_frame_host();
// 2) Navigate to B. A should be stored in the back-forward cache.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_FALSE(root->current_frame_host()
->last_committed_common_params_has_user_gesture());
// 3) GoBack to A. RenderFrameHost of A should be restored from the
// back-forward cache, and "has_user_gesture" is set to false correctly.
// Note that since this is a back-forward cache restore we create the
// DidCommitProvisionalLoadParams completely in the browser, so we got the
// correct value from the latest navigation. However, we did not update the
// renderer's navigation-related values, so the renderer's DocumentLoader
// still thinks the last "gesture" value is "true", which will get corrected
// on the next navigation.
{
FrameNavigateParamsCapturer params_capturer(root);
controller.GoBack();
params_capturer.Wait();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
// The navigation doesn't have user gesture.
EXPECT_FALSE(params_capturer.has_user_gesture());
EXPECT_FALSE(root->current_frame_host()
->last_committed_common_params_has_user_gesture());
}
// 4) Same-document navigation to A#foo without user gesture. At this point
// we will update the renderer's DocumentLoader's latest gesture value to
// "no user gesture", and we'll get the correct gesture value in
// DidCommitProvisionalLoadParams.
{
FrameNavigateParamsCapturer params_capturer(root);
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), url_a_foo));
params_capturer.Wait();
// The navigation doesn't have user gesture.
EXPECT_FALSE(params_capturer.has_user_gesture());
EXPECT_FALSE(root->current_frame_host()
->last_committed_common_params_has_user_gesture());
}
}
testing::Matcher<BackForwardCacheCanStoreTreeResult> MatchesTreeResult(
testing::Matcher<bool> same_origin,
GURL url) {
return testing::AllOf(
testing::Property("IsSameOrigin",
&BackForwardCacheCanStoreTreeResult::IsSameOrigin,
same_origin),
testing::Property("GetUrl", &BackForwardCacheCanStoreTreeResult::GetUrl,
url));
}
RenderFrameHostImpl* ChildFrame(RenderFrameHostImpl* rfh, int child_index) {
return rfh->child_at(child_index)->current_frame_host();
}
// Verifies that the reasons match those given and no others.
testing::Matcher<BackForwardCacheCanStoreDocumentResult>
BackForwardCacheBrowserTest::MatchesDocumentResult(
testing::Matcher<NotRestoredReasons> not_stored,
BlockListedFeatures block_listed) {
return testing::AllOf(
testing::Property(
"not_restored_reasons",
&BackForwardCacheCanStoreDocumentResult::not_restored_reasons,
not_stored),
testing::Property(
"blocklisted_features",
&BackForwardCacheCanStoreDocumentResult::blocklisted_features,
block_listed),
testing::Property(
"disabled_reasons",
&BackForwardCacheCanStoreDocumentResult::disabled_reasons,
BackForwardCacheCanStoreDocumentResult::DisabledReasonsMap()),
testing::Property(
"disallow_activation_reasons",
&BackForwardCacheCanStoreDocumentResult::disallow_activation_reasons,
std::set<uint64_t>()));
}
// Check the contents of the BackForwardCacheCanStoreTreeResult of a page.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, TreeResultFeatureUsage) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a, b, c)"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to a(a, b, c).
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh(current_frame_host());
// 2) Add a blocking feature to the main frame A and the sub frame B.
current_frame_host()
->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
current_frame_host()
->child_at(1)
->current_frame_host()
->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
GURL url_subframe_a = ChildFrame(rfh.get(), 0)->GetLastCommittedURL();
GURL url_subframe_b = ChildFrame(rfh.get(), 1)->GetLastCommittedURL();
GURL url_subframe_c = ChildFrame(rfh.get(), 2)->GetLastCommittedURL();
// 3) Initialize the reasons tree and navigate away to ensure that everything
// from the old frame has been destroyed.
BackForwardCacheCanStoreDocumentResultWithTree can_store_result =
web_contents()
->GetController()
.GetBackForwardCache()
.GetCurrentBackForwardCacheEligibility(rfh.get());
ASSERT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
// 4) Check IsSameOrigin() and GetUrl().
// a
EXPECT_THAT(*can_store_result.tree_reasons,
MatchesTreeResult(/*same_origin=*/true,
/*url=*/url_a));
// a->a
EXPECT_THAT(*can_store_result.tree_reasons->GetChildren().at(0),
MatchesTreeResult(/*same_origin=*/true,
/*url=*/url_subframe_a));
// a->b
EXPECT_THAT(*can_store_result.tree_reasons->GetChildren().at(1),
MatchesTreeResult(/*same_origin=*/false,
/*url=*/url_subframe_b));
// a->c
EXPECT_THAT(*can_store_result.tree_reasons->GetChildren().at(2),
MatchesTreeResult(/*same_origin=*/false,
/*url=*/url_subframe_c));
// 5) Check that the blocking reasons match.
// a
EXPECT_THAT(can_store_result.tree_reasons->GetDocumentResult(),
MatchesDocumentResult(
NotRestoredReasons({NotRestoredReason::kBlocklistedFeatures}),
BlockListedFeatures(
{blink::scheduler::WebSchedulerTrackedFeature::kDummy})));
// a->a
EXPECT_THAT(
can_store_result.tree_reasons->GetChildren().at(0)->GetDocumentResult(),
MatchesDocumentResult(NotRestoredReasons(),
BlockListedFeatures(BlockListedFeatures())));
// a->b
EXPECT_THAT(
can_store_result.tree_reasons->GetChildren().at(1)->GetDocumentResult(),
MatchesDocumentResult(
NotRestoredReasons({NotRestoredReason::kBlocklistedFeatures}),
BlockListedFeatures(
{blink::scheduler::WebSchedulerTrackedFeature::kDummy})));
// a->c
EXPECT_THAT(
can_store_result.tree_reasons->GetChildren().at(2)->GetDocumentResult(),
MatchesDocumentResult(NotRestoredReasons(),
BlockListedFeatures(BlockListedFeatures())));
}
// Check the contents of the BackForwardCacheCanStoreTreeResult of a page when
// it is evicted.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
TreeResultEvictionMainFrame) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to a.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// 2) Navigate to B and evict A by JavaScript execution.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
EvictByJavaScript(rfh_a.get());
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kJavaScriptExecution}, {}, {}, {}, {},
FROM_HERE);
EXPECT_THAT(GetTreeResult()->GetDocumentResult(),
MatchesDocumentResult(
NotRestoredReasons({NotRestoredReason::kJavaScriptExecution}),
BlockListedFeatures()));
}
// Check the contents of the BackForwardCacheCanStoreTreeResult of a page when
// its subframe is evicted.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
TreeResultEvictionSubFrame) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
RenderFrameHostImplWrapper rfh_b(
current_frame_host()->child_at(0)->current_frame_host());
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// 2) Navigate to C and evict A's subframe B by JavaScript execution.
ASSERT_TRUE(NavigateToURL(shell(), url_c));
EvictByJavaScript(rfh_b.get());
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kJavaScriptExecution}, {}, {}, {}, {},
FROM_HERE);
// Main frame result in the tree is empty.
EXPECT_THAT(
GetTreeResult()->GetDocumentResult(),
MatchesDocumentResult(NotRestoredReasons(), BlockListedFeatures()));
// Subframe result in the tree contains the reason.
EXPECT_THAT(GetTreeResult()->GetChildren().at(0)->GetDocumentResult(),
MatchesDocumentResult(
NotRestoredReasons({NotRestoredReason::kJavaScriptExecution}),
BlockListedFeatures()));
}
// Check the contents of the BackForwardCacheCanStoreTreeResult of a page when
// its subframe's subframe is evicted.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
TreeResultEvictionSubFramesSubframe) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
GURL url_d(embedded_test_server()->GetURL("d.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
RenderFrameHostImplWrapper rfh_c(current_frame_host()
->child_at(0)
->current_frame_host()
->child_at(0)
->current_frame_host());
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// 2) Navigate to D and evict C by JavaScript execution.
ASSERT_TRUE(NavigateToURL(shell(), url_d));
EvictByJavaScript(rfh_c.get());
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kJavaScriptExecution}, {}, {}, {}, {},
FROM_HERE);
// Main frame result in the tree is empty.
EXPECT_THAT(
GetTreeResult()->GetDocumentResult(),
MatchesDocumentResult(NotRestoredReasons(), BlockListedFeatures()));
// The first level subframe result in the tree is empty.
EXPECT_THAT(
GetTreeResult()->GetChildren().at(0)->GetDocumentResult(),
MatchesDocumentResult(NotRestoredReasons(), BlockListedFeatures()));
// The second level subframe result in the tree contains the reason.
EXPECT_THAT(GetTreeResult()
->GetChildren()
.at(0)
->GetChildren()
.at(0)
->GetDocumentResult(),
MatchesDocumentResult(
NotRestoredReasons({NotRestoredReason::kJavaScriptExecution}),
BlockListedFeatures()));
}
void BackForwardCacheBrowserTest::InstallUnloadHandlerOnMainFrame() {
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
localStorage["unload_run_count"] = 0;
window.onunload = () => {
localStorage["unload_run_count"] =
1 + parseInt(localStorage["unload_run_count"]);
};
)"));
EXPECT_EQ("0", GetUnloadRunCount());
}
void BackForwardCacheBrowserTest::InstallUnloadHandlerOnSubFrame() {
TestNavigationObserver navigation_observer(shell()->web_contents(), 1);
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
const iframeElement = document.createElement("iframe");
iframeElement.src = "%s";
document.body.appendChild(iframeElement);
)"));
navigation_observer.Wait();
RenderFrameHostImpl* subframe_render_frame_host =
current_frame_host()->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(subframe_render_frame_host, R"(
localStorage["unload_run_count"] = 0;
window.onunload = () => {
localStorage["unload_run_count"] =
1 + parseInt(localStorage["unload_run_count"]);
};
)"));
EXPECT_EQ("0", GetUnloadRunCount());
}
EvalJsResult BackForwardCacheBrowserTest::GetUnloadRunCount() {
return GetLocalStorage(current_frame_host(), "unload_run_count");
}
bool BackForwardCacheBrowserTest::AddBlocklistedFeature(RenderFrameHost* rfh) {
// Add kDummy as blocking feature.
RenderFrameHostImplWrapper rfh_a(rfh);
rfh_a->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
return true;
}
void BackForwardCacheBrowserTest::ExpectNotRestoredDueToBlocklistedFeature(
base::Location location) {
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kDummy}, {},
{}, {}, location);
}
const ukm::TestAutoSetUkmRecorder& BackForwardCacheBrowserTest::ukm_recorder() {
return *ukm_recorder_;
}
const base::HistogramTester& BackForwardCacheBrowserTest::histogram_tester() {
return *histogram_tester_;
}
// Ensure that psges with unload are only allowed to enter back/forward cache by
// default on Android.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, UnloadAllowedFlag) {
#if BUILDFLAG(IS_ANDROID)
ASSERT_TRUE(BackForwardCacheImpl::IsUnloadAllowed());
#else
ASSERT_FALSE(BackForwardCacheImpl::IsUnloadAllowed());
#endif
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
FrameWithBlocklistedFeatureNotCached) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to a page that contains a blocklisted feature.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
RenderFrameHostWrapper rfh(current_frame_host());
ASSERT_TRUE(AddBlocklistedFeature(rfh.get()));
// Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page with the unsupported feature should be deleted (not cached).
ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestoredDueToBlocklistedFeature(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
SubframeWithBlocklistedFeatureNotCached) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to a page with an iframe that contains a blocklisted feature.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)")));
RenderFrameHostWrapper rfh(
current_frame_host()->child_at(0)->current_frame_host());
ASSERT_TRUE(AddBlocklistedFeature(rfh.get()));
// Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page with the unsupported feature should be deleted (not cached).
ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestoredDueToBlocklistedFeature(FROM_HERE);
}
class BackForwardCacheBrowserUnloadHandlerTest
: public BackForwardCacheBrowserTest,
public ::testing::WithParamInterface<
std::tuple<bool, bool, bool, TestFrameType>> {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
if (IsUnloadAllowed()) {
EnableFeatureAndSetParams(kBackForwardCacheUnloadAllowed, "", "");
} else {
DisableFeature(kBackForwardCacheUnloadAllowed);
}
if (IsUnloadBlocklisted()) {
EnableFeatureAndSetParams(blink::features::kUnloadBlocklisted, "", "");
} else {
DisableFeature(blink::features::kUnloadBlocklisted);
}
if (IsUnloadDeprecationOptedOut()) {
EnableFeatureAndSetParams(blink::features::kDeprecateUnloadOptOut, "",
"");
} else {
DisableFeature(blink::features::kDeprecateUnloadOptOut);
}
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
bool IsUnloadAllowed() { return std::get<0>(GetParam()); }
bool IsUnloadBlocklisted() { return std::get<1>(GetParam()); }
bool IsUnloadDeprecationOptedOut() { return std::get<2>(GetParam()); }
TestFrameType GetTestFrameType() { return std::get<3>(GetParam()); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Ensure that unload handlers in main frames and subframes block caching,
// depending on unload deprecation status and OS.
IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserUnloadHandlerTest,
UnloadHandlerPresent) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
BackForwardCacheCanStoreDocumentResult::NotRestoredReasons
expected_blocking_reasons;
std::vector<blink::scheduler::WebSchedulerTrackedFeature>
expected_blocklisted_reason;
if (IsUnloadBlocklisted()) {
expected_blocking_reasons.Put(
BackForwardCacheMetrics::NotRestoredReason::kBlocklistedFeatures);
expected_blocklisted_reason.push_back(
blink::scheduler::WebSchedulerTrackedFeature::kUnloadHandler);
}
switch (GetTestFrameType()) {
case content::TestFrameType::kMainFrame:
InstallUnloadHandlerOnMainFrame();
expected_blocking_reasons.Put(BackForwardCacheMetrics::NotRestoredReason::
kUnloadHandlerExistsInMainFrame);
break;
case content::TestFrameType::kSubFrame:
InstallUnloadHandlerOnSubFrame();
expected_blocking_reasons.Put(BackForwardCacheMetrics::NotRestoredReason::
kUnloadHandlerExistsInSubFrame);
break;
default:
NOTREACHED();
}
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
bool unload_never_blocks = IsUnloadAllowed();
bool unload_deprecated_and_not_opted_out =
(base::FeatureList::IsEnabled(network::features::kDeprecateUnload) &&
!IsUnloadDeprecationOptedOut());
if (unload_never_blocks || unload_deprecated_and_not_opted_out) {
// Pages with unload handlers are eligible for bfcache only if it is
// specifically allowed (happens on Android). Also, when unload is
// deprecated and `kDeprecateUnloadOptOut` doesn't override it, unload
// handlers cannot be installed so there should be no blocker for BFCache.
ExpectRestored(FROM_HERE);
EXPECT_EQ("0", GetUnloadRunCount());
} else {
ExpectNotRestored(expected_blocking_reasons, expected_blocklisted_reason,
{}, {}, {}, FROM_HERE);
EXPECT_EQ("1", GetUnloadRunCount());
}
}
// First param: whether unload is allowed or not.
// Second one: whether unload is blocklisted or not.
// Third one: whether it's opted out from unload deprecation or not.
INSTANTIATE_TEST_SUITE_P(
All,
BackForwardCacheBrowserUnloadHandlerTest,
::testing::Combine(::testing::Bool(),
::testing::Bool(),
::testing::Bool(),
::testing::Values(TestFrameType::kMainFrame,
TestFrameType::kSubFrame)));
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, DisableForRenderFrameHost) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostWrapper rfh_wrapper_a(current_frame_host());
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostWrapper rfh_wrapper_b(current_frame_host());
// Regardless of whether the source Id is set or not, it shouldn't affect the
// result of the BFCache eviction.
BackForwardCache::DisabledReason test_reason =
BackForwardCacheDisable::DisabledReason(
BackForwardCacheDisable::DisabledReasonId::kUnknown);
// 3) Disable BFCache for A with UKM source Id and go back.
BackForwardCache::DisableForRenderFrameHost(
rfh_wrapper_a.get(), test_reason, ukm::UkmRecorder::GetNewSourceID());
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_TRUE(rfh_wrapper_a.WaitUntilRenderFrameDeleted());
// Page A should be evicted properly.
ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason::
kDisableForRenderFrameHostCalled},
{}, {}, {test_reason}, {}, FROM_HERE);
// 4) Disable BFCache for B without UKM source Id and go forward.
BackForwardCache::DisableForRenderFrameHost(rfh_wrapper_b.get(), test_reason);
ASSERT_TRUE(HistoryGoForward(web_contents()));
ASSERT_TRUE(rfh_wrapper_b.WaitUntilRenderFrameDeleted());
// Page B should be evicted properly.
ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason::
kDisableForRenderFrameHostCalled},
{}, {}, {test_reason}, {}, FROM_HERE);
}
namespace {
enum class SubframeType { SameSite, CrossSite };
}
class BackForwardCacheEvictionDueToSubframeNavigationBrowserTest
: public BackForwardCacheBrowserTest,
public ::testing::WithParamInterface<SubframeType> {
public:
// Provides meaningful param names instead of /0 and /1.
static std::string DescribeParams(
const ::testing::TestParamInfo<ParamType>& info) {
switch (info.param) {
case SubframeType::SameSite:
return "SameSite";
case SubframeType::CrossSite:
return "CrossSite";
}
}
protected:
bool UseCrossOriginSubframe() const {
return GetParam() == SubframeType::CrossSite;
}
};
IN_PROC_BROWSER_TEST_P(
BackForwardCacheEvictionDueToSubframeNavigationBrowserTest,
SubframePendingCommitShouldPreventCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL subframe_url = embedded_test_server()->GetURL(
UseCrossOriginSubframe() ? "b.com" : "a.com", "/title1.html");
IsolateOriginsForTesting(embedded_test_server(), web_contents(),
std::vector<std::string>{"a.com", "b.com"});
// 1) Navigate to a.com.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
RenderFrameHostImpl* main_frame = current_frame_host();
// 2) Add subframe and wait for empty document to commit.
CreateSubframe(web_contents(), "child", GURL(""), true);
CommitMessageDelayer commit_message_delayer(
web_contents(), subframe_url,
base::BindLambdaForTesting([&](RenderFrameHost*) {
// 5) Test that page cannot be stored in bfcache when subframe is
// pending commit.
BackForwardCacheCanStoreDocumentResultWithTree can_store_result =
web_contents()
->GetController()
.GetBackForwardCache()
.GetCurrentBackForwardCacheEligibility(
static_cast<RenderFrameHostImpl*>(main_frame));
EXPECT_TRUE(can_store_result.flattened_reasons.HasNotRestoredReason(
BackForwardCacheMetrics::NotRestoredReason::kSubframeIsNavigating));
}));
// 3) Start navigation in subframe to |subframe_url|.
ExecuteScriptAsync(
main_frame,
JsReplace("document.querySelector('#child').src = $1;", subframe_url));
// 4) Wait until subframe navigation is pending commit.
commit_message_delayer.Wait();
}
// Check that when the main frame gets BFCached while the subframe navigation
// deferring NavigationThrottles has already ran, the issue that the subframe
// navigation escapes the throttle deferral is addressed.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheEvictionDueToSubframeNavigationBrowserTest,
MainFrameCommitFirstAndSubframePendingCommitShouldBeEvicted) {
ASSERT_TRUE(embedded_test_server()->Start());
// Prepare the main frame and the sub frame, where both of them are same-site.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
FrameTreeNode* child = root->child_at(0);
RenderFrameHostImplWrapper child_rfh(child->current_frame_host());
// Navigate both frames simultaneously.
const std::string subframe_origin =
UseCrossOriginSubframe() ? "b.com" : "a.com";
GURL new_url_1(
embedded_test_server()->GetURL(subframe_origin, "/title1.html"));
GURL new_url_2(
embedded_test_server()->GetURL(subframe_origin, "/title2.html"));
TestNavigationManager manager1(web_contents(), new_url_1);
TestNavigationManager manager2(web_contents(), new_url_2);
auto script = JsReplace("location = $1; frames[0].location = $2;", new_url_1,
new_url_2);
EXPECT_TRUE(ExecJs(web_contents(), script));
// Wait for main frame request, but don't commit it yet. This should create
// a speculative RenderFrameHost.
ASSERT_TRUE(manager1.WaitForRequestStart());
// Wait for subframe request, but don't commit it yet.
ASSERT_TRUE(manager2.WaitForRequestStart());
// Now let the main frame commit.
ASSERT_TRUE(manager1.WaitForNavigationFinished());
// Make sure the main frame is at the new URL.
ASSERT_TRUE(root->current_frame_host()->IsRenderFrameLive());
ASSERT_EQ(new_url_1, root->current_frame_host()->GetLastCommittedURL());
// The subframe should be gone now: it should have been evicted from BFCache
// because the subframe is still navigating; otherwise, this will cause a
// crash.
ASSERT_TRUE(manager2.WaitForNavigationFinished());
EXPECT_TRUE(child_rfh.IsDestroyed());
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{BackForwardCacheMetrics::NotRestoredReason::kSubframeIsNavigating}, {},
{}, {}, {}, FROM_HERE);
}
INSTANTIATE_TEST_SUITE_P(
All,
BackForwardCacheEvictionDueToSubframeNavigationBrowserTest,
::testing::Values(SubframeType::SameSite, SubframeType::CrossSite),
&BackForwardCacheEvictionDueToSubframeNavigationBrowserTest::
DescribeParams);
namespace {
enum class SubframeNavigationType { WithoutURLLoader, WithURLLoader };
}
// Test for pages which has subframe(s) with ongoing navigation(s).
class BackForwardCacheWithSubframeNavigationBrowserTest
: public BackForwardCacheBrowserTest {
protected:
void SetUpOnMainThread() override {
BackForwardCacheBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
EnableFeatureAndSetParams(features::kBackForwardCache, "cache_size",
base::NumberToString(2));
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
// Start a subframe navigation and pause it when we get the confirmation
// dialog triggered by beforeunload event, which is before
// WillCommitWithoutUrlLoader or WillStartRequest.
void NavigateSubframeAndPauseAtBeforeUnload(
BeforeUnloadBlockingDelegate& beforeunload_pauser,
RenderFrameHostImpl* sub_rfh,
const GURL& subframe_navigate_url,
std::string_view iframe_id) {
ASSERT_TRUE(ExecJs(sub_rfh, R"(
window.addEventListener('beforeunload', e =>
e.returnValue='blocked'
);)"));
// Start a subframe navigation which will trigger the beforeunload dialog
// that pauses that navigation. Using `BeginNavigateIframeToURL` is
// necessary here, since we pause this navigation on beforeunload event. So,
// we don't want to wait for the navigation to finish.
BeginNavigateIframeToURL(web_contents(), iframe_id, subframe_navigate_url);
beforeunload_pauser.Wait();
}
// Start a subframe navigation and pause it before `DidCommitNavigation`.
void NavigateSubframeAndPauseAtDidCommit(FrameTreeNode* ftn,
const GURL& subframe_navigate_url) {
// Enforce the creation of speculative RFH to correctly wait for
// the commit event.
SpeculativeRenderFrameHostObserver observer(web_contents(),
subframe_navigate_url);
// We have to pause a navigation before `DidCommitNavigation`, so we don't
// want to wait for the navigation to finish.
ASSERT_TRUE(BeginNavigateToURLFromRenderer(ftn, subframe_navigate_url));
// Navigation without a URL loader shall always create the speculative RFH
// immediately or never create one.
// The same-site navigation to the subframe will not create a new
// speculative RFH if render document is not enabled for subframes.
if (subframe_navigate_url.SchemeIsHTTPOrHTTPS() &&
ShouldCreateNewRenderFrameHostOnSameSiteNavigation(
/*is_main_frame=*/false, /*is_local_root=*/true)) {
observer.Wait();
}
// Wait until the navigation is pending commit. Note that the navigation
// might use a speculative RenderFrameHost, so use that if necessary.
RenderFrameHostImpl* speculative_rfh =
ftn->render_manager()->speculative_frame_host();
CommitNavigationPauser commit_pauser(
speculative_rfh ? speculative_rfh : ftn->current_frame_host());
commit_pauser.WaitForCommitAndPause();
}
// Put a page which has a subframe with a navigation which hasn't reached the
// "pending commit" stage nor sent a network request into BackForwardCache and
// confirm the subframe navigation has been deferred.
void BFCachePageWithSubframeNavigationBeforeDidStartNavigation(
const GURL& main_frame_navigate_url,
const GURL& subframe_navigate_url,
RenderFrameHostImplWrapper& sub_rfh,
TestNavigationManager& subframe_navigation_manager,
std::string_view iframe_id) {
FrameTreeNode* child_ftn =
web_contents()->GetPrimaryFrameTree().root()->child_at(0);
{
BeforeUnloadBlockingDelegate beforeunload_pauser(web_contents());
NavigateSubframeAndPauseAtBeforeUnload(beforeunload_pauser, sub_rfh.get(),
subframe_navigate_url, iframe_id);
// Subframe navigation is ongoing, so `NavigateToURL` cannot be used since
// this function waits for all frames including subframe to finish
// loading.
ASSERT_TRUE(NavigateToURLFromRenderer(sub_rfh->GetMainFrame(),
main_frame_navigate_url));
// The subframe navigation hasn't reached the "pending commit" stage nor
// sent a network request, so the page is eligible for BackForwardCache.
EXPECT_TRUE(sub_rfh->GetMainFrame()->IsInBackForwardCache());
EXPECT_TRUE(sub_rfh->IsInBackForwardCache());
}
web_contents()->SetDelegate(shell());
// Wait until the subframe navigation is deferred.
ASSERT_TRUE(
subframe_navigation_manager.WaitForFirstYieldAfterDidStartNavigation());
NavigationRequest* child_navigation = child_ftn->navigation_request();
ASSERT_NE(child_navigation, nullptr);
EXPECT_TRUE(child_navigation->IsDeferredForTesting());
}
};
class BackForwardCacheWithSubframeNavigationWithParamBrowserTest
: public BackForwardCacheWithSubframeNavigationBrowserTest,
public ::testing::WithParamInterface<SubframeNavigationType> {
public:
// Provides meaningful param names instead of /0 and /1.
static std::string DescribeParams(
const ::testing::TestParamInfo<ParamType>& info) {
switch (info.param) {
case SubframeNavigationType::WithoutURLLoader:
return "WithoutURLLoader";
case SubframeNavigationType::WithURLLoader:
return "WithURLLoader";
}
}
};
// Confirm that BackForwardCache is blocked when there is only 1 navigation and
// it's pending commit.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithSubframeNavigationWithParamBrowserTest,
SubframeNavigationWithPendingCommitShouldPreventCache) {
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
const GURL subframe_url = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
const GURL navigate_url(
embedded_test_server()->GetURL("c.com", "/title1.html"));
const GURL subframe_navigate_url =
GetParam() == SubframeNavigationType::WithURLLoader
? embedded_test_server()->GetURL("b.com", "/title1.html")
: GURL("about:blank");
// Navigate to a page with a cross site iframe.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
FrameTreeNode* child_node = main_rfh.get()->child_at(0);
RenderFrameHostImplWrapper sub_rfh(child_node->current_frame_host());
// Pause subframe's navigation before `DidCommitNavigation`.
NavigateSubframeAndPauseAtDidCommit(child_node, subframe_navigate_url);
// Subframe navigation is ongoing, so `NavigateToURL` cannot be used since
// this function waits for all frames including subframe to finish loading.
ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url));
// Subframe navigation has reached the "pending commit" stage, so the page is
// not eligible for BackForwardCache.
EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(sub_rfh.WaitUntilRenderFrameDeleted());
// Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {},
FROM_HERE);
// Confirm that subframe's url didn't change.
EXPECT_EQ(subframe_url, current_frame_host()->child_at(0)->current_url());
}
// Confirm that BackForwardCache is blocked when there are 2 navigations, 1 not
// pending commit yet, and 1 pending commit.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheWithSubframeNavigationBrowserTest,
MultipleSubframeNavigationWithBeforeAndPendingCommitShouldPreventCache) {
// This test relies on the main frame and the iframe to live in different
// processes. This allows one renderer process to proceed a navigation while
// the other renderer process is busy executing its beforeunload handler.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,c)"));
const GURL subframe_b_url = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
const GURL subframe_c_url = embedded_test_server()->GetURL(
"c.com", "/cross_site_iframe_factory.html?c()");
const GURL navigate_url(
embedded_test_server()->GetURL("d.com", "/title1.html"));
const GURL subframe_navigate_url = GURL("about:blank");
// Navigate to a page with two cross site iframes.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh_b(
main_rfh.get()->child_at(0)->current_frame_host());
RenderFrameHostImplWrapper sub_rfh_c(
main_rfh.get()->child_at(1)->current_frame_host());
{
// The subframe_b itself does have a dialog-showing beforeunload handler.
// Pause subframe_b's navigation when we get the confirmation dialog
// triggered by beforeunload event.
BeforeUnloadBlockingDelegate beforeunload_pauser(web_contents());
NavigateSubframeAndPauseAtBeforeUnload(beforeunload_pauser, sub_rfh_b.get(),
subframe_navigate_url,
/*iframe_id=*/"child-0");
// Pause subframe_c's navigation before `DidCommitNavigation`.
NavigateSubframeAndPauseAtDidCommit(main_rfh.get()->child_at(1),
subframe_navigate_url);
// Subframe navigation is ongoing, so `NavigateToURL` cannot be used since
// this function waits for all frames including subframe to finish loading.
ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url));
// The subframe_c's navigation already started committing, so the page is
// not eligible for BackForwardCache.
EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(sub_rfh_b.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(sub_rfh_c.WaitUntilRenderFrameDeleted());
}
web_contents()->SetDelegate(shell());
// Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {},
FROM_HERE);
// Confirm that subframe's url didn't change.
EXPECT_EQ(subframe_b_url, current_frame_host()->child_at(0)->current_url());
EXPECT_EQ(subframe_c_url, current_frame_host()->child_at(1)->current_url());
}
// Confirm that BackForwardCache is blocked when there are 2 navigations, 1 has
// not sent a network request yet, and 1 has already sent request.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheWithSubframeNavigationBrowserTest,
MultipleSubframeNavigationWithBeforeAndAfterSendingRequestShouldPreventCache) {
// This test relies on the main frame and the iframe to live in different
// processes. This allows one renderer process to proceed a navigation while
// the other renderer process is busy executing its beforeunload handler.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,c)"));
const GURL subframe_b_url = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
const GURL subframe_c_url = embedded_test_server()->GetURL(
"c.com", "/cross_site_iframe_factory.html?c()");
const GURL navigate_url(
embedded_test_server()->GetURL("d.com", "/title1.html"));
const GURL subframe_b_navigate_url(
embedded_test_server()->GetURL("b.com", "/title1.html"));
const GURL subframe_c_navigate_url(
embedded_test_server()->GetURL("c.com", "/title1.html"));
// Navigate to a page with two cross site iframes.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh_b(
main_rfh.get()->child_at(0)->current_frame_host());
RenderFrameHostImplWrapper sub_rfh_c(
main_rfh.get()->child_at(1)->current_frame_host());
// Pause a subframe_b navigation on `WillStartRequest` before sending a
// network request.
TestNavigationManager subframe_b_navigation_manager(web_contents(),
subframe_b_navigate_url);
ASSERT_TRUE(
BeginNavigateToURLFromRenderer(sub_rfh_b.get(), subframe_b_navigate_url));
ASSERT_TRUE(subframe_b_navigation_manager.WaitForRequestStart());
// Pause a subframe_c navigation on `WillProcessResponse` after sending a
// network request.
TestNavigationManager subframe_c_navigation_manager(web_contents(),
subframe_c_navigate_url);
ASSERT_TRUE(
BeginNavigateToURLFromRenderer(sub_rfh_c.get(), subframe_c_navigate_url));
ASSERT_TRUE(subframe_c_navigation_manager.WaitForResponse());
// Subframe navigation is ongoing, so `NavigateToURL` cannot be used since
// this function waits for all frames including subframe to finish loading.
ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url));
// The subframe_c's navigation has already sent a network request, so the page
// is not eligible for BackForwardCache.
EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(sub_rfh_b.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(sub_rfh_c.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(subframe_b_navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(subframe_c_navigation_manager.WaitForNavigationFinished());
EXPECT_FALSE(subframe_b_navigation_manager.was_committed());
EXPECT_FALSE(subframe_c_navigation_manager.was_committed());
// Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {},
FROM_HERE);
// Confirm that subframe's url didn't change.
EXPECT_EQ(subframe_b_url, current_frame_host()->child_at(0)->current_url());
EXPECT_EQ(subframe_c_url, current_frame_host()->child_at(1)->current_url());
}
// Confirm that subframe navigation which needs url loader that has already sent
// a network request should block BackForwardCache.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheWithSubframeNavigationBrowserTest,
SubframeNavigationWithUrlLoaderAfterSendingRequestShouldPreventCache) {
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
const GURL subframe_url = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
const GURL navigate_url(
embedded_test_server()->GetURL("c.com", "/title1.html"));
const GURL subframe_navigate_url(
embedded_test_server()->GetURL("b.com", "/title2.html"));
// Navigate to a page with a cross site iframe.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh(
main_rfh.get()->child_at(0)->current_frame_host());
TestNavigationManager subframe_navigation_manager(web_contents(),
subframe_navigate_url);
ASSERT_TRUE(
BeginNavigateToURLFromRenderer(sub_rfh.get(), subframe_navigate_url));
// Pause the subframe navigation on `WillProcessResponse`.
ASSERT_TRUE(subframe_navigation_manager.WaitForResponse());
// Subframe navigation is ongoing, so `NavigateToURL` cannot be used since
// this function waits for all frames including subframe to finish loading.
ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url));
// Subframe navigation has already sent a network request, so the page is not
// eligible for BackForwardCache.
EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted());
EXPECT_TRUE(sub_rfh.WaitUntilRenderFrameDeleted());
EXPECT_FALSE(subframe_navigation_manager.was_committed());
// Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {},
FROM_HERE);
// Confirm that subframe's url didn't change.
EXPECT_EQ(subframe_url, current_frame_host()->child_at(0)->current_url());
}
// Confirm that subframe navigation which needs url loader that hasn't sent a
// network request should not block BackForwardCache.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheWithSubframeNavigationBrowserTest,
SubframeNavigationWithUrlLoaderBeforeSendingRequestShouldNotPreventCache) {
// This test relies on the main frame and the iframe to live in different
// processes. This allows one renderer process to proceed a navigation while
// the other renderer process is busy executing its beforeunload handler.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
const GURL subframe_url = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
const GURL navigate_url(
embedded_test_server()->GetURL("c.com", "/title1.html"));
const GURL subframe_navigate_url(
embedded_test_server()->GetURL("b.com", "/title1.html"));
// Navigate to a page with a cross site iframe.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh(
main_rfh.get()->child_at(0)->current_frame_host());
// Put a page which has a subframe with a URLLoader navigation which hasn't
// sent a network request into BackForwardCache. The iframe itself
// does have a dialog-showing beforeunload handler.
TestNavigationManager subframe_navigation_manager(web_contents(),
subframe_navigate_url);
BFCachePageWithSubframeNavigationBeforeDidStartNavigation(
navigate_url, subframe_navigate_url, sub_rfh, subframe_navigation_manager,
/*iframe_id=*/"child-0");
// Navigate back.
TestNavigationObserver back_load_observer(shell()->web_contents());
web_contents()->GetController().GoBack();
back_load_observer.WaitForNavigationFinished();
ASSERT_FALSE(main_rfh->IsInBackForwardCache());
// Wait until the resumed subframe navigation finishes.
EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(subframe_navigation_manager.was_successful());
EXPECT_EQ(subframe_navigate_url,
current_frame_host()->child_at(0)->current_url());
}
// Confirm that subframe no-url loader navigation (e.g., about:blank) in
// bfcached page is deferred and then resumed when the page is navigated back.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheWithSubframeNavigationBrowserTest,
SubframeNavigationWithoutUrlLoaderBeforeCommitShouldNotPreventCache) {
// This test relies on the main frame and the iframe to live in different
// processes. This allows one renderer process to proceed a navigation while
// the other renderer process is busy executing its beforeunload handler.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
const GURL navigate_url(
embedded_test_server()->GetURL("c.com", "/title1.html"));
const GURL subframe_navigate_url = GURL("about:blank");
// Navigate to a page with a cross site iframe.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh(
main_rfh.get()->child_at(0)->current_frame_host());
// Put a page which has a subframe with a no-URLLoader navigation which hasn't
// reached the "pending commit" stage into BackForwardCache. The iframe itself
// does have a dialog-showing beforeunload handler.
TestNavigationManager subframe_navigation_manager(web_contents(),
subframe_navigate_url);
BFCachePageWithSubframeNavigationBeforeDidStartNavigation(
navigate_url, subframe_navigate_url, sub_rfh, subframe_navigation_manager,
/*iframe_id=*/"child-0");
// Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_FALSE(main_rfh->IsInBackForwardCache());
// Confirm the deferred navigation was resumed and subframe's url changed.
EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(subframe_navigation_manager.was_successful());
EXPECT_EQ(subframe_navigate_url,
current_frame_host()->child_at(0)->current_url());
}
// Confirm that we don't resume a subframe navigation when an unrelated BFCached
// page gets restored.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithSubframeNavigationWithParamBrowserTest,
SubframeNavigationShouldNotBeResumedWhenUnrelatedPageRestored) {
// This test relies on the main frame and the iframe to live in different
// processes. This allows one renderer process to proceed a navigation while
// the other renderer process is busy executing its beforeunload handler.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
const GURL main_url_a(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
const GURL navigate_url_c(
embedded_test_server()->GetURL("c.com", "/title1.html"));
const GURL navigate_url_d(
embedded_test_server()->GetURL("d.com", "/title1.html"));
const GURL subframe_navigate_url =
GetParam() == SubframeNavigationType::WithURLLoader
? embedded_test_server()->GetURL("b.com", "/title1.html")
: GURL("about:blank");
// Navigate to a page with a cross site iframe.
ASSERT_TRUE(NavigateToURL(shell(), main_url_a));
RenderFrameHostImplWrapper main_rfh_a(current_frame_host());
RenderFrameHostImplWrapper sub_rfh_b(
main_rfh_a.get()->child_at(0)->current_frame_host());
// Put a page which has a subframe with a navigation which hasn't reached the
// "pending commit" stage or sent a network request into BackForwardCache.
TestNavigationManager subframe_navigation_manager(web_contents(),
subframe_navigate_url);
BFCachePageWithSubframeNavigationBeforeDidStartNavigation(
navigate_url_c, subframe_navigate_url, sub_rfh_b,
subframe_navigation_manager,
/*iframe_id=*/"child-0");
// Navigate away.
// Currently, `main_rfh_a` is in BFCache and we are on `navigate_url_c`. Then,
// we will navigate to `navigate_url_d` which will put `main_rfh_c` in
// BFCache.
RenderFrameHostImplWrapper main_rfh_c(current_frame_host());
ASSERT_TRUE(NavigateToURL(shell(), navigate_url_d));
ASSERT_TRUE(main_rfh_c->IsInBackForwardCache());
// Navigate back to `main_rfh_c` and restore that from BFCache, while
// `main_rfh_a` is still in BFCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_EQ(main_rfh_c.get(), current_frame_host());
ASSERT_TRUE(main_rfh_a->IsInBackForwardCache());
// Confirm the subframe's deferred navigation is not committed.
EXPECT_FALSE(subframe_navigation_manager.was_committed());
// Navigate back to `main_rfh_a`.
TestNavigationObserver back_load_observer(shell()->web_contents());
web_contents()->GetController().GoBack();
back_load_observer.WaitForNavigationFinished();
ASSERT_FALSE(main_rfh_a->IsInBackForwardCache());
// Confirm the deferred navigation was resumed and subframe's url changed.
EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(subframe_navigation_manager.was_successful());
EXPECT_EQ(subframe_navigate_url,
current_frame_host()->child_at(0)->current_url());
}
// Evict the bfcached page which has a subframe with a deferred navigation and
// confirm the subframe'url didn't change when the page is navigated back.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithSubframeNavigationWithParamBrowserTest,
EvictBFCachedPageWithDeferredSubframeNavigationBeforeCommit) {
// This test relies on the main frame and the iframe to live in different
// processes. This allows one renderer process to proceed a navigation while
// the other renderer process is busy executing its beforeunload handler.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
const GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
const GURL subframe_url = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
const GURL navigate_url(
embedded_test_server()->GetURL("c.com", "/title1.html"));
const GURL subframe_navigate_url =
GetParam() == SubframeNavigationType::WithURLLoader
? embedded_test_server()->GetURL("b.com", "/title1.html")
: GURL("about:blank");
// Navigate to a page with a cross site iframe.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh(
main_rfh.get()->child_at(0)->current_frame_host());
// Put a page which has a subframe with a navigation which hasn't reached the
// "pending commit" stage or sent a network request into BackForwardCache. The
// iframe itself does have a dialog-showing beforeunload handler.
TestNavigationManager subframe_navigation_manager(web_contents(),
subframe_navigate_url);
BFCachePageWithSubframeNavigationBeforeDidStartNavigation(
navigate_url, subframe_navigate_url, sub_rfh, subframe_navigation_manager,
/*iframe_id=*/"child-0");
// Flush the cache and evict the previously BFCached page.
web_contents()->GetController().GetBackForwardCache().Flush();
ASSERT_TRUE(main_rfh.WaitUntilRenderFrameDeleted());
ASSERT_TRUE(sub_rfh.WaitUntilRenderFrameDeleted());
// Confirm the subframe's deferred navigation has finished and was not
// committed.
EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished());
EXPECT_FALSE(subframe_navigation_manager.was_committed());
// Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Confirm that subframe's url didn't change.
EXPECT_EQ(subframe_url, current_frame_host()->child_at(0)->current_url());
}
INSTANTIATE_TEST_SUITE_P(
All,
BackForwardCacheWithSubframeNavigationWithParamBrowserTest,
::testing::Values(SubframeNavigationType::WithoutURLLoader,
SubframeNavigationType::WithURLLoader),
&BackForwardCacheWithSubframeNavigationWithParamBrowserTest::
DescribeParams);
class BackForwardCacheFencedFrameBrowserTest
: public BackForwardCacheBrowserTest {
public:
BackForwardCacheFencedFrameBrowserTest() = default;
~BackForwardCacheFencedFrameBrowserTest() override = default;
BackForwardCacheFencedFrameBrowserTest(
const BackForwardCacheFencedFrameBrowserTest&) = delete;
BackForwardCacheFencedFrameBrowserTest& operator=(
const BackForwardCacheFencedFrameBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
fenced_frame_helper_ = std::make_unique<test::FencedFrameTestHelper>();
}
test::FencedFrameTestHelper& fenced_frame_test_helper() {
return *fenced_frame_helper_;
}
private:
std::unique_ptr<test::FencedFrameTestHelper> fenced_frame_helper_;
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheFencedFrameBrowserTest,
FencedFramePageNotStoredInBackForwardCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(
embedded_test_server()->GetURL("b.com", "/fenced_frames/title1.html"));
GURL url_c(
embedded_test_server()->GetURL("c.com", "/fenced_frames/title1.html"));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// 2) Create a fenced frame.
content::RenderFrameHostImpl* fenced_frame_host =
static_cast<content::RenderFrameHostImpl*>(
fenced_frame_test_helper().CreateFencedFrame(
web_contents()->GetPrimaryMainFrame(), url_b));
RenderFrameHostWrapper fenced_frame_host_wrapper(fenced_frame_host);
// 3) Navigate to C on the fenced frame host.
fenced_frame_test_helper().NavigateFrameInFencedFrameTree(fenced_frame_host,
url_c);
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
if (!fenced_frame_host_wrapper.IsRenderFrameDeleted())
EXPECT_FALSE(fenced_frame_host->IsInBackForwardCache());
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
RendererInitiatedNavigateToSameUrl) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
// 3) Navigate to B again, renderer initiated.
ASSERT_TRUE(NavigateToURLFromRenderer(rfh_b.get(), url_b));
RenderFrameHostImplWrapper rfh_b2(current_frame_host());
// This is treated as replacement, and the previous B page did not get into
// back/forward cache.
if (ShouldCreateNewHostForAllFrames()) {
EXPECT_TRUE(rfh_b.WaitUntilRenderFrameDeleted());
} else {
EXPECT_FALSE(rfh_b->IsInBackForwardCache());
EXPECT_EQ(rfh_b.get(), rfh_b2.get());
}
// 4) Go back. Make sure we go back to A instead of B and restore from
// bfcache.
ASSERT_TRUE(HistoryGoBack(shell()->web_contents()));
EXPECT_EQ(current_frame_host(), rfh_a.get());
EXPECT_TRUE(rfh_b2.get()->IsInBackForwardCache());
ExpectRestored(FROM_HERE);
// 5) Go forward and restore from bfcache.
ASSERT_TRUE(HistoryGoForward(shell()->web_contents()));
EXPECT_EQ(current_frame_host(), rfh_b2.get());
ExpectRestored(FROM_HERE);
}
// BEFORE ADDING A NEW TEST HERE
// Read the note at the top about the other files you could add it to.
} // namespace content