blob: f5a6b3db5b0f3d8ca928a57560d8461d9178e16f [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/containers/contains.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/back_forward_cache_browsertest.h"
#include "content/browser/bluetooth/bluetooth_adapter_factory_wrapper.h"
#include "content/browser/bluetooth/test/mock_bluetooth_delegate.h"
#include "content/browser/browser_interface_binders.h"
#include "content/browser/generic_sensor/frame_sensor_provider_proxy.h"
#include "content/browser/generic_sensor/web_contents_sensor_provider_proxy.h"
#include "content/browser/hid/hid_test_utils.h"
#include "content/browser/presentation/presentation_test_utils.h"
#include "content/browser/renderer_host/back_forward_cache_disable.h"
#include "content/browser/renderer_host/media/media_devices_dispatcher_host.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/worker_host/dedicated_worker_hosts_for_document.h"
#include "content/public/browser/disallow_activation_reason.h"
#include "content/public/browser/hid_delegate.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/payment_app_provider.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/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/media_start_stop_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_transport_simple_test_server.h"
#include "content/shell/browser/shell.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "net/test/test_data_directory.h"
#include "services/device/public/cpp/test/fake_hid_manager.h"
#include "services/device/public/cpp/test/fake_sensor_and_provider.h"
#include "services/device/public/cpp/test/scoped_geolocation_overrider.h"
#include "services/device/public/mojom/vibration_manager.mojom.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
#include "third_party/blink/public/mojom/app_banner/app_banner.mojom.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "ui/base/idle/idle_time_provider.h"
#include "ui/base/test/idle_test_utils.h"
// This file contains back-/forward-cache tests for web-platform features and
// APIs. It was forked from
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/back_forward_cache_browsertest.cc;drc=1288c1bd6a81785cd85b965d61820a7cd87a0e9c
//
// When adding tests for new features please also add WPTs. See
// third_party/blink/web_tests/external/wpt/html/browsers/browsing-the-web/back-forward-cache/README.md
namespace content {
using ::testing::_;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Not;
using ::testing::Return;
using ::testing::UnorderedElementsAreArray;
using NotRestoredReason = BackForwardCacheMetrics::NotRestoredReason;
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
PageWithDedicatedWorkerCachedOrNot) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(NavigateToURL(
shell(),
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_dedicated_worker.html")));
ASSERT_EQ(42, EvalJs(current_frame_host(), "window.receivedMessagePromise"));
RenderFrameHostWrapper rfh(current_frame_host());
// Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// Go back
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Check the outcome.
EXPECT_EQ(rfh.get(), current_frame_host());
ExpectRestored(FROM_HERE);
}
// The bool parameter is used for switching PlzDedicatedWorker.
class BackForwardCacheWithDedicatedWorkerBrowserTest
: public BackForwardCacheBrowserTest,
public testing::WithParamInterface<bool> {
public:
const int kMaxBufferedBytesPerProcess = 10000;
const base::TimeDelta kGracePeriodToFinishLoading = base::Seconds(5);
BackForwardCacheWithDedicatedWorkerBrowserTest() { server_.Start(); }
void SetUpCommandLine(base::CommandLine* command_line) override {
if (IsPlzDedicatedWorkerEnabled()) {
EnableFeatureAndSetParams(blink::features::kPlzDedicatedWorker, "", "");
} else {
DisableFeature(blink::features::kPlzDedicatedWorker);
}
// Disable the feature to test eviction for dedicated worker.
DisableFeature(
blink::features::kAllowDatapipeDrainedAsBytesConsumerInBFCache);
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kLoadingTasksUnfreezable,
{{"max_buffered_bytes_per_process",
base::NumberToString(kMaxBufferedBytesPerProcess)},
{"grace_period_to_finish_loading_in_seconds",
base::NumberToString(kGracePeriodToFinishLoading.InSeconds())}}}},
{});
server_.SetUpCommandLine(command_line);
}
bool IsPlzDedicatedWorkerEnabled() { return GetParam(); }
int port() const { return server_.server_address().port(); }
int CountWorkerClients(RenderFrameHostImpl* rfh) {
return EvalJs(rfh, JsReplace(R"(
new Promise(async (resolve) => {
const resp = await fetch('/service_worker/count_worker_clients');
resolve(parseInt(await resp.text(), 10));
});
)"))
.ExtractInt();
}
private:
base::test::ScopedFeatureList feature_list_;
WebTransportSimpleTestServer server_;
};
INSTANTIATE_TEST_SUITE_P(All,
BackForwardCacheWithDedicatedWorkerBrowserTest,
testing::Bool());
// Confirms that a page using a dedicated worker is cached.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
CacheWithDedicatedWorker) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(),
https_server()->GetURL(
"a.test", "/back_forward_cache/page_with_dedicated_worker.html")));
EXPECT_EQ(42, EvalJs(current_frame_host(), "window.receivedMessagePromise"));
// Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
// Go back to the original page.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Confirms that an active page using a dedicated worker that calls
// importScripts won't trigger an eviction IPC, causing the page to reload.
// Regression test for https://crbug.com/1305041.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
PageWithDedicatedWorkerAndImportScriptsWontTriggerReload) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL(
"a.test",
"/back_forward_cache/"
"page_with_dedicated_worker_and_importscripts.html")));
// Wait until the importScripts() call finished running.
EXPECT_EQ(42, EvalJs(current_frame_host(), "window.receivedMessagePromise"));
// If the importScripts() call triggered an eviction, a reload will be
// triggered due to the "evict after docment is restored" will be hit, as the
// page is not in back/forward cache.
EXPECT_FALSE(
web_contents()->GetPrimaryFrameTree().root()->navigation_request());
}
// Confirms that a page using a dedicated worker with WebTransport is not
// cached.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
DoNotCacheWithDedicatedWorkerWithWebTransport) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL(
"a.test",
"/back_forward_cache/"
"page_with_dedicated_worker_and_webtransport.html")));
// Open a WebTransport.
EXPECT_EQ("opened",
EvalJs(current_frame_host(),
JsReplace("window.testOpenWebTransport($1);", port())));
RenderFrameDeletedObserver delete_observer_rfh(current_frame_host());
// Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
delete_observer_rfh.WaitUntilDeleted();
// Go back to the original page. The page was not cached as the worker used
// WebTransport.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebTransport}, {}, {}, {},
FROM_HERE);
}
// Confirms that a page using a dedicated worker with a closed WebTransport is
// cached as WebTransport is not a sticky feature.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
CacheWithDedicatedWorkerWithWebTransportClosed) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL(
"a.test",
"/back_forward_cache/"
"page_with_dedicated_worker_and_webtransport.html")));
// Open and close a WebTransport.
EXPECT_EQ("opened",
EvalJs(current_frame_host(),
JsReplace("window.testOpenWebTransport($1);", port())));
EXPECT_EQ("closed",
EvalJs(current_frame_host(), "window.testCloseWebTransport();"));
// Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
// Go back to the original page. The page was cached. Even though WebTransport
// is used once, the page is eligible for back-forward cache as the feature is
// not sticky.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// TODO(crbug.com/40823301): Flaky on Linux.
#if BUILDFLAG(IS_LINUX)
#define MAYBE_DoNotCacheWithDedicatedWorkerWithWebTransportAndDocumentWithBlockingFeature \
DISABLED_DoNotCacheWithDedicatedWorkerWithWebTransportAndDocumentWithBlockingFeature
#else
#define MAYBE_DoNotCacheWithDedicatedWorkerWithWebTransportAndDocumentWithBlockingFeature \
DoNotCacheWithDedicatedWorkerWithWebTransportAndDocumentWithBlockingFeature
#endif
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
MAYBE_DoNotCacheWithDedicatedWorkerWithWebTransportAndDocumentWithBlockingFeature) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL(
"a.test",
"/back_forward_cache/"
"page_with_dedicated_worker_and_webtransport.html")));
// Open a WebTransport in the dedicated worker.
EXPECT_EQ("opened",
EvalJs(current_frame_host(),
JsReplace("window.testOpenWebTransport($1);", port())));
// testOpenWebTransport sends the IPC (BackForwardCacheController.
// DidChangeBackForwardCacheDisablingFeatures) from a renderer. Run a script
// to wait for the IPC reaching to the browser.
EXPECT_EQ(42, EvalJs(current_frame_host(), "42;"));
EXPECT_TRUE(
DedicatedWorkerHostsForDocument::GetOrCreateForCurrentDocument(
current_frame_host())
->GetBackForwardCacheDisablingFeatures()
.HasAll(
{blink::scheduler::WebSchedulerTrackedFeature::kWebTransport}));
// Use a blocking feature in the frame.
EXPECT_TRUE(ExecJs(current_frame_host(), kBlockingScript));
RenderFrameDeletedObserver delete_observer_rfh(current_frame_host());
// Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
delete_observer_rfh.WaitUntilDeleted();
// Go back to the original page. The page was not cached due to WebTransport
// and a broadcast channel, which came from the dedicated worker and the frame
// respectively. Confirm both are recorded.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebTransport,
kBlockingReasonEnum},
{}, {}, {}, FROM_HERE);
}
// TODO(crbug.com/40821593): Disabled due to being flaky.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
DISABLED_DoNotCacheWithDedicatedWorkerWithClosedWebTransportAndDocumentWithBroadcastChannel) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL(
"a.test",
"/back_forward_cache/"
"page_with_dedicated_worker_and_webtransport.html")));
// Open and close a WebTransport in the dedicated worker.
EXPECT_EQ("opened",
EvalJs(current_frame_host(),
JsReplace("window.testOpenWebTransport($1);", port())));
// testOpenWebTransport sends the IPC (BackForwardCacheController.
// DidChangeBackForwardCacheDisablingFeatures) from a renderer. Run a script
// to wait for the IPC reaching to the browser.
EXPECT_EQ(42, EvalJs(current_frame_host(), "42;"));
EXPECT_TRUE(
DedicatedWorkerHostsForDocument::GetOrCreateForCurrentDocument(
current_frame_host())
->GetBackForwardCacheDisablingFeatures()
.HasAll(
{blink::scheduler::WebSchedulerTrackedFeature::kWebTransport}));
EXPECT_EQ("closed",
EvalJs(current_frame_host(),
JsReplace("window.testCloseWebTransport($1);", port())));
// testOpenWebTransport sends the IPC (BackForwardCacheController.
// DidChangeBackForwardCacheDisablingFeatures) from a renderer. Run a script
// to wait for the IPC reaching to the browser.
EXPECT_EQ(42, EvalJs(current_frame_host(), "42;"));
EXPECT_TRUE(DedicatedWorkerHostsForDocument::GetOrCreateForCurrentDocument(
current_frame_host())
->GetBackForwardCacheDisablingFeatures()
.empty());
// Use a broadcast channel in the frame.
EXPECT_TRUE(ExecJs(current_frame_host(),
"window.foo = new BroadcastChannel('foo');"));
RenderFrameDeletedObserver delete_observer_rfh(current_frame_host());
// Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
delete_observer_rfh.WaitUntilDeleted();
// Go back to the original page. The page was not cached due to a broadcast
// channel, which came from the frame. WebTransport was used once in the
// dedicated worker but was closed, then this doesn't affect the cache usage.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kBroadcastChannel}, {}, {},
{}, FROM_HERE);
}
// Tests the case when the page starts fetching in a dedicated worker, goes to
// BFcache, and then a redirection happens. The cached page should evicted in
// this case.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
FetchRedirectedWhileStoring) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse fetch1_response(https_server(),
"/fetch1");
net::test_server::ControllableHttpResponse fetch2_response(https_server(),
"/fetch2");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Trigger a fetch in a dedicated worker.
std::string worker_script =
JsReplace(R"(
fetch($1);
)",
https_server()->GetURL("a.test", "/fetch1"));
EXPECT_TRUE(ExecJs(rfh_a, JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
worker_script)));
fetch1_response.WaitForRequest();
// Navigate to B.
PageLifecycleStateManagerTestDelegate delegate(
rfh_a->render_view_host()->GetPageLifecycleStateManager());
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(delegate.WaitForInBackForwardCacheAck());
// Page A is initially stored in the back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Respond the fetch with a redirect.
fetch1_response.Send(
"HTTP/1.1 302 Moved Temporarily\r\n"
"Location: /fetch2\r\n\r\n");
fetch1_response.Done();
// Ensure that the request to /fetch2 was never sent (because the page is
// immediately evicted) by checking after 3 seconds.
base::RunLoop loop1;
base::OneShotTimer timer1;
timer1.Start(FROM_HERE, base::Seconds(3), loop1.QuitClosure());
loop1.Run();
EXPECT_EQ(nullptr, fetch2_response.http_request());
// Page A should be evicted from the back-forward cache.
delete_observer_rfh_a.WaitUntilDeleted();
// Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kNetworkRequestRedirected}, {}, {}, {},
{}, FROM_HERE);
}
// Tests the case when the page starts fetching in a nested dedicated worker,
// goes to BFcache, and then a redirection happens. The cached page should
// evicted in this case.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
FetchRedirectedWhileStoring_Nested) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse fetch1_response(https_server(),
"/fetch1");
net::test_server::ControllableHttpResponse fetch2_response(https_server(),
"/fetch2");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Trigger a fetch in a nested dedicated worker.
std::string child_worker_script =
JsReplace(R"(
fetch($1);
)",
https_server()->GetURL("a.test", "/fetch1"));
std::string parent_worker_script = JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
child_worker_script);
EXPECT_TRUE(ExecJs(rfh_a, JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
worker.onmessage = () => { resolve(); }
)",
parent_worker_script)));
fetch1_response.WaitForRequest();
// Navigate to B.
PageLifecycleStateManagerTestDelegate delegate(
rfh_a->render_view_host()->GetPageLifecycleStateManager());
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(delegate.WaitForInBackForwardCacheAck());
// Page A is initially stored in the back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Respond the fetch with a redirect.
fetch1_response.Send(
"HTTP/1.1 302 Moved Temporarily\r\n"
"Location: /fetch2\r\n\r\n");
fetch1_response.Done();
// Ensure that the request to /fetch2 was never sent (because the page is
// immediately evicted) by checking after 3 seconds.
base::RunLoop loop2;
base::OneShotTimer timer2;
timer2.Start(FROM_HERE, base::Seconds(3), loop2.QuitClosure());
loop2.Run();
EXPECT_EQ(nullptr, fetch2_response.http_request());
// Page A should be evicted from the back-forward cache.
delete_observer_rfh_a.WaitUntilDeleted();
// Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kNetworkRequestRedirected}, {}, {}, {},
{}, FROM_HERE);
}
// Tests the case when the page starts fetching in a dedicated worker, goes to
// BFcache, and then the response amount reaches the threshold. The cached page
// should evicted in this case.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
FetchStillLoading_ResponseStartedWhileFrozen_ExceedsPerProcessBytesLimit) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse image_response(https_server(),
"/image.png");
ASSERT_TRUE(https_server()->Start());
// Navigate to a page.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("a.test", "/title1.html")));
RenderFrameHostImpl* rfh_a = current_frame_host();
// Trigger a fetch in a dedicated worker.
std::string worker_script =
JsReplace(R"(
fetch($1);
)",
https_server()->GetURL("a.test", "/image.png"));
EXPECT_TRUE(ExecJs(rfh_a, JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
worker_script)));
// Wait for the image request, but don't send anything yet.
image_response.WaitForRequest();
// Navigate away.
PageLifecycleStateManagerTestDelegate delegate(
rfh_a->render_view_host()->GetPageLifecycleStateManager());
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title2.html")));
ASSERT_TRUE(delegate.WaitForInBackForwardCacheAck());
// The worker was still loading when we navigated away, but it's still
// eligible for back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Start sending the image response while in the back-forward cache.
image_response.Send(net::HTTP_OK, "image/png");
std::string body(kMaxBufferedBytesPerProcess + 1, '*');
image_response.Send(body);
image_response.Done();
delete_observer_rfh_a.WaitUntilDeleted();
// Go back to the first page. We should not restore the page from the
// back-forward cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kNetworkExceedsBufferLimit}, {}, {}, {},
{}, FROM_HERE);
}
// Tests the case when the page starts fetching in a nested dedicated worker,
// goes to BFcache, and then the response amount reaches the threshold. The
// cached page should evicted in this case.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
FetchStillLoading_ResponseStartedWhileFrozen_ExceedsPerProcessBytesLimit_Nested) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse image_response(https_server(),
"/image.png");
ASSERT_TRUE(https_server()->Start());
// Navigate to a page.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("a.test", "/title1.html")));
RenderFrameHostImpl* rfh_a = current_frame_host();
// Trigger a fetch in a nested dedicated worker.
std::string child_worker_script =
JsReplace(R"(
fetch($1);
)",
https_server()->GetURL("a.test", "/image.png"));
std::string parent_worker_script = JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
child_worker_script);
EXPECT_TRUE(ExecJs(rfh_a, JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
parent_worker_script)));
// Wait for the image request, but don't send anything yet.
image_response.WaitForRequest();
// Navigate away.
PageLifecycleStateManagerTestDelegate delegate(
rfh_a->render_view_host()->GetPageLifecycleStateManager());
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title2.html")));
ASSERT_TRUE(delegate.WaitForInBackForwardCacheAck());
// The worker was still loading when we navigated away, but it's still
// eligible for back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Start sending the image response while in the back-forward cache.
image_response.Send(net::HTTP_OK, "image/png");
std::string body(kMaxBufferedBytesPerProcess + 1, '*');
image_response.Send(body);
image_response.Done();
delete_observer_rfh_a.WaitUntilDeleted();
// Go back to the first page. We should not restore the page from the
// back-forward cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kNetworkExceedsBufferLimit}, {}, {}, {},
{}, FROM_HERE);
}
// Tests the case when fetching started in a dedicated worker and the header was
// received before the page is frozen, but parts of the response body is
// received when the page is frozen.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse fetch_response(https_server(),
"/fetch");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call fetch in a dedicated worker before navigating away.
std::string worker_script =
JsReplace("fetch($1)", https_server()->GetURL("a.test", "/fetch"));
EXPECT_TRUE(ExecJs(rfh_a.get(), JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
worker_script)));
// Send response header and a piece of the body. This receiving the response
// doesn't end (i.e. Done is not called) before navigating away. In this case,
// the page will be evicted when the page is frozen.
fetch_response.WaitForRequest();
fetch_response.Send(net::HTTP_OK, "text/plain");
fetch_response.Send("body");
// Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// Go back to A. kNetworkRequestDatapipeDrainedAsBytesConsumer is recorded
// since receiving the response body started but this didn't end before the
// navigation to B.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kNetworkRequestDatapipeDrainedAsBytesConsumer}, {},
{}, {}, {}, FROM_HERE);
}
// Tests the case when fetching started in a nested dedicated worker and the
// header was received before the page is frozen, but parts of the response body
// is received when the page is frozen.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted_Nested) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse fetch_response(https_server(),
"/fetch");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call fetch in a nested dedicated worker before navigating away.
std::string child_worker_script =
JsReplace("fetch($1)", https_server()->GetURL("a.test", "/fetch"));
std::string parent_worker_script = JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
child_worker_script);
EXPECT_TRUE(ExecJs(rfh_a.get(), JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
parent_worker_script)));
// Send response header and a piece of the body. This receiving the response
// doesn't end (i.e. Done is not called) before navigating away. In this case,
// the page will be evicted when the page is frozen.
fetch_response.WaitForRequest();
fetch_response.Send(net::HTTP_OK, "text/plain");
fetch_response.Send("body");
// Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// Go back to A. kNetworkRequestDatapipeDrainedAsBytesConsumer is recorded
// since receiving the response body started but this didn't end before the
// navigation to B.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kNetworkRequestDatapipeDrainedAsBytesConsumer}, {},
{}, {}, {}, FROM_HERE);
}
// Tests the case when fetch started in a dedicated worker, but the response
// never ends after the page is frozen. This should result in an eviction due to
// timeout.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
ImageStillLoading_ResponseStartedWhileFrozen_Timeout) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse image_response(https_server(),
"/image.png");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call fetch in a dedicated worker before navigating away.
std::string worker_script =
JsReplace(R"(
fetch($1);
)",
https_server()->GetURL("a.test", "/image.png"));
EXPECT_TRUE(ExecJs(rfh_a.get(), JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
worker_script)));
// Wait for the image request, but don't send anything yet.
image_response.WaitForRequest();
// Navigate away.
PageLifecycleStateManagerTestDelegate delegate(
rfh_a->render_view_host()->GetPageLifecycleStateManager());
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(delegate.WaitForInBackForwardCacheAck());
// The page was still loading when we navigated away, but it's still eligible
// for back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Start sending the image response while in the back-forward cache, but never
// finish the request. Eventually the page will get deleted due to network
// request timeout.
image_response.Send(net::HTTP_OK, "image/png");
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back to the first page. We should not restore the page from the
// back-forward cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kNetworkRequestTimeout}, {}, {}, {}, {},
FROM_HERE);
}
// Tests the case when fetch started in a nested dedicated worker, but the
// response never ends after the page is frozen. This should result in an
// eviction due to timeout.
IN_PROC_BROWSER_TEST_P(
BackForwardCacheWithDedicatedWorkerBrowserTest,
ImageStillLoading_ResponseStartedWhileFrozen_Timeout_Nested) {
CreateHttpsServer();
net::test_server::ControllableHttpResponse image_response(https_server(),
"/image.png");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call fetch in a dedicated worker before navigating away.
std::string child_worker_script =
JsReplace(R"(
fetch($1);
)",
https_server()->GetURL("a.test", "/image.png"));
std::string parent_worker_script = JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
child_worker_script);
EXPECT_TRUE(ExecJs(rfh_a.get(), JsReplace(R"(
const blob = new Blob([$1]);
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
)",
parent_worker_script)));
// Wait for the image request, but don't send anything yet.
image_response.WaitForRequest();
// Navigate away.
PageLifecycleStateManagerTestDelegate delegate(
rfh_a->render_view_host()->GetPageLifecycleStateManager());
EXPECT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(delegate.WaitForInBackForwardCacheAck());
// The page was still loading when we navigated away, but it's still eligible
// for back-forward cache.
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Start sending the image response while in the back-forward cache, but never
// finish the request. Eventually the page will get deleted due to network
// request timeout.
image_response.Send(net::HTTP_OK, "image/png");
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back to the first page. We should not restore the page from the
// back-forward cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kNetworkRequestTimeout}, {}, {}, {}, {},
FROM_HERE);
}
// Tests that dedicated workers in back/forward cache are not visible to a
// service worker.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
ServiceWorkerClientMatchAll) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
GURL url_a1(https_server()->GetURL(
"a.test", "/service_worker/create_service_worker.html"));
GURL url_a2(https_server()->GetURL("a.test", "/service_worker/empty.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a1));
EXPECT_EQ(
"DONE",
EvalJs(current_frame_host(),
"register('/service_worker/fetch_event_worker_clients.js');"));
// Reload the page to enable fetch to be hooked by the service worker.
web_contents()->GetController().Reload(content::ReloadType::NORMAL, false);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Confirm there is no worker client.
EXPECT_EQ(0, CountWorkerClients(rfh_a.get()));
// Call fetch in a dedicated worker. If the PlzDedicatedWorker is enabled, the
// number of worker clients should be 1. If PlzDedicatedWorker is disabled,
// worker clients are not supported, so the number should be 0.
int expected_number = IsPlzDedicatedWorkerEnabled() ? 1 : 0;
std::string dedicated_worker_script = JsReplace(
R"(
(async() => {
const response = await fetch($1);
postMessage(await response.text());
})();
)",
https_server()->GetURL("a.test", "/service_worker/count_worker_clients"));
EXPECT_EQ(base::NumberToString(expected_number),
EvalJs(rfh_a.get(), JsReplace(R"(
new Promise(async (resolve) => {
const blobURL = URL.createObjectURL(new Blob([$1]));
const dedicatedWorker = new Worker(blobURL);
dedicatedWorker.addEventListener('message', e => {
resolve(e.data);
});
});
)",
dedicated_worker_script)));
// Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_a2));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Confirm that the worker in back/forward cache is invisible from the service
// worker.
EXPECT_EQ(0, CountWorkerClients(current_frame_host()));
// Restore from the back/forward cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(expected_number, CountWorkerClients(current_frame_host()));
}
// Tests that dedicated workers, including a nested dedicated workers, in
// back/forward cache are not visible to a service worker.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
ServiceWorkerClientMatchAll_Nested) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
GURL url_a1(https_server()->GetURL(
"a.test", "/service_worker/create_service_worker.html"));
GURL url_a2(https_server()->GetURL("a.test", "/service_worker/empty.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a1));
EXPECT_EQ(
"DONE",
EvalJs(current_frame_host(),
"register('/service_worker/fetch_event_worker_clients.js');"));
// Reload the page to enable fetch to be hooked by the service worker.
web_contents()->GetController().Reload(content::ReloadType::NORMAL, false);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Confirm there is no worker client.
EXPECT_EQ(0, CountWorkerClients(rfh_a.get()));
// Call fetch in a dedicated worker. If the PlzDedicatedWorker is enabled, the
// number of worker clients should be 2. If PlzDedicatedWorker is disabled,
// worker clients are not supported, so the number should be 0.
int expected_number = IsPlzDedicatedWorkerEnabled() ? 2 : 0;
std::string child_worker_script = JsReplace(
R"(
(async() => {
const response = await fetch($1);
postMessage(await response.text());
})();
)",
https_server()->GetURL("a.test", "/service_worker/count_worker_clients"));
std::string parent_worker_script = JsReplace(
R"(
const blobURL = URL.createObjectURL(new Blob([$1]));
const dedicatedWorker = new Worker(blobURL);
dedicatedWorker.addEventListener('message', e => {
postMessage(e.data);
});
)",
child_worker_script);
EXPECT_EQ(base::NumberToString(expected_number),
EvalJs(rfh_a.get(), JsReplace(R"(
new Promise(async (resolve) => {
const blobURL = URL.createObjectURL(new Blob([$1]));
const dedicatedWorker = new Worker(blobURL);
dedicatedWorker.addEventListener('message', e => {
resolve(e.data);
});
});
)",
parent_worker_script)));
// Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_a2));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Confirm that the worker in back/forward cache is invisible from the service
// worker.
EXPECT_EQ(0, CountWorkerClients(current_frame_host()));
// Restore from the back/forward cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(expected_number, CountWorkerClients(current_frame_host()));
}
// Tests that dedicated workers in back/forward cache are not visible to a
// service worker. This works correctly even if a dedicated worker is not loaded
// completely when the page is put into back/forward cache,
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithDedicatedWorkerBrowserTest,
ServiceWorkerClientMatchAll_LoadWorkerAfterRestoring) {
CreateHttpsServer();
// Prepare a controllable HTTP response for a dedicated worker. Use
// /service_worker path to match with the service worker's scope.
net::test_server::ControllableHttpResponse dedicated_worker_response(
https_server(),
"/service_worker/dedicated_worker_using_service_worker.js");
ASSERT_TRUE(https_server()->Start());
GURL url_a1(https_server()->GetURL(
"a.test", "/service_worker/create_service_worker.html"));
GURL url_a2(https_server()->GetURL("a.test", "/service_worker/empty.html"));
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a1));
EXPECT_EQ(
"DONE",
EvalJs(current_frame_host(),
"register('/service_worker/fetch_event_worker_clients.js');"));
// Reload the page to enable fetch to be hooked by the service worker.
web_contents()->GetController().Reload(content::ReloadType::NORMAL, false);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Confirm there is no worker client.
EXPECT_EQ(0, CountWorkerClients(rfh_a.get()));
// Start to requet a worker URL.
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
window.dedicatedWorkerUsingServiceWorker = new Worker(
'/service_worker/dedicated_worker_using_service_worker.js');
)"));
dedicated_worker_response.WaitForRequest();
// Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_a2));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Return the dedicated worker script.
dedicated_worker_response.Send(net::HTTP_OK, "text/javascript");
dedicated_worker_response.Send(R"(
onmessage = e => {
postMessage(e.data);
};
)");
dedicated_worker_response.Done();
// Confirm that the worker in back/forward cache is invisible from the service
// worker.
EXPECT_EQ(0, CountWorkerClients(current_frame_host()));
// Restore from the back/forward cache. Now the number of client is 1.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// Confirm that the dedicated worker is completely loaded.
EXPECT_EQ("foo", EvalJs(current_frame_host(), JsReplace(R"(
new Promise(async (resolve) => {
window.dedicatedWorkerUsingServiceWorker.onmessage = e => {
resolve(e.data);
};
window.dedicatedWorkerUsingServiceWorker.postMessage("foo");
});
)")));
// If the PlzDedicatedWorker is enabled, the number of worker clients should
// be 1. If PlzDedicatedWorker is disabled, worker clients are not supported,
// so the number should be 0.
EXPECT_EQ(IsPlzDedicatedWorkerEnabled() ? 1 : 0,
CountWorkerClients(current_frame_host()));
}
// TODO(crbug.com/40290702): Shared workers are not available on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_PageWithSharedWorkerNotCached \
DISABLED_PageWithSharedWorkerNotCached
#else
#define MAYBE_PageWithSharedWorkerNotCached PageWithSharedWorkerNotCached
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_PageWithSharedWorkerNotCached) {
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(),
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_shared_worker.html")));
RenderFrameDeletedObserver delete_observer_rfh_a(current_frame_host());
// 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).
delete_observer_rfh_a.WaitUntilDeleted();
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kSharedWorker}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
AllowedFeaturesForSubframesDoNotEvict) {
// The main purpose of this test is to check that when a state of a subframe
// is updated, CanStoreDocument is still called for the main frame - otherwise
// we would always evict the document, even when the feature is allowed as
// CanStoreDocument always returns false for non-main frames.
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));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameHostImpl* rfh_b = rfh_a->child_at(0)->current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b);
// 2) Navigate to C.
ASSERT_TRUE(NavigateToURL(shell(), url_c));
// 3) No-op feature update on a subframe while in cache, should be no-op.
ASSERT_FALSE(delete_observer_rfh_b.deleted());
RenderFrameHostImpl::BackForwardCacheBlockingDetails empty_vector;
rfh_b->DidChangeBackForwardCacheDisablingFeatures(std::move(empty_vector));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(current_frame_host(), rfh_a);
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheIfRecordingAudio) {
ASSERT_TRUE(embedded_test_server()->Start());
BackForwardCacheDisabledTester tester;
// Navigate to an empty page.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Request for audio recording.
EXPECT_EQ("success", EvalJs(current_frame_host(), R"(
new Promise(resolve => {
navigator.mediaDevices.getUserMedia({audio: true})
.then(m => { window.keepaliveMedia = m; resolve("success"); })
.catch(() => { resolve("error"); });
});
)"));
RenderFrameDeletedObserver deleted(current_frame_host());
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page was still recording audio when we navigated away, so it shouldn't
// have been cached.
deleted.WaitUntilDeleted();
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// A MediaStreamTrack that's in the live state
// will block BFCache.
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kLiveMediaStreamTrack}, {},
{}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheIfSubframeRecordingAudio) {
ASSERT_TRUE(embedded_test_server()->Start());
BackForwardCacheDisabledTester tester;
// Navigate to a page with an iframe.
GURL url(embedded_test_server()->GetURL("/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh = current_frame_host();
// Request for audio recording from the subframe.
EXPECT_EQ("success", EvalJs(rfh->child_at(0)->current_frame_host(), R"(
new Promise(resolve => {
navigator.mediaDevices.getUserMedia({audio: true})
.then(m => { resolve("success"); })
.catch(() => { resolve("error"); });
});
)"));
RenderFrameDeletedObserver deleted(current_frame_host());
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page was still recording audio when we navigated away, so it shouldn't
// have been cached.
deleted.WaitUntilDeleted();
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// A MediaStreamTrack that's in the live state blocks BFCache.
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kLiveMediaStreamTrack}, {},
{}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheIfMediaDeviceSubscribedButDoesCache) {
ASSERT_TRUE(embedded_test_server()->Start());
BackForwardCacheDisabledTester tester;
// Navigate to a page with an iframe.
GURL url(embedded_test_server()->GetURL("/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh = current_frame_host();
EXPECT_EQ("success", EvalJs(rfh->child_at(0)->current_frame_host(), R"(
new Promise(resolve => {
navigator.mediaDevices.addEventListener(
'devicechange', function(event){});
resolve("success");
});
)"));
RenderFrameDeletedObserver deleted(current_frame_host());
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Ended MediaStreamTrack does not block BFCache.
ExpectRestored(FROM_HERE);
}
// Checks that the page is restored from BFCache when it calls
// mediaDevice.enumerateDevices().
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
RestoreIfDevicesEnumerated) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to an empty page.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostWrapper rfh(current_frame_host());
// Use the method enumerateDevices() of MediaDevices API.
EXPECT_EQ("success", EvalJs(rfh.get(), R"(
navigator.mediaDevices.enumerateDevices().then(() => {return "success"});
)"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// 3) Go back. MediaDevicesDispatcherHost does not block BFCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Checks that the page is not restored from BFCache when it calls
// mediaDevice.getDisplayMedia() and still has live MediaStreamTrack.
// Since mediaDevice.getDisplayMedia() is not supported in Android, the tests
// can't run on the OS.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheIfDisplayMediaAccessGranted) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to an empty page.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostWrapper rfh(current_frame_host());
// Request for video and audio display permission.
EXPECT_EQ("success", EvalJs(rfh.get(), R"(
new Promise((resolve) => {
navigator.mediaDevices.getDisplayMedia({audio: true, video: true})
.then(() => { resolve("success"); })
});
)"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
// 3) Go back. A MediaStreamTrack that's in the live state blocks BFCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kLiveMediaStreamTrack}, {},
{}, {}, FROM_HERE);
}
// Checks that the page is successfully restored from BFCache after stopping the
// MediaStreamTrack that was caused by getDisplayMedia().
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfMediaStreamTrackUsingGetDisplayMediaEnded) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to an empty page.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostWrapper rfh(current_frame_host());
// Request for video and audio display permission, and stop it.
EXPECT_EQ("success", EvalJs(rfh.get(), R"(
new Promise((resolve) => {
navigator.mediaDevices.getDisplayMedia({ audio: true })
.then((mediaStream) => {
mediaStream.getTracks().forEach((track) => track.stop());
resolve("success");
})
.catch((error) => {
resolve("error");
});
});
)"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// 3) Go back. an ended MediaStreamTrack doesn't
// block BFCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
#endif // !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, CacheIfWebGL) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with WebGL usage
GURL url(embedded_test_server()->GetURL(
"example.com", "/back_forward_cache/page_with_webgl.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page had an active WebGL context when we navigated away,
// but it should be cached.
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Since blink::mojom::HidService binder is not added in
// content/browser/browser_interface_binders.cc for Android, this test is not
// applicable for this OS.
#if !BUILDFLAG(IS_ANDROID)
class HidBrowserTestContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
MockHidDelegate& delegate() { return delegate_; }
// ContentBrowserClient:
HidDelegate* GetHidDelegate() override { return &delegate_; }
private:
testing::NiceMock<MockHidDelegate> delegate_;
};
class BackForwardCacheWebHidTest : public BackForwardCacheBrowserTest {
public:
void SetUpOnMainThread() override {
BackForwardCacheBrowserTest::SetUpOnMainThread();
test_client_ = std::make_unique<HidBrowserTestContentBrowserClient>();
ON_CALL(delegate(), GetHidManager).WillByDefault(Return(&hid_manager_));
}
void TearDownOnMainThread() override {
BackForwardCacheBrowserTest::TearDownOnMainThread();
test_client_.reset();
}
MockHidDelegate& delegate() { return test_client_->delegate(); }
device::FakeHidManager* hid_manager() { return &hid_manager_; }
private:
std::unique_ptr<HidBrowserTestContentBrowserClient> test_client_;
device::FakeHidManager hid_manager_;
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebHidTest,
DoesNotCacheIfGetDevicesWasCalled) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to an empty page.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
RenderFrameHostWrapper rfh_wrapper(current_frame_host());
// Call getDevices to get a list of devices the page is allowed to access.
EXPECT_EQ("success", EvalJs(current_frame_host(), R"(
new Promise(resolve => {
navigator.hid.getDevices()
.then(m => { resolve("success"); })
.catch(() => { resolve("error"); });
});
)"));
EXPECT_TRUE(current_frame_host()->GetBackForwardCacheDisablingFeatures().Has(
blink::scheduler::WebSchedulerTrackedFeature::kWebHID));
// Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page called a WebHID method so it should be deleted.
EXPECT_TRUE(rfh_wrapper.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebHID}, {},
{}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebHidTest,
DoesNotCacheIfRequestDeviceWasCalled) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to an empty page.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
RenderFrameHostWrapper rfh_wrapper(current_frame_host());
// Call requestDevice to open a permission request dialog. Simulate closing
// the dialog without selecting a device.
EXPECT_CALL(delegate(), CanRequestDevicePermission).WillOnce(Return(true));
EXPECT_CALL(delegate(), RunChooserInternal).WillOnce([]() {
return std::vector<device::mojom::HidDeviceInfoPtr>();
});
EXPECT_TRUE(ExecJs(current_frame_host(),
"navigator.hid.requestDevice({filters: []})"));
EXPECT_TRUE(current_frame_host()->GetBackForwardCacheDisablingFeatures().Has(
blink::scheduler::WebSchedulerTrackedFeature::kWebHID));
// Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page called a WebHID method so it should be deleted.
EXPECT_TRUE(rfh_wrapper.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebHID}, {},
{}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebHidTest,
DoesCacheIfHidAttributeWasAccessed) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to an empty page.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
RenderFrameHostWrapper rfh_a(current_frame_host());
// Access navigator.hid without invoking any WebHID API methods.
EXPECT_TRUE(ExecJs(current_frame_host(), "navigator.hid"));
EXPECT_FALSE(current_frame_host()->GetBackForwardCacheDisablingFeatures().Has(
blink::scheduler::WebSchedulerTrackedFeature::kWebHID));
// Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
ASSERT_TRUE(!rfh_a.IsDestroyed());
EXPECT_TRUE(
static_cast<RenderFrameHostImpl*>(rfh_a.get())->IsInBackForwardCache());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(current_frame_host(), rfh_a.get());
ExpectRestored(FROM_HERE);
}
#endif // !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
WakeLockReleasedUponEnteringBfcache) {
ASSERT_TRUE(CreateHttpsServer()->Start());
// 1) Navigate to a page with WakeLock usage.
GURL url(https_server()->GetURL(
"a.test", "/back_forward_cache/page_with_wakelock.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
// Acquire WakeLock.
EXPECT_EQ("DONE", EvalJs(rfh_a, "acquireWakeLock()"));
// Make sure that WakeLock is not released yet.
EXPECT_FALSE(EvalJs(rfh_a, "wakeLockIsReleased()").ExtractBool());
// 2) Navigate away.
shell()->LoadURL(https_server()->GetURL("b.test", "/title1.html"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page with WakeLock, restored from BackForwardCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(current_frame_host(), rfh_a);
EXPECT_TRUE(EvalJs(rfh_a, "wakeLockIsReleased()").ExtractBool());
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, CacheWithWebFileSystem) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with WebFileSystem usage.
GURL url(embedded_test_server()->GetURL("a.test", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
// Writer a file 'file.txt' with a content 'foo'.
EXPECT_EQ("success", EvalJs(rfh_a, R"(
new Promise((resolve, reject) => {
window.webkitRequestFileSystem(
window.TEMPORARY,
1024 * 1024,
(fs) => {
fs.root.getFile('file.txt', {create: true}, (entry) => {
entry.createWriter((writer) => {
writer.onwriteend = () => {
resolve('success');
};
writer.onerror = reject;
var blob = new Blob(['foo'], {type: 'text/plain'});
writer.write(blob);
}, reject);
}, reject);
}, reject);
});
)"));
// 2) Navigate away.
shell()->LoadURL(embedded_test_server()->GetURL("b.test", "/title1.html"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// 3) Go back to the page with WebFileSystem.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// Check the file content is reserved.
EXPECT_EQ("foo", EvalJs(rfh_a, R"(
new Promise((resolve, reject) => {
window.webkitRequestFileSystem(
window.TEMPORARY,
1024 * 1024,
(fs) => {
fs.root.getFile('file.txt', {}, (entry) => {
entry.file((file) => {
const reader = new FileReader();
reader.onloadend = (e) => {
resolve(e.target.result);
};
reader.readAsText(file);
}, reject);
}, reject);
}, reject);
});
)"));
}
namespace {
class FakeIdleTimeProvider : public ui::IdleTimeProvider {
public:
FakeIdleTimeProvider() = default;
~FakeIdleTimeProvider() override = default;
FakeIdleTimeProvider(const FakeIdleTimeProvider&) = delete;
FakeIdleTimeProvider& operator=(const FakeIdleTimeProvider&) = delete;
base::TimeDelta CalculateIdleTime() override { return base::Seconds(0); }
bool CheckIdleStateIsLocked() override { return false; }
};
} // namespace
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, DoesNotCacheIdleManager) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the IdleManager class.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
ui::test::ScopedIdleProviderForTest scoped_idle_provider(
std::make_unique<FakeIdleTimeProvider>());
EXPECT_EQ(42, EvalJs(rfh_a, R"(
new Promise(async resolve => {
let idleDetector = new IdleDetector();
await idleDetector.start();
resolve(42);
});
)"));
// 2) Navigate away.
shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title1.html"));
// The page uses IdleManager so it should be deleted.
deleted.WaitUntilDeleted();
// 3) Go back and make sure the IdleManager page wasn't in the cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kIdleManager}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, DoesNotCacheSMSService) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the SMSService.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver rfh_a_deleted(rfh_a);
EXPECT_TRUE(ExecJs(rfh_a, R"(
navigator.credentials.get({otp: {transport: ["sms"]}});
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page uses SMSService so it should be deleted.
rfh_a_deleted.WaitUntilDeleted();
// 3) Go back and make sure the SMSService page wasn't in the cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Note that on certain linux tests, there is occasionally a not restored
// reason of kDisableForRenderFrameHostCalled. This is due to the javascript
// navigator.credentials.get, which will call on authentication code for linux
// but not other operating systems. The authenticator code explicitly invokes
// kDisableForRenderFrameHostCalled. This causes flakiness if we check against
// all not restored reasons. As a result, we only check for the blocklist
// reason.
ExpectBlocklistedFeature(
blink::scheduler::WebSchedulerTrackedFeature::kWebOTPService, FROM_HERE);
}
namespace {
void OnInstallPaymentApp(base::OnceClosure done_callback,
bool* out_success,
bool success) {
*out_success = success;
std::move(done_callback).Run();
}
} // namespace
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCachePaymentManager) {
ASSERT_TRUE(CreateHttpsServer()->Start());
base::RunLoop run_loop;
GURL service_worker_javascript_file_url =
https_server()->GetURL("a.test", "/payments/payment_app.js");
bool success = false;
PaymentAppProvider::GetOrCreateForWebContents(shell()->web_contents())
->InstallPaymentAppForTesting(
/*app_icon=*/SkBitmap(), service_worker_javascript_file_url,
/*service_worker_scope=*/
service_worker_javascript_file_url.GetWithoutFilename(),
/*payment_method_identifier=*/
url::Origin::Create(service_worker_javascript_file_url).Serialize(),
base::BindOnce(&OnInstallPaymentApp, run_loop.QuitClosure(),
&success));
run_loop.Run();
ASSERT_TRUE(success);
// 1) Navigate to a page which includes PaymentManager functionality. Note
// that service workers are used, and therefore we use https server instead of
// embedded_server()
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL(
"a.test", "/payments/payment_app_invocation.html")));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver rfh_a_deleted(rfh_a);
// Execute functionality that calls PaymentManager.
EXPECT_EQ(42, EvalJs(rfh_a, R"(
new Promise(async resolve => {
const registration = await navigator.serviceWorker.getRegistration(
'/payments/payment_app.js');
await registration.paymentManager.enableDelegations(['shippingAddress']);
resolve(42);
});
)"));
// 2) Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
// The page uses PaymentManager so it should be deleted.
rfh_a_deleted.WaitUntilDeleted();
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kPaymentManager}, {}, {},
{}, FROM_HERE);
// Note that on Mac10.10, there is occasionally blocklisting for network
// requests (kOutstandingNetworkRequestOthers). This causes flakiness if we
// check against all blocklisted features. As a result, we only check for the
// blocklist we care about.
base::HistogramBase::Sample32 sample = base::HistogramBase::Sample32(
blink::scheduler::WebSchedulerTrackedFeature::kPaymentManager);
std::vector<base::Bucket> blocklist_values = histogram_tester().GetAllSamples(
"BackForwardCache.HistoryNavigationOutcome."
"BlocklistedFeature");
EXPECT_TRUE(base::Contains(blocklist_values, sample, &base::Bucket::min));
std::vector<base::Bucket> all_sites_blocklist_values =
histogram_tester().GetAllSamples(
"BackForwardCache.AllSites.HistoryNavigationOutcome."
"BlocklistedFeature");
EXPECT_TRUE(
base::Contains(all_sites_blocklist_values, sample, &base::Bucket::min));
}
// Pages with acquired keyboard lock should not enter BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheOnKeyboardLock) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the Keyboard lock.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver rfh_a_deleted(rfh_a);
AcquireKeyboardLock(rfh_a);
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page uses keyboard lock so it should be deleted.
rfh_a_deleted.WaitUntilDeleted();
// 3) Go back and make sure the keyboard lock page wasn't in the cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kKeyboardLock}, {}, {}, {},
FROM_HERE);
}
// If pages released keyboard lock, they can enter BackForwardCache. It will
// remain eligible for multiple restores.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfKeyboardLockReleasedMultipleRestores) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the Keyboard lock.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
AcquireKeyboardLock(rfh_a.get());
ReleaseKeyboardLock(rfh_a.get());
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
// 3) Go back and page should be restored from BackForwardCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// 4) Go forward and back, the page should be restored from BackForwardCache.
ASSERT_TRUE(HistoryGoForward(web_contents()));
EXPECT_EQ(rfh_b.get(), current_frame_host());
ExpectRestored(FROM_HERE);
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a.get(), current_frame_host());
ExpectRestored(FROM_HERE);
}
// If pages previously released the keyboard lock, but acquired it again, they
// cannot enter BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoNotCacheIfKeyboardLockIsHeldAfterRelease) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the Keyboard lock.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
AcquireKeyboardLock(rfh_a.get());
ReleaseKeyboardLock(rfh_a.get());
AcquireKeyboardLock(rfh_a.get());
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page uses keyboard lock so it should be deleted.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back and make sure the keyboard lock page wasn't in the cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kKeyboardLock}, {}, {}, {},
FROM_HERE);
}
// If pages released keyboard lock before navigation, they can enter
// BackForwardCache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfKeyboardLockReleased) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the Keyboard lock.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
AcquireKeyboardLock(rfh_a.get());
ReleaseKeyboardLock(rfh_a.get());
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// 3) Go back and page should be restored from BackForwardCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
class BackForwardCacheNonStickyDoubleFixBrowserTest
: public BackForwardCacheBrowserTest,
public testing::WithParamInterface<bool> {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
if (IsBackForwardCacheNonStickyDoubleFixEnabled()) {
EnableFeatureAndSetParams(kBackForwardCacheNonStickyDoubleFix, "", "");
} else {
DisableFeature(kBackForwardCacheNonStickyDoubleFix);
}
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
bool IsBackForwardCacheNonStickyDoubleFixEnabled() { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(All,
BackForwardCacheNonStickyDoubleFixBrowserTest,
testing::Bool());
// If pages released keyboard lock during pagehide, they can enter
// BackForwardCache. This also covers the case of entering BFCache for a
// second time. KeyboardLock is a good feature to use as it will always
// block BFCache. See https://crbug.com/360183659
IN_PROC_BROWSER_TEST_P(BackForwardCacheNonStickyDoubleFixBrowserTest,
CacheIfKeyboardLockReleasedInPagehide) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to a page and start using the Keyboard lock.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
AcquireKeyboardLock(rfh_a.get());
// Register a pagehide handler to release keyboard lock.
ASSERT_TRUE(ExecJs(rfh_a.get(), R"(
window.onpagehide = function(e) {
new Promise(resolve => {
navigator.keyboard.unlock();
resolve();
});
};
)"));
// Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// Go back and page should be restored from BackForwardCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// Acquire the lock again.
AcquireKeyboardLock(rfh_a.get());
// Navigate away again.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// Go back again.
ASSERT_TRUE(HistoryGoBack(web_contents()));
if (IsBackForwardCacheNonStickyDoubleFixEnabled()) {
// The page should be restored from BackForwardCache.
ExpectRestored(FROM_HERE);
} else {
// The page should not be restored from BackForwardCache.
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kKeyboardLock}, {}, {},
{}, FROM_HERE);
}
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheWithDummyStickyFeature) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page and start using the dummy sticky feature.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
rfh_a->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// The page uses the dummy sticky feature so it should be deleted.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back and make sure the dummy sticky feature page wasn't in the cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kDummy}, {},
{}, {}, FROM_HERE);
}
// Tests which blocklisted features are tracked in the metrics when we used
// blocklisted features (sticky and non-sticky) and do a browser-initiated
// cross-site navigation.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
BlocklistedFeaturesTracking_CrossSite_BrowserInitiated) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.test", kBlockingPagePath));
GURL url_b(https_server()->GetURL("b.test", "/title2.html"));
// 1) Navigate to a page.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* rfh_a = current_frame_host();
scoped_refptr<SiteInstanceImpl> site_instance_a =
static_cast<SiteInstanceImpl*>(rfh_a->GetSiteInstance());
RenderFrameDeletedObserver rfh_a_deleted(rfh_a);
// 2) Use a dummy sticky blocklisted feature.
rfh_a->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
// 3) Navigate cross-site, browser-initiated.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// The previous page won't get into the back-forward cache because of the
// blocklisted features. Because we used sticky blocklisted features, we will
// not do a proactive BrowsingInstance swap, however the RFH will still change
// and get deleted.
rfh_a_deleted.WaitUntilDeleted();
EXPECT_FALSE(site_instance_a->IsRelatedSiteInstance(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Both sticky and non-sticky features are recorded.
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kDummy,
kBlockingReasonEnum},
{}, {}, {}, FROM_HERE);
}
// Tests which blocklisted features are tracked in the metrics when we used
// blocklisted features (sticky and non-sticky) and do a renderer-initiated
// cross-site navigation.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
BlocklistedFeaturesTracking_CrossSite_RendererInitiated) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.test", kBlockingPagePath));
GURL url_b(https_server()->GetURL("b.test", "/title2.html"));
// 1) Navigate to a page.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* rfh_a = current_frame_host();
scoped_refptr<SiteInstanceImpl> site_instance_a =
static_cast<SiteInstanceImpl*>(rfh_a->GetSiteInstance());
// 2) Use a Dummy sticky blocklisted feature.
rfh_a->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
// 3) Navigate cross-site, renderer-inititated.
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), url_b));
// The previous page won't get into the back-forward cache because of the
// blocklisted features. Because we used sticky blocklisted features, we will
// not do a proactive BrowsingInstance swap.
EXPECT_TRUE(site_instance_a->IsRelatedSiteInstance(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Both sticky and non-sticky features are recorded.
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures,
NotRestoredReason::kBrowsingInstanceNotSwapped},
{blink::scheduler::WebSchedulerTrackedFeature::kDummy,
kBlockingReasonEnum},
{ShouldSwapBrowsingInstance::kNo_NotNeededForBackForwardCache}, {}, {},
FROM_HERE);
ASSERT_TRUE(HistoryGoForward(web_contents()));
ExpectBrowsingInstanceNotSwappedReason(
ShouldSwapBrowsingInstance::kNo_AlreadyHasMatchingBrowsingInstance,
FROM_HERE);
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectBrowsingInstanceNotSwappedReason(
ShouldSwapBrowsingInstance::kNo_AlreadyHasMatchingBrowsingInstance,
FROM_HERE);
}
// Tests which blocklisted features are tracked in the metrics when we used
// blocklisted features (sticky and non-sticky) and do a same-site navigation.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
BlocklistedFeaturesTracking_SameSite) {
ASSERT_TRUE(CreateHttpsServer()->Start());
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_1(https_server()->GetURL(kBlockingPagePath));
GURL url_2(https_server()->GetURL("/title2.html"));
// 1) Navigate to a page.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* rfh_1 = current_frame_host();
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(rfh_1->GetSiteInstance());
rfh_1->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// 2) Use a dummy sticky blocklisted features.
rfh_1->UseDummyStickyBackForwardCacheDisablingFeatureForTesting();
// 3) Navigate same-site.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Because we used sticky blocklisted features, we will not do a proactive
// BrowsingInstance swap.
EXPECT_TRUE(site_instance_1->IsRelatedSiteInstance(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Both sticky and non-sticky reasons are recorded here.
ExpectNotRestored(
{
NotRestoredReason::kBlocklistedFeatures,
NotRestoredReason::kBrowsingInstanceNotSwapped,
},
{blink::scheduler::WebSchedulerTrackedFeature::kDummy,
kBlockingReasonEnum},
{ShouldSwapBrowsingInstance::kNo_NotNeededForBackForwardCache}, {}, {},
FROM_HERE);
// NotRestoredReason tree should match the flattened list.
EXPECT_THAT(
GetTreeResult()->GetDocumentResult(),
MatchesDocumentResult(
NotRestoredReasons({NotRestoredReason::kBlocklistedFeatures,
NotRestoredReason::kBrowsingInstanceNotSwapped}),
BlockListedFeatures(
{blink::scheduler::WebSchedulerTrackedFeature::kDummy,
kBlockingReasonEnum})));
}
// Tests which blocklisted features are tracked in the metrics when we used a
// non-sticky blocklisted feature and do a browser-initiated cross-site
// navigation.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
BlocklistedFeaturesTracking_CrossSite_BrowserInitiated_NonSticky) {
ASSERT_TRUE(CreateHttpsServer()->Start());
// 1) Navigate to a blocking page.
GURL url_a(https_server()->GetURL("a.test", kBlockingPagePath));
GURL url_b(https_server()->GetURL("b.test", "/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
scoped_refptr<SiteInstanceImpl> site_instance_a =
static_cast<SiteInstanceImpl*>(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate cross-site, browser-initiated.
// The previous page won't get into the back-forward cache because of the
// blocklisted feature.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Because we only used non-sticky blocklisted features, we will still do a
// proactive BrowsingInstance swap.
EXPECT_FALSE(site_instance_a->IsRelatedSiteInstance(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Because the RenderFrameHostManager changed, the blocklisted features will
// be tracked in RenderFrameHostManager::UnloadOldFrame.
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{kBlockingReasonEnum}, {}, {}, {}, FROM_HERE);
}
// Tests which blocklisted features are tracked in the metrics when we used a
// non-sticky blocklisted feature and do a renderer-initiated cross-site
// navigation.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
BlocklistedFeaturesTracking_CrossSite_RendererInitiated_NonSticky) {
ASSERT_TRUE(CreateHttpsServer()->Start());
// 1) Navigate to an blocking page.
GURL url_a(https_server()->GetURL("a.test", kBlockingPagePath));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
scoped_refptr<SiteInstanceImpl> site_instance_a =
static_cast<SiteInstanceImpl*>(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
// 3) Navigate cross-site, renderer-inititated.
// The previous page won't get into the back-forward cache because of the
// blocklisted feature.
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), url_b));
// Because we only used non-sticky blocklisted features, we will still do a
// proactive BrowsingInstance swap.
EXPECT_FALSE(site_instance_a->IsRelatedSiteInstance(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Because the RenderFrameHostManager changed, the blocklisted features will
// be tracked in RenderFrameHostManager::UnloadOldFrame.
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{kBlockingReasonEnum}, {}, {}, {}, FROM_HERE);
}
// Tests which blocklisted features are tracked in the metrics when we used a
// non-sticky blocklisted feature and do a same-site navigation.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
BlocklistedFeaturesTracking_SameSite_NonSticky) {
ASSERT_TRUE(CreateHttpsServer()->Start());
// 1) Navigate to an empty page.
GURL url_1(https_server()->GetURL(kBlockingPagePath));
GURL url_2(https_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate same-site.
// The previous page won't get into the back-forward cache because of the
// blocklisted feature.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Because we only used non-sticky blocklisted features, we will still do a
// proactive BrowsingInstance swap.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(
web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Because the RenderFrameHostManager changed, the blocklisted features will
// be tracked in RenderFrameHostManager::UnloadOldFrame.
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{kBlockingReasonEnum}, {}, {}, {}, FROM_HERE);
}
// Use a blocklisted feature in multiple locations from an external JavaScript
// file and make sure all the JavaScript location details are captured.
// TODO(crbug.com/40241677): WebSocket server is flaky Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_MultipleBlocksFromJavaScriptFile \
DISABLED_MultipleBlocksFromJavaScriptFile
#else
#define MAYBE_MultipleBlocksFromJavaScriptFile MultipleBlocksFromJavaScriptFile
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_MultipleBlocksFromJavaScriptFile) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with multiple WebSocket usage.
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_websocket_external_script.html"));
GURL url_js(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/websocket_external_script.js"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Open WebSocket connections.
const char scriptA[] = R"(
openWebSocketConnectionA($1);
)";
const char scriptB[] = R"(
openWebSocketConnectionB($1);
)";
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptA,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptB,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketAOpen()"));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketBOpen()"));
// Call this to access tree result later.
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// 2) Navigate to b.com.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_EQ(url_a.spec(), current_frame_host()->GetLastCommittedURL());
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebSocket},
{}, {}, {}, FROM_HERE);
auto& map = GetTreeResult()->GetBlockingDetailsMap();
// Only WebSocket should be reported.
EXPECT_EQ(static_cast<int>(map.size()), 1);
EXPECT_TRUE(
map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket));
// Both socketA and socketB's JavaScript locations should be reported.
EXPECT_THAT(
map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket),
testing::UnorderedElementsAre(
MatchesBlockingDetails(MatchesSourceLocation(url_js, "", 10, 15)),
MatchesBlockingDetails(MatchesSourceLocation(url_js, "", 17, 15))));
}
// Use a blocklisted feature in multiple locations from an external JavaScript
// file but stop using one of them before navigating away. Make sure that only
// the one still in use is reported.
// TODO(crbug.com/40241677): WebSocket server is flaky Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_BlockAndUnblockFromJavaScriptFile \
DISABLED_BlockAndUnblockFromJavaScriptFile
#else
#define MAYBE_BlockAndUnblockFromJavaScriptFile \
BlockAndUnblockFromJavaScriptFile
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_BlockAndUnblockFromJavaScriptFile) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with multiple WebSocket usage.
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_websocket_external_script.html"));
GURL url_js(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/websocket_external_script.js"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call this to access tree result later.
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// Open WebSocket connections socketA and socketB, but close socketA
// immediately..
const char scriptA[] = R"(
openWebSocketConnectionA($1);
)";
const char scriptB[] = R"(
openWebSocketConnectionB($1);
)";
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptA,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptB,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketAOpen()"));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketBOpen()"));
ASSERT_TRUE(ExecJs(rfh_a.get(), "closeConnection();"));
ASSERT_EQ(false, EvalJs(rfh_a.get(), "isSocketAOpen()"));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketBOpen()"));
// 2) Navigate to b.com.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back and ensure that the socketB's detail is captured.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_EQ(url_a.spec(), current_frame_host()->GetLastCommittedURL());
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebSocket},
{}, {}, {}, FROM_HERE);
auto& map = GetTreeResult()->GetBlockingDetailsMap();
// Only WebSocket should be reported.
EXPECT_EQ(static_cast<int>(map.size()), 1);
EXPECT_TRUE(
map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket));
// Only socketB's JavaScript locations should be reported.
EXPECT_THAT(map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket),
testing::UnorderedElementsAre(MatchesBlockingDetails(
MatchesSourceLocation(url_js, "", 17, 15))));
}
// Use a blocklisted feature in multiple places from HTML file and make sure all
// the JavaScript locations detail are captured.
// TODO(crbug.com/40241677): WebSocket server is flaky Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_MultipleBlocksFromHTMLFile DISABLED_MultipleBlocksFromHTMLFile
#else
#define MAYBE_MultipleBlocksFromHTMLFile MultipleBlocksFromHTMLFile
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_MultipleBlocksFromHTMLFile) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with multiple WebSocket usage.
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_websocket_inline_script.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Open WebSocket connections.
const char scriptA[] = R"(
openWebSocketConnectionA($1);
)";
const char scriptB[] = R"(
openWebSocketConnectionB($1);
)";
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptA,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptB,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketAOpen()"));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketBOpen()"));
// Call this to access tree result later.
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// 2) Navigate to b.com.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_EQ(url_a.spec(), current_frame_host()->GetLastCommittedURL());
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebSocket},
{}, {}, {}, FROM_HERE);
auto& map = GetTreeResult()->GetBlockingDetailsMap();
// Only WebSocket should be reported.
EXPECT_EQ(static_cast<int>(map.size()), 1);
EXPECT_TRUE(
map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket));
// Both socketA and socketB's JavaScript locations should be reported.
EXPECT_THAT(
map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket),
testing::UnorderedElementsAre(
MatchesBlockingDetails(MatchesSourceLocation(url_a, "", 11, 15)),
MatchesBlockingDetails(MatchesSourceLocation(url_a, "", 18, 15))));
}
// Use a blocklisted feature in multiple locations from HTML file but stop using
// one of them before navigating away. Make sure that only the one still in use
// is reported.
// TODO(crbug.com/40241677): WebSocket server is flaky Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_BlockAndUnblockFromHTMLFile DISABLED_BlockAndUnblockFromHTMLFile
#else
#define MAYBE_BlockAndUnblockFromHTMLFile BlockAndUnblockFromHTMLFile
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_BlockAndUnblockFromHTMLFile) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with multiple broadcast channel usage.
GURL url_a(embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_websocket_inline_script.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call this to access tree result later.
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// Open WebSocket connections socketA and socketB, but close socketA
// immediately.
const char scriptA[] = R"(
openWebSocketConnectionA($1);
)";
const char scriptB[] = R"(
openWebSocketConnectionB($1);
)";
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptA,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(123, EvalJs(rfh_a.get(),
JsReplace(scriptB,
ws_server.GetURL("echo-with-no-extension"))));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketAOpen()"));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketBOpen()"));
ASSERT_TRUE(ExecJs(rfh_a.get(), "closeConnection();"));
ASSERT_EQ(false, EvalJs(rfh_a.get(), "isSocketAOpen()"));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "isSocketBOpen()"));
// 2) Navigate to b.com.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_EQ(url_a.spec(), current_frame_host()->GetLastCommittedURL());
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebSocket},
{}, {}, {}, FROM_HERE);
auto& map = GetTreeResult()->GetBlockingDetailsMap();
// Only WebSocket should be reported.
EXPECT_EQ(static_cast<int>(map.size()), 1);
EXPECT_TRUE(
map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket));
// Only socketB's JavaScript locations should be reported.
EXPECT_THAT(map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket),
testing::UnorderedElementsAre(MatchesBlockingDetails(
MatchesSourceLocation(url_a, "", 18, 15))));
}
// Test that details for sticky feature are captured.
// TODO(crbug.com/40241677): WebSocket server is flaky Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_StickyFeaturesWithDetails DISABLED_StickyFeaturesWithDetails
#else
#define MAYBE_StickyFeaturesWithDetails StickyFeaturesWithDetails
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_StickyFeaturesWithDetails) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a_no_store(embedded_test_server()->GetURL(
"a.com", "/set-header?Cache-Control: no-store"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to `url_a_no_store`.
ASSERT_TRUE(NavigateToURL(shell(), url_a_no_store));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call this to access tree result later.
rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
// Open a WebSocket.
const char script[] = R"(
new Promise(resolve => {
const socket = new WebSocket($1);
socket.addEventListener('open', () => resolve(42));
});)";
ASSERT_EQ(42, EvalJs(rfh_a.get(),
JsReplace(script,
ws_server.GetURL("echo-with-no-extension"))));
// 3) Navigate away to `url_b`.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 4) Go back to `url_a`.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebSocket,
blink::scheduler::WebSchedulerTrackedFeature::
kMainResourceHasCacheControlNoStore,
blink::scheduler::WebSchedulerTrackedFeature::kWebSocketSticky},
{}, {}, {}, FROM_HERE);
auto& map = GetTreeResult()->GetBlockingDetailsMap();
EXPECT_EQ(static_cast<int>(map.size()), 3);
EXPECT_TRUE(
map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket));
EXPECT_TRUE(map.contains(
blink::scheduler::WebSchedulerTrackedFeature::kWebSocketSticky));
EXPECT_THAT(map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebSocket),
testing::UnorderedElementsAre(MatchesBlockingDetails(
MatchesSourceLocation(GURL::EmptyGURL(), "", 3, 24))));
EXPECT_THAT(
map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebSocketSticky),
testing::UnorderedElementsAre(MatchesBlockingDetails(
MatchesSourceLocation(GURL::EmptyGURL(), "", 3, 24))));
}
// TODO(crbug.com/40834769): WebSQL does not work on Fuchsia.
// TODO(crbug.com/337202186): Flaky timeouts on all other platforms.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DISABLED_DoesNotCacheIfWebDatabase) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page with WebDatabase usage.
GURL url(embedded_test_server()->GetURL("/simple_database.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
// 2) Navigate away.
shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title1.html"));
// The page uses WebDatabase so it should be deleted.
deleted.WaitUntilDeleted();
// 3) Go back to the page with WebDatabase.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebDatabase}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfOpenIndexedDBConnection) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
ASSERT_TRUE(ExecJs(rfh_a.get(), "setupIndexedDBConnection()"));
// 2) Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page with IndexedDB.
// After navigating back, the page should be restored.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
EvictCacheIfOnVersionChangeEventReceived) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* tab_receiving_version_change = shell();
Shell* tab_sending_version_change = CreateBrowser();
// 1) Navigate the tab receiving version change to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
tab_receiving_version_change,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Create two connection with the same version here so that it can cover the
// cases when IndexedDB connection coordinator is not implemented correctly to
// handle multiple connections' back/forward cache status.
ASSERT_TRUE(ExecJs(rfh_a.get(), "setupIndexedDBConnection()"));
ASSERT_TRUE(
ExecJs(rfh_a.get(), "setupNewIndexedDBConnectionWithSameVersion()"));
// 2) Navigate the tab receiving version change away, and navigate the tab
// sending version change to the same page, and create a new IndexedDB
// connection with a higher version. The new IndexedDB connection should be
// created without being blocked by the page in back/forward cache.
ASSERT_TRUE(
NavigateToURL(tab_receiving_version_change,
embedded_test_server()->GetURL("a.com", "/title1.html")));
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
ASSERT_TRUE(NavigateToURL(
tab_sending_version_change,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
// Running `setupNewIndexedDBConnectionWithHigherVersion()` will trigger the
// `versionchange` event, which should cause the document receiving the
// version change to be evicted from back/forward cache.
content::DOMMessageQueue queue_sending_version_change(
tab_sending_version_change->web_contents());
std::string message_sending_version_change;
ExecuteScriptAsync(tab_sending_version_change,
"setupNewIndexedDBConnectionWithHigherVersion()");
ASSERT_TRUE(queue_sending_version_change.WaitForMessage(
&message_sending_version_change));
ASSERT_EQ("\"onsuccess\"", message_sending_version_change);
// 3) Go back to the page a with IndexedDB.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The page should be put into the back/forward cache after the navigation,
// but gets evicted due to `kIndexedDBEvent`.
ExpectNotRestored({NotRestoredReason::kIgnoreEventAndEvict}, {}, {}, {},
{DisallowActivationReasonId::kIndexedDBEvent}, FROM_HERE);
}
// Check if the non-sticky feature is properly registered before the
// `versionchange ` is sent. Since the `versionchange` event's handler won't
// close the IndexedDB connection, so when the navigation happens, the
// non-sticky feature will prevent the document from entering BFCache.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
DoesNotCacheIfVersionChangeEventIsSentButIndexedDBConnectionIsNotClosed) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* tab_receiving_version_change = shell();
Shell* tab_sending_version_change = CreateBrowser();
// 1) Navigate the receiving tab to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
tab_receiving_version_change,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_receiving(current_frame_host());
GURL destination_url =
embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(
ExecJs(tab_receiving_version_change,
JsReplace("setupIndexedDBVersionChangeHandlerToNavigateTo($1)",
destination_url.spec())));
// 2) Navigate the sending tab to A and use IndexedDB with higher version.
ASSERT_TRUE(NavigateToURL(
tab_sending_version_change,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
content::DOMMessageQueue queue_receiving_version_change(
tab_receiving_version_change->web_contents());
std::string message_receiving_version_change;
content::DOMMessageQueue queue_sending_version_change(
tab_sending_version_change->web_contents());
std::string message_sending_version_change;
ExecuteScriptAsync(tab_sending_version_change,
"setupNewIndexedDBConnectionWithHigherVersion()");
// 3) Wait until receiving tab receives the event and sending tab successfully
// opens the connection. The receiving tab should navigate to another page in
// the event handler. Before the navigation, the page should register a
// corresponding feature handle and should not be eligible for BFCache.
// The document will be disallowed to enter BFCache because of the
// `versionchange` event without proper closure of connection.
ASSERT_TRUE(queue_receiving_version_change.WaitForMessage(
&message_receiving_version_change));
ASSERT_EQ("\"onversionchange\"", message_receiving_version_change);
TestNavigationManager navigation_manager(
tab_receiving_version_change->web_contents(), destination_url);
ASSERT_TRUE(navigation_manager.WaitForRequestStart());
ASSERT_TRUE(rfh_receiving.get()->GetBackForwardCacheDisablingFeatures().Has(
blink::scheduler::WebSchedulerTrackedFeature::kIndexedDBEvent));
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
ASSERT_TRUE(queue_sending_version_change.WaitForMessage(
&message_sending_version_change));
ASSERT_EQ("\"onsuccess\"", message_sending_version_change);
// 4) Go back to the page A in the receiving tab, the page should not be put
// into back/forward cache at all, and the recorded blocklisted feature should
// be `kIndexedDBEvent`.
ASSERT_TRUE(rfh_receiving.WaitUntilRenderFrameDeleted());
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kIndexedDBEvent}, {}, {},
{}, FROM_HERE);
}
// Check if the non-sticky feature is properly registered before the
// `versionchange ` is sent and removed after the IndexedDB Connection is
// closed. Since the `versionchange` event's handler will close the IndexedDB
// connection before navigating away, so the document is eligible for BFCache as
// the non-sticky feature is removed.
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
CacheIfVersionChangeEventIsSentAndIndexedDBConnectionIsClosed) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* tab_receiving_version_change = shell();
Shell* tab_sending_version_change = CreateBrowser();
// 1) Navigate the receiving tab to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
tab_receiving_version_change,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_receiving(current_frame_host());
GURL destination_url =
embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(ExecJs(tab_receiving_version_change,
JsReplace("setupIndexedDBVersionChangeHandlerToCloseConnec"
"tionAndNavigateTo($1)",
destination_url.spec())));
// 2) Navigate the sending tab to A and use IndexedDB with higher version.
ASSERT_TRUE(NavigateToURL(
tab_sending_version_change,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
content::DOMMessageQueue queue_receiving_version_change(
tab_receiving_version_change->web_contents());
std::string message_receiving_version_change;
content::DOMMessageQueue queue_sending_version_change(
tab_sending_version_change->web_contents());
std::string message_sending_version_change;
ExecuteScriptAsync(tab_sending_version_change,
"setupNewIndexedDBConnectionWithHigherVersion()");
// 3) Wait until receiving tab receives the event and sending tab successfully
// opens the connection. The receiving tab should navigate to another page in
// the event handler. Before the navigation, the page should register a
// corresponding feature handle and should not be eligible for BFCache, but it
// will be removed when the connection is closed, making the page eligible for
// BFCache.
ASSERT_TRUE(queue_receiving_version_change.WaitForMessage(
&message_receiving_version_change));
ASSERT_EQ("\"onversionchange\"", message_receiving_version_change);
TestNavigationManager navigation_manager(
tab_receiving_version_change->web_contents(), destination_url);
ASSERT_TRUE(navigation_manager.WaitForRequestStart());
// Since the connection is closed, the tracked feature should be reset so
// the page is allowed to enter BFCache again.
ASSERT_FALSE(rfh_receiving.get()->GetBackForwardCacheDisablingFeatures().Has(
blink::scheduler::WebSchedulerTrackedFeature::kIndexedDBEvent));
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
ASSERT_TRUE(queue_sending_version_change.WaitForMessage(
&message_sending_version_change));
ASSERT_EQ("\"onsuccess\"", message_sending_version_change);
// 4) Go back to the page A in the receiving tab, it should be restored from
// BFCache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfIndexedDBConnectionClosedInPagehide) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to A and use IndexedDB, and close the connection on pagehide.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
EXPECT_TRUE(ExecJs(rfh_a.get(), "setupIndexedDBConnection()"));
// This registers a pagehide handler to close the IDB connection. This should
// remove the bfcache blocking.
EXPECT_TRUE(
ExecJs(rfh_a.get(), "registerPagehideToCloseIndexedDBConnection()"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page with IndexedDB. The connection is closed so it
// should be restored from bfcache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfIndexedDBTransactionNotCommitted) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
ASSERT_TRUE(ExecJs(rfh_a.get(), "setupIndexedDBConnection()"));
// This registers a pagehide handler to start a new transaction. This will
// block bfcache because there is an inflight transaction.
ASSERT_TRUE(ExecJs(rfh_a.get(), "registerPagehideToStartTransaction()"));
// 2) Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// 3) Go back to the page with IndexedDB.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfIndexedDBConnectionTransactionCommit) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
ASSERT_TRUE(ExecJs(rfh_a.get(), "setupIndexedDBConnection()"));
// This registers a pagehide handler to start and commit the IDB transactions.
// Since the transactions are ended inside the handler, the page is no longer
// blocked for inflight IDB transactions.
ASSERT_TRUE(
ExecJs(rfh_a.get(), "registerPagehideToStartAndCommitTransaction()"));
// 2) Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page with IndexedDB.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Verifies that transactions from a single client/render frame cannot disable
// BFCache for that client. Regression test for https://crbug.com/1517989
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
IndexedDBClientDoesntBlockSelf) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Use IDB and spam transactions.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
ASSERT_TRUE(ExecJs(shell(), "setupIndexedDBConnection()"));
ASSERT_TRUE(ExecJs(shell(), "runInfiniteIndexedDBTransactionLoop()"));
ASSERT_TRUE(ExecJs(shell(), "runInfiniteIndexedDBTransactionLoop()"));
// 2) Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_TRUE(rfh_a.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page with IndexedDB.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Verifies that transactions from a single client/render frame and a dedicated
// worker belonging to the frame cannot disable BFCache for that client.
// Regression test for https://crbug.com/343519262.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
IndexedDBClientWithDedicatedWorkerDoesntBlockSelf) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Use IDB and spam transactions.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com",
"/back_forward_cache/"
"page_with_dedicated_worker_using_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// 1.a) Setup IndexedDB on the main page and a dedicated worker.
ASSERT_TRUE(ExecJs(shell(), "setupIndexedDBConnection()"));
ASSERT_TRUE(
ExecJs(shell(), "sendMessageToWorker('setupIndexedDBConnection')"));
// 1.b) Run infinite loops on the worker and the main page.
ASSERT_TRUE(ExecJs(
shell(), "sendMessageToWorker('runInfiniteIndexedDBTransactionLoop')"));
ASSERT_TRUE(ExecJs(shell(), "runInfiniteIndexedDBTransactionLoop()"));
// 2) Navigate away.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_TRUE(rfh_a.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page with IndexedDB.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Verifies that a RF will be evicted from the cache if one of its transactions
// attempts to start while the RF is already in the cache, assuming the
// transaction is blocking other clients. That is, the
// kIndexedDBTransactionIsStartingWhileBlockingOthers case.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
IndexedDBDoNotCacheIfInactiveAndBlockingActive) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* tab_holding_locks = CreateBrowser();
Shell* tab_waiting_for_locks = shell();
Shell* next_tab_waiting_for_locks = CreateBrowser();
// 1) Navigate the tab holding locks to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
tab_holding_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
ASSERT_TRUE(ExecJs(tab_holding_locks, "setupIndexedDBConnection()"));
// Make sure the page keeps holding the lock by running infinite tasks on the
// object store.
ASSERT_TRUE(
ExecJs(tab_holding_locks, "runInfiniteIndexedDBTransactionLoop()"));
// 2) Navigate the tab waiting for locks to A as well and make it request
// the same lock.
ASSERT_TRUE(NavigateToURL(
tab_waiting_for_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
ASSERT_TRUE(ExecJs(tab_waiting_for_locks, "setupIndexedDBConnection()"));
ASSERT_TRUE(ExecJs(tab_waiting_for_locks, "startIndexedDBTransaction()"));
// 3) Navigate away the tab that's waiting for locks. It should enter BFCache.
ASSERT_TRUE(
NavigateToURL(tab_waiting_for_locks,
embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_TRUE(rfh_a.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
// 4) Go back to the page with IndexedDB.
ASSERT_TRUE(HistoryGoBack(tab_waiting_for_locks->web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_FALSE(rfh_a->IsInBackForwardCache());
// 5) Set up a third tab that's waiting for the same lock.
ASSERT_TRUE(NavigateToURL(
next_tab_waiting_for_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
ASSERT_TRUE(ExecJs(next_tab_waiting_for_locks, "setupIndexedDBConnection()"));
ASSERT_TRUE(
ExecJs(next_tab_waiting_for_locks, "startIndexedDBTransaction()"));
// Ensure that the transaction for the above is processed before continuing
// by round-tripping a task through the browser IDB thread (this task happens
// to be the opening of a new connection, which doesn't require acquiring
// locks). Without this step, the above transaction may not be processed until
// after the navigation below, which would affect the disallow activation
// reason.
{
content::DOMMessageQueue queue(next_tab_waiting_for_locks->web_contents());
EXPECT_TRUE(ExecJs(next_tab_waiting_for_locks,
"setupNewIndexedDBConnectionWithSameVersion()"));
std::string message;
ASSERT_TRUE(queue.WaitForMessage(&message));
ASSERT_EQ("\"success_same_version\"", message);
}
// 6) Repeat step 3. Still enters BFCache.
ASSERT_TRUE(
NavigateToURL(tab_waiting_for_locks,
embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_TRUE(rfh_a.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
// 7) Now navigate the tab holding the locks to a different site. Since the
// locks are released, and the BFCached tab is next in line, but is blocking a
// non-BFCached page, the BFCached tab should be evicted.
ASSERT_TRUE(NavigateToURL(tab_holding_locks, embedded_test_server()->GetURL(
"b.com", "/title1.html")));
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
ASSERT_TRUE(HistoryGoBack(tab_waiting_for_locks->web_contents()));
ExpectNotRestored({NotRestoredReason::kIgnoreEventAndEvict}, {}, {}, {},
{DisallowActivationReasonId::
kIndexedDBTransactionIsStartingWhileBlockingOthers},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTest,
DoNotCacheIfIndexedDBTransactionHoldingLocksAndBlockingOthers) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* tab_holding_locks = shell();
Shell* tab_waiting_for_locks = CreateBrowser();
// 1) Navigate the tab holding locks to A and use IndexedDB.
ASSERT_TRUE(NavigateToURL(
tab_holding_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
ASSERT_TRUE(ExecJs(tab_holding_locks, "setupIndexedDBConnection()"));
ASSERT_TRUE(ExecJs(tab_holding_locks,
"registerPagehideToCloseIndexedDBConnection()"));
// Make sure the page keeps holding the lock by running infinite tasks on the
// object store.
ExecuteScriptAsync(tab_holding_locks,
"runInfiniteIndexedDBTransactionLoop()");
// 2) Navigate the tab waiting for locks to A as well and make it request for
// the same lock. Since the other tab is holding the lock, this
// tab will be blocked and waiting for the lock to be released.
ASSERT_TRUE(NavigateToURL(
tab_waiting_for_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
ASSERT_TRUE(ExecJs(tab_waiting_for_locks, "setupIndexedDBConnection()"));
ASSERT_TRUE(ExecJs(tab_waiting_for_locks, "startIndexedDBTransaction()"));
// 3) Navigate the tab holding locks away.
// The page should be evicted by disallowing activation.
ASSERT_TRUE(NavigateToURL(tab_holding_locks, embedded_test_server()->GetURL(
"b.com", "/title1.html")));
// 4) Go back to the page with IndexedDB from the tab holding the locks.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
ASSERT_TRUE(HistoryGoBack(tab_holding_locks->web_contents()));
ExpectNotRestored({NotRestoredReason::kIgnoreEventAndEvict}, {}, {}, {},
{DisallowActivationReasonId::
kIndexedDBTransactionIsOngoingAndBlockingOthers},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
EvictCacheIfPageBlocksNewIndexedDBTransaction) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* tab_holding_locks = shell();
Shell* tab_acquiring_locks = CreateBrowser();
// 1) Navigate the tab holding locks to A and use IndexedDB, it also register
// a event on pagehide to run tasks that never ends to keep the IndexedDB
// transaction locks.
ASSERT_TRUE(NavigateToURL(
tab_holding_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
content::DOMMessageQueue queue_holding_locks(
tab_holding_locks->web_contents());
ASSERT_TRUE(ExecJs(tab_holding_locks, "setupIndexedDBConnection()"));
ASSERT_TRUE(
ExecJs(tab_holding_locks, "registerPagehideToStartTransaction()"));
// 2) Navigate the tab holding locks away.
ASSERT_TRUE(NavigateToURL(tab_holding_locks, embedded_test_server()->GetURL(
"b.com", "/title1.html")));
// 3) After confirming the transaction has been created from the tab holding
// locks, navigate the tab acquiring locks to A that tries to acquire the same
// lock.
std::string message_holding_locks;
ASSERT_TRUE(queue_holding_locks.WaitForMessage(&message_holding_locks));
ASSERT_EQ("\"transaction_created\"", message_holding_locks);
ASSERT_TRUE(NavigateToURL(
tab_acquiring_locks,
embedded_test_server()->GetURL(
"a.com", "/back_forward_cache/page_with_indexedDB.html")));
content::DOMMessageQueue queue_acquiring_locks(
tab_acquiring_locks->web_contents());
ASSERT_TRUE(ExecJs(tab_acquiring_locks, "setupIndexedDBConnection()"));
ASSERT_TRUE(ExecJs(tab_acquiring_locks, "startIndexedDBTransaction()"));
// 4) After confirming that the transaction from the tab acquiring locks is
// completed (which should evict the other tab if it's in BFCache), navigate
// the tab holding locks back to the page with IndexedDB.
std::string message_acquiring_locks;
ASSERT_TRUE(queue_acquiring_locks.WaitForMessage(&message_acquiring_locks));
ASSERT_EQ("\"transaction_completed\"", message_acquiring_locks);
// The page should be evicted by disallowing activation.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
ASSERT_TRUE(HistoryGoBack(tab_holding_locks->web_contents()));
ExpectNotRestored(
{NotRestoredReason::kIgnoreEventAndEvict}, {}, {}, {},
{DisallowActivationReasonId::kIndexedDBTransactionIsAcquiringLocks},
FROM_HERE);
}
// The parameter is used for switching `kBFCacheOpenBroadcastChannel`.
class BackForwardCacheWithBroadcastChannelTest
: public BackForwardCacheBrowserTest,
public testing::WithParamInterface<bool> {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
if (IsBFCacheOpenBroadcastChannelEnabled()) {
EnableFeatureAndSetParams(blink::features::kBFCacheOpenBroadcastChannel,
"", "");
} else {
DisableFeature(blink::features::kBFCacheOpenBroadcastChannel);
}
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
bool IsBFCacheOpenBroadcastChannelEnabled() { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(All,
BackForwardCacheWithBroadcastChannelTest,
testing::Bool());
// Checks that a page with an open broadcast channel is eligible for BFCache.
// Expects it's not eligible if the flag is disabled.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithBroadcastChannelTest,
MaybeCacheIfBroadcastChannelStillOpen) {
ASSERT_TRUE(CreateHttpsServer()->Start());
// 1) Navigate to an empty page.
GURL url_a(https_server()->GetURL(
"a.test", "/back_forward_cache/page_with_broadcastchannel.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// 2) Use BroadcastChannel (a non-sticky blocklisted feature).
RenderFrameHostImpl* rfh_a = current_frame_host();
EXPECT_TRUE(ExecJs(rfh_a, "acquireBroadcastChannel();"));
EXPECT_TRUE(ExecJs(rfh_a, "setShouldCloseChannelInPageHide(false);"));
// 3) Navigate cross-site, browser-initiated.
// The previous page won't get into the back-forward cache because of the
// blocklisted feature.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
if (IsBFCacheOpenBroadcastChannelEnabled()) {
ExpectRestored(FROM_HERE);
} else {
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kBroadcastChannel}, {},
{}, {}, FROM_HERE);
}
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CacheIfBroadcastChannelIsClosedInPagehide) {
ASSERT_TRUE(CreateHttpsServer()->Start());
// 1) Navigate to an empty page.
GURL url_a(https_server()->GetURL(
"a.test", "/back_forward_cache/page_with_broadcastchannel.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* rfh_a = current_frame_host();
// 2) Use BroadcastChannel (a non-sticky blocklisted feature).
EXPECT_TRUE(ExecJs(rfh_a, "acquireBroadcastChannel();"));
EXPECT_TRUE(ExecJs(rfh_a, "setShouldCloseChannelInPageHide(true);"));
// 3) Navigate cross-site, browser-initiated.
// The previous page won't get into the back-forward cache because of the
// blocklisted feature.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Checks that a page will be evicted from BFCache as soon as its broadcast
// channel receives a message.
IN_PROC_BROWSER_TEST_P(BackForwardCacheWithBroadcastChannelTest,
MaybeEvictOnMessage) {
// No need to test for when the flag is disabled. In that case the page will
// not enter BFCache if there's an open broadcast channel.
if (!IsBFCacheOpenBroadcastChannelEnabled()) {
return;
}
ASSERT_TRUE(CreateHttpsServer()->Start());
// Two same-origin pages and one empty page.
GURL url_a_receiver(https_server()->GetURL(
"a.test", "/back_forward_cache/page_with_broadcastchannel.html"));
GURL url_a_sender(https_server()->GetURL(
"a.test", "/back_forward_cache/page_with_broadcastchannel_sender.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// Navigate to a page which will receive message.
EXPECT_TRUE(NavigateToURL(shell(), url_a_receiver));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameDeletedObserver receiver_rfh_deleted_observer(
current_frame_host());
// Set up a broadcast channel.
RenderFrameHostImpl* rfh_a_receiver = current_frame_host();
EXPECT_TRUE(ExecJs(rfh_a_receiver, "acquireBroadcastChannel();"));
EXPECT_TRUE(ExecJs(rfh_a_receiver, "setOnMessage();"));
// Navigate to an empty page.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_TRUE(rfh_a_receiver->IsInBackForwardCache());
// Open another tab and navigate to a page which will send message.
Shell* shell2 =
Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
url_a_sender, nullptr, gfx::Size());
ASSERT_TRUE(WaitForLoadStop(shell2->web_contents()));
// Open a broadcast channel and cast a message.
RenderFrameHostImplWrapper rfh_a_sender(
shell2->web_contents()->GetPrimaryMainFrame());
EXPECT_TRUE(ExecJs(rfh_a_sender.get(), "acquireBroadcastChannel();"));
EXPECT_TRUE(ExecJs(rfh_a_sender.get(), "sendMessageOnce();"));
// The receiver page's rfh should be deleted.
receiver_rfh_deleted_observer.WaitUntilDeleted();
// Navigate back from the empty page to the receiver page.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The receiver page should have been evicted upon message.
ExpectNotRestored({NotRestoredReason::kBroadcastChannelOnMessage}, {}, {}, {},
{}, FROM_HERE);
}
// Disabled on Android, since we have problems starting up the websocket test
// server in the host
// TODO(crbug.com/40241677): Re-enable the test after solving the WS server.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_WebSocketCachedIfClosed DISABLED_WebSocketCachedIfClosed
#else
#define MAYBE_WebSocketCachedIfClosed WebSocketCachedIfClosed
#endif
// Pages with WebSocket should be cached if the connection is closed.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_WebSocketCachedIfClosed) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
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());
// Open a WebSocket.
const char script[] = R"(
let socket;
window.onpagehide = event => {
socket.close();
}
new Promise(resolve => {
socket = new WebSocket($1);
socket.addEventListener('open', () => resolve(42));
});)";
ASSERT_EQ(42, EvalJs(rfh_a.get(),
JsReplace(script,
ws_server.GetURL("echo-with-no-extension"))));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
class WebTransportBackForwardCacheBrowserTest
: public BackForwardCacheBrowserTest {
public:
WebTransportBackForwardCacheBrowserTest() { server_.Start(); }
void SetUpCommandLine(base::CommandLine* command_line) override {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
server_.SetUpCommandLine(command_line);
}
int port() const { return server_.server_address().port(); }
private:
WebTransportSimpleTestServer server_;
};
// Pages with active WebTransport should not be cached.
// TODO(yhirano): Update this test once
// https://github.com/w3c/webtransport/issues/326 is resolved.
IN_PROC_BROWSER_TEST_F(WebTransportBackForwardCacheBrowserTest,
ActiveWebTransportEvictsPage) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Establish a WebTransport session.
const char script[] = R"(
let transport = new WebTransport('https://localhost:$1/echo');
)";
ASSERT_TRUE(ExecJs(rfh_a.get(), JsReplace(script, port())));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// Confirm A is evicted.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebTransport}, {}, {}, {},
FROM_HERE);
}
// Pages with inactive WebTransport should be cached.
IN_PROC_BROWSER_TEST_F(WebTransportBackForwardCacheBrowserTest,
WebTransportCachedIfClosed) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Establish a WebTransport session.
const char script[] = R"(
let transport;
window.onpagehide = event => {
transport.close();
};
transport = new WebTransport('https://localhost:$1/echo');
)";
ASSERT_TRUE(ExecJs(rfh_a.get(), JsReplace(script, port())));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Navigate back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Disabled on Android, since we have problems starting up the websocket test
// server in the host
// TODO(crbug.com/40241677): Re-enable the test after solving the WS server.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_WebSocketNotCached DISABLED_WebSocketNotCached
#else
#define MAYBE_WebSocketNotCached WebSocketNotCached
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, MAYBE_WebSocketNotCached) {
net::SpawnedTestServer ws_server(net::SpawnedTestServer::TYPE_WS,
net::GetWebSocketTestDataDirectory());
ASSERT_TRUE(ws_server.Start());
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));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Open a WebSocket.
const char script[] = R"(
new Promise(resolve => {
const socket = new WebSocket($1);
socket.addEventListener('open', () => resolve(42));
});)";
ASSERT_EQ(
42, EvalJs(rfh_a, JsReplace(script,
ws_server.GetURL("echo-with-no-extension"))));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// Confirm A is evicted.
delete_observer_rfh_a.WaitUntilDeleted();
}
namespace {
void RegisterServiceWorker(RenderFrameHostImpl* rfh) {
EXPECT_EQ("success", EvalJs(rfh, R"(
let controller_changed_promise = new Promise(resolve_controller_change => {
navigator.serviceWorker.oncontrollerchange = resolve_controller_change;
});
new Promise(async resolve => {
try {
await navigator.serviceWorker.register(
"./service-worker.js", {scope: "./"})
} catch (e) {
resolve("error: registration has failed");
}
await controller_changed_promise;
if (navigator.serviceWorker.controller) {
resolve("success");
} else {
resolve("error: not controlled by service worker");
}
});
)"));
}
// Returns a unique script for each request, to test service worker update.
std::unique_ptr<net::test_server::HttpResponse> RequestHandlerForUpdateWorker(
const net::test_server::HttpRequest& request) {
if (request.relative_url != "/back_forward_cache/service-worker.js") {
return nullptr;
}
static int counter = 0;
auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
const char script[] = R"(
// counter = $1
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
)";
http_response->set_content(JsReplace(script, counter++));
http_response->set_content_type("text/javascript");
http_response->AddCustomHeader("Cache-Control",
"no-cache, no-store, must-revalidate");
return http_response;
}
} // namespace
class TestVibrationManager : public device::mojom::VibrationManager {
public:
TestVibrationManager() {
OverrideVibrationManagerBinderForTesting(base::BindRepeating(
&TestVibrationManager::BindVibrationManager, base::Unretained(this)));
}
~TestVibrationManager() override {
OverrideVibrationManagerBinderForTesting(base::NullCallback());
}
void BindVibrationManager(
mojo::PendingReceiver<device::mojom::VibrationManager> receiver,
mojo::PendingRemote<device::mojom::VibrationManagerListener> listener) {
receiver_.Bind(std::move(receiver));
}
bool TriggerVibrate(RenderFrameHostImpl* rfh, int duration) {
return EvalJs(rfh, JsReplace("navigator.vibrate($1)", duration))
.ExtractBool();
}
bool TriggerShortVibrationSequence(RenderFrameHostImpl* rfh) {
return EvalJs(rfh, "navigator.vibrate([10] * 1000)").ExtractBool();
}
bool WaitForCancel() {
run_loop_.Run();
return IsCancelled();
}
bool IsCancelled() { return cancelled_; }
private:
// device::mojom::VibrationManager:
void Vibrate(int64_t milliseconds, VibrateCallback callback) override {
cancelled_ = false;
std::move(callback).Run();
}
void Cancel(CancelCallback callback) override {
cancelled_ = true;
std::move(callback).Run();
run_loop_.Quit();
}
bool cancelled_ = false;
base::RunLoop run_loop_;
mojo::Receiver<device::mojom::VibrationManager> receiver_{this};
};
// Tests that vibration stops after the page enters bfcache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
VibrationStopsAfterEnteringCache) {
ASSERT_TRUE(embedded_test_server()->Start());
TestVibrationManager vibration_manager;
// 1) Navigate to a page with a long vibration.
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
ASSERT_TRUE(vibration_manager.TriggerVibrate(rfh_a, 10000));
EXPECT_FALSE(vibration_manager.IsCancelled());
// 2) Navigate away and expect the vibration to be canceled.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
EXPECT_NE(current_frame_host(), rfh_a);
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_TRUE(vibration_manager.WaitForCancel());
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Tests that the short vibration sequence on the page stops after it enters
// bfcache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
ShortVibrationSequenceStopsAfterEnteringCache) {
ASSERT_TRUE(embedded_test_server()->Start());
TestVibrationManager vibration_manager;
// 1) Navigate to a page with a long vibration.
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHostImpl* rfh_a = current_frame_host();
ASSERT_TRUE(vibration_manager.TriggerShortVibrationSequence(rfh_a));
EXPECT_FALSE(vibration_manager.IsCancelled());
// 2) Navigate away and expect the vibration to be canceled.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
EXPECT_NE(current_frame_host(), rfh_a);
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_TRUE(vibration_manager.WaitForCancel());
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CachedPagesWithServiceWorkers) {
CreateHttpsServer();
SetupCrossSiteRedirector(https_server());
ASSERT_TRUE(https_server()->Start());
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(
shell(),
https_server()->GetURL("a.test", "/back_forward_cache/empty.html")));
// Register a service worker.
RegisterServiceWorker(current_frame_host());
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
// 2) Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html")));
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to A. The navigation should be served from the cache.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_FALSE(deleted.deleted());
EXPECT_EQ(rfh_a, current_frame_host());
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
EvictIfCacheBlocksServiceWorkerVersionActivation) {
CreateHttpsServer();
https_server()->RegisterRequestHandler(
base::BindRepeating(&RequestHandlerForUpdateWorker));
SetupCrossSiteRedirector(https_server());
ASSERT_TRUE(https_server()->Start());
Shell* tab_x = shell();
Shell* tab_y = CreateBrowser();
// 1) Navigate to A in tab X.
EXPECT_TRUE(NavigateToURL(
tab_x,
https_server()->GetURL("a.test", "/back_forward_cache/empty.html")));
// 2) Register a service worker.
RegisterServiceWorker(current_frame_host());
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
// 3) Navigate away to B in tab X.
EXPECT_TRUE(
NavigateToURL(tab_x, https_server()->GetURL("b.test", "/title1.html")));
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 4) Navigate to A in tab Y.
EXPECT_TRUE(NavigateToURL(
tab_y,
https_server()->GetURL("a.test", "/back_forward_cache/empty.html")));
// 5) Close tab Y to activate a service worker version.
// This should evict |rfh_a| from the cache.
tab_y->Close();
deleted.WaitUntilDeleted();
// 6) Navigate to A in tab X.
ASSERT_TRUE(HistoryGoBack(tab_x->web_contents()));
ExpectNotRestored(
{
NotRestoredReason::kServiceWorkerVersionActivation,
},
{}, {}, {}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
EvictWithPostMessageToCachedClient) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.RegisterRequestHandler(
base::BindRepeating(&RequestHandlerForUpdateWorker));
https_server.AddDefaultHandlers(GetTestDataFilePath());
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
SetupCrossSiteRedirector(&https_server);
ASSERT_TRUE(https_server.Start());
Shell* tab_to_execute_service_worker = shell();
Shell* tab_to_be_bfcached = CreateBrowser();
// Observe the new WebContents to trace the navigation ID.
WebContentsObserver::Observe(tab_to_be_bfcached->web_contents());
// 1) Navigate to A in |tab_to_execute_service_worker|.
EXPECT_TRUE(NavigateToURL(
tab_to_execute_service_worker,
https_server.GetURL(
"a.test", "/back_forward_cache/service_worker_post_message.html")));
// 2) Register a service worker.
EXPECT_EQ("DONE", EvalJs(tab_to_execute_service_worker,
"register('service_worker_post_message.js')"));
// 3) Navigate to A in |tab_to_be_bfcached|.
EXPECT_TRUE(NavigateToURL(
tab_to_be_bfcached,
https_server.GetURL(
"a.test", "/back_forward_cache/service_worker_post_message.html")));
const std::string script_to_store =
"executeCommandOnServiceWorker('StoreClients')";
EXPECT_EQ("DONE", EvalJs(tab_to_execute_service_worker, script_to_store));
RenderFrameHostImplWrapper rfh(
tab_to_be_bfcached->web_contents()->GetPrimaryMainFrame());
// 4) Navigate away to B in |tab_to_be_bfcached|.
EXPECT_TRUE(NavigateToURL(tab_to_be_bfcached,
https_server.GetURL("b.test", "/title1.html")));
EXPECT_FALSE(rfh.IsDestroyed());
EXPECT_TRUE(rfh->IsInBackForwardCache());
// 5) Trigger client.postMessage via |tab_to_execute_service_worker|. Cache in
// |tab_to_be_bfcached| will be evicted.
const std::string script_to_post_message =
"executeCommandOnServiceWorker('PostMessageToStoredClients')";
EXPECT_EQ("DONE",
EvalJs(tab_to_execute_service_worker, script_to_post_message));
ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
// 6) Go back to A in |tab_to_be_bfcached|.
ASSERT_TRUE(HistoryGoBack(tab_to_be_bfcached->web_contents()));
ExpectNotRestored({NotRestoredReason::kServiceWorkerPostMessage}, {}, {}, {},
{}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, EvictOnServiceWorkerClaim) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.RegisterRequestHandler(
base::BindRepeating(&RequestHandlerForUpdateWorker));
https_server.AddDefaultHandlers(GetTestDataFilePath());
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
SetupCrossSiteRedirector(&https_server);
ASSERT_TRUE(https_server.Start());
Shell* tab_to_be_bfcached = shell();
Shell* tab_to_execute_service_worker = CreateBrowser();
// 1) Navigate to A in |tab_to_be_bfcached|.
EXPECT_TRUE(NavigateToURL(
tab_to_be_bfcached,
https_server.GetURL(
"a.test", "/back_forward_cache/service_worker_registration.html")));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
// 2) Navigate away to B in |tab_to_be_bfcached|.
EXPECT_TRUE(NavigateToURL(tab_to_be_bfcached,
https_server.GetURL("b.test", "/title1.html")));
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Navigate to A in |tab_to_execute_service_worker|.
EXPECT_TRUE(NavigateToURL(
tab_to_execute_service_worker,
https_server.GetURL(
"a.test", "/back_forward_cache/service_worker_registration.html")));
// 4) Register a service worker for |tab_to_execute_service_worker|.
EXPECT_EQ("DONE", EvalJs(tab_to_execute_service_worker,
"register('service_worker_registration.js')"));
// 5) The service worker calls clients.claim(). |rfh_a| would normally be
// claimed but because it's in bfcache, it is evicted from the cache.
EXPECT_EQ("DONE", EvalJs(tab_to_execute_service_worker, "claim()"));
deleted.WaitUntilDeleted();
// 6) Navigate to A in |tab_to_be_bfcached|.
ASSERT_TRUE(HistoryGoBack(tab_to_be_bfcached->web_contents()));
ExpectNotRestored({NotRestoredReason::kServiceWorkerClaim}, {}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
EvictOnServiceWorkerUnregistration) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.RegisterRequestHandler(
base::BindRepeating(&RequestHandlerForUpdateWorker));
https_server.AddDefaultHandlers(GetTestDataFilePath());
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
SetupCrossSiteRedirector(&https_server);
ASSERT_TRUE(https_server.Start());
Shell* tab_to_be_bfcached = shell();
Shell* tab_to_unregister_service_worker = CreateBrowser();
// 1) Navigate to A in |tab_to_be_bfcached|. This tab will be controlled by a
// service worker.
EXPECT_TRUE(NavigateToURL(
tab_to_be_bfcached,
https_server.GetURL("a.test",
"/back_forward_cache/"
"service_worker_registration.html?to_be_bfcached")));
// 2) Register a service worker for |tab_to_be_bfcached|, but with a narrow
// scope with URL param. This is to prevent |tab_to_unregister_service_worker|
// from being controlled by the service worker.
EXPECT_EQ("DONE",
EvalJs(tab_to_be_bfcached,
"register('service_worker_registration.js', "
"'service_worker_registration.html?to_be_bfcached')"));
EXPECT_EQ("DONE", EvalJs(tab_to_be_bfcached, "claim()"));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
// 3) Navigate to A in |tab_to_unregister_service_worker|. This tab is not
// controlled by the service worker.
EXPECT_TRUE(NavigateToURL(
tab_to_unregister_service_worker,
https_server.GetURL(
"a.test", "/back_forward_cache/service_worker_registration.html")));
// 5) Navigate from A to B in |tab_to_be_bfcached|. Now |tab_to_be_bfcached|
// should be in bfcache.
EXPECT_TRUE(NavigateToURL(tab_to_be_bfcached,
https_server.GetURL("b.test", "/title1.html")));
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 6) The service worker gets unregistered. Now |tab_to_be_bfcached| should be
// notified of the unregistration and evicted from bfcache.
EXPECT_EQ(
"DONE",
EvalJs(tab_to_unregister_service_worker,
"unregister('service_worker_registration.html?to_be_bfcached')"));
deleted.WaitUntilDeleted();
// 7) Navigate back to A in |tab_to_be_bfcached|.
ASSERT_TRUE(HistoryGoBack(tab_to_be_bfcached->web_contents()));
ExpectNotRestored({NotRestoredReason::kServiceWorkerUnregistration}, {}, {},
{}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, BeaconAndBfCache) {
constexpr char kKeepalivePath[] = "/keepalive";
net::test_server::ControllableHttpResponse keepalive(embedded_test_server(),
kKeepalivePath);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_ping(embedded_test_server()->GetURL("a.com", kKeepalivePath));
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a.get());
EXPECT_TRUE(
ExecJs(shell(), JsReplace(R"(navigator.sendBeacon($1, "");)", url_ping)));
// 2) Navigate to B.
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// Ensure that the keepalive request is sent.
keepalive.WaitForRequest();
// Don't actually send the response.
// Page A should be in the cache.
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
}
class GeolocationBackForwardCacheBrowserTest
: public BackForwardCacheBrowserTest {
protected:
GeolocationBackForwardCacheBrowserTest() : geo_override_(0.0, 0.0) {}
device::ScopedGeolocationOverrider geo_override_;
};
// Test that a page which has queried geolocation in the past, but have no
// active geolocation query, can be bfcached.
IN_PROC_BROWSER_TEST_F(GeolocationBackForwardCacheBrowserTest,
CacheAfterGeolocationRequest) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("/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();
// Query current position, and wait for the query to complete.
EXPECT_EQ("received", EvalJs(rfh_a, R"(
new Promise(resolve => {
navigator.geolocation.getCurrentPosition(() => resolve('received'));
});
)"));
RenderFrameDeletedObserver deleted(rfh_a);
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// The page has no inflight geolocation request when we navigated away,
// so it should have been cached.
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
}
// Test that a page which has an in-flight geolocation query can be bfcached,
// and verify that the page does not observe any geolocation while the page
// was inside bfcache.
IN_PROC_BROWSER_TEST_F(GeolocationBackForwardCacheBrowserTest,
CancelGeolocationRequestInFlight) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("/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();
EXPECT_TRUE(ExecJs(rfh_a, R"(
// If set, will be called by handleEvent.
window.pending_resolve = null;
window.longitude_log = [];
window.err_log = [];
// Returns a promise that will resolve when the `longitude` is recorded in
// the `longitude_log`. The promise will resolve with the index.
function waitForLongitudeRecorded(longitude) {
let index = window.longitude_log.indexOf(longitude);
if (index >= 0) {
return Promise.resolve(index);
}
return new Promise(resolve => {
window.pending_resolve = resolve;
}).then(() => waitForLongitudeRecorded(longitude));
}
// Continuously query current geolocation, if the longitude is different
// from the last recorded value, update the result in the list,
// and resolve the pending promises with the longitude value.
navigator.geolocation.watchPosition(
pos => {
let new_longitude = pos.coords.longitude;
let log_length = window.longitude_log.length;
if (log_length == 0 ||
window.longitude_log[log_length - 1] != new_longitude) {
window.longitude_log.push(pos.coords.longitude);
if (window.pending_resolve != null) {
window.pending_resolve();
window.pending_resolve = null;
}
}
},
err => window.err_log.push(err)
);
)"));
// Wait for the initial value to be updated in the callback.
EXPECT_EQ(
0, EvalJs(rfh_a, "window.waitForLongitudeRecorded(0.0);").ExtractInt());
// Update the location and wait for the promise, this location should be
// observed.
geo_override_.UpdateLocation(10.0, 10.0);
EXPECT_EQ(
1, EvalJs(rfh_a, "window.waitForLongitudeRecorded(10.0);").ExtractInt())
<< "Geoposition before the page is put into BFCache should be visible.";
// Pause resolving Geoposition queries to keep the request in-flight.
// This location should not be observed.
geo_override_.Pause();
geo_override_.UpdateLocation(20.0, 20.0);
EXPECT_EQ(1u, geo_override_.GetGeolocationInstanceCount());
// 2) Navigate away.
base::RunLoop loop_until_close;
geo_override_.SetGeolocationCloseCallback(loop_until_close.QuitClosure());
RenderFrameDeletedObserver deleted(rfh_a);
EXPECT_TRUE(NavigateToURL(shell(), url_b));
loop_until_close.Run();
// The page has no in-flight geolocation request when we navigated away,
// so it should have been cached.
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Resume resolving Geoposition queries.
geo_override_.Resume();
// We update the location while the page is BFCached, but this location should
// not be observed.
geo_override_.UpdateLocation(30.0, 30.0);
// 3) Navigate back to A.
// Pause resolving Geoposition queries to keep the request in-flight.
// The location when navigated back can be observed
geo_override_.Pause();
geo_override_.UpdateLocation(40.0, 40.0);
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
EXPECT_FALSE(rfh_a->IsInBackForwardCache());
// Resume resolving Geoposition queries.
geo_override_.Resume();
// Wait for an update after the user navigates back to A.
EXPECT_EQ(2,
EvalJs(rfh_a, "window.waitForLongitudeRecorded(40.0)").ExtractInt())
<< "Geoposition when the page is restored from BFCache should be visible";
EXPECT_EQ("0,10,40", EvalJs(rfh_a, "window.longitude_log.toString();"))
<< "Geoposition while the page is put into BFCache should be invisible, "
"so the log array should only contain 0, 10 and 40 but not 20 and 30";
EXPECT_EQ(0, EvalJs(rfh_a, "err_log.length;"))
<< "watchPosition API should have reported no errors";
}
class BluetoothBrowserTestContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
// ContentBrowserClient:
BluetoothDelegate* GetBluetoothDelegate() override { return &delegate_; }
MockBluetoothDelegate& delegate() { return delegate_; }
private:
testing::NiceMock<MockBluetoothDelegate> delegate_;
};
class BackForwardCacheWebBluetoothTest : public BackForwardCacheBrowserTest {
protected:
BackForwardCacheWebBluetoothTest() = default;
~BackForwardCacheWebBluetoothTest() override = default;
void SetUpOnMainThread() override {
BackForwardCacheBrowserTest::SetUpOnMainThread();
test_client_ = std::make_unique<BluetoothBrowserTestContentBrowserClient>();
ON_CALL(delegate(), MayUseBluetooth).WillByDefault(Return(true));
}
void SetUp() override {
// The test requires a mock Bluetooth adapter to perform WebBluetooth API
// calls. To avoid conflicts with the default Bluetooth adapter, e.g.
// Windows adapter, which is configured during Bluetooth initialization, the
// mock adapter is configured in SetUp().
adapter_ =
base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
BluetoothAdapterFactoryWrapper::Get().SetBluetoothAdapterOverride(adapter_);
ON_CALL(*adapter_, IsPresent).WillByDefault(Return(true));
// Configure the mock adapter to return a scanning error to avoid leaking
// the adapter after teardown due to an ongoing scanning session.
ON_CALL(*adapter_, StartScanWithFilter_)
.WillByDefault(
[](const device::BluetoothDiscoveryFilter* filter,
base::OnceCallback<void(
bool, device::UMABluetoothDiscoverySessionOutcome)>&
callback) {
std::move(callback).Run(
/*is_error=*/true,
device::UMABluetoothDiscoverySessionOutcome::UNKNOWN);
});
BackForwardCacheBrowserTest::SetUp();
}
void TearDown() override {
testing::Mock::VerifyAndClearExpectations(adapter_.get());
BluetoothAdapterFactoryWrapper::Get().SetBluetoothAdapterOverride(nullptr);
adapter_.reset();
BackForwardCacheBrowserTest::TearDown();
}
MockBluetoothDelegate& delegate() { return test_client_->delegate(); }
scoped_refptr<device::MockBluetoothAdapter> adapter_;
std::unique_ptr<BluetoothBrowserTestContentBrowserClient> test_client_;
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebBluetoothTest,
DoesNotCacheIfRequestDeviceWasCalled) {
// WebBluetooth requires HTTPS.
ASSERT_TRUE(CreateHttpsServer()->Start());
// Navigate to an empty page.
ASSERT_TRUE(NavigateToURL(
web_contents(),
https_server()->GetURL("a.test", "/back_forward_cache/empty.html")));
RenderFrameHostWrapper rfh_wrapper(current_frame_host());
// Call requestDevice to open a permission request dialog. Cancel the dialog
// once it is opened.
EXPECT_CALL(delegate(), RunBluetoothChooser)
.WillOnce([](RenderFrameHost* frame,
const BluetoothChooser::EventHandler& event_handler) {
event_handler.Run(BluetoothChooserEvent::CANCELLED, std::string());
return std::make_unique<BluetoothChooser>();
});
EXPECT_EQ("device not found", EvalJs(current_frame_host(), R"(
new Promise(resolve => {
navigator.bluetooth.requestDevice({
filters: [
{ services: [0x1802, 0x1803] },
]
})
.then(() => resolve("device found"))
.catch(() => resolve("device not found"))
});
)"));
// Navigate away.
ASSERT_TRUE(NavigateToURL(web_contents(),
https_server()->GetURL("b.test", "/title1.html")));
// The page called requestDevice so it should be deleted.
EXPECT_TRUE(rfh_wrapper.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebBluetooth}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebBluetoothTest,
DoesNotCacheIfGetDevicesWasCalled) {
// WebBluetooth requires HTTPS.
ASSERT_TRUE(CreateHttpsServer()->Start());
// Navigate to an empty page.
ASSERT_TRUE(NavigateToURL(
web_contents(),
https_server()->GetURL("a.test", "/back_forward_cache/empty.html")));
RenderFrameHostWrapper rfh_a(current_frame_host());
// Call getDevices to get a list of devices the page is allowed to access.
EXPECT_TRUE(ExecJs(current_frame_host(), "navigator.bluetooth.getDevices()"));
// Navigate away.
ASSERT_TRUE(NavigateToURL(web_contents(),
https_server()->GetURL("b.test", "/title1.html")));
// The page called getDevices so it should be deleted.
EXPECT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebBluetooth}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebBluetoothTest,
DoesNotCacheIfScanningWasStarted) {
// WebBluetooth requires HTTPS.
ASSERT_TRUE(CreateHttpsServer()->Start());
// Navigate to an empty page.
ASSERT_TRUE(NavigateToURL(
web_contents(),
https_server()->GetURL("a.test", "/back_forward_cache/empty.html")));
RenderFrameHostWrapper rfh_a(current_frame_host());
// Call requestLEScan to start scanning for nearby devices.
EXPECT_EQ("scan error", EvalJs(current_frame_host(),
R"(
new Promise(resolve => {
navigator.bluetooth.requestLEScan({acceptAllAdvertisements: true})
.then(() => resolve("scan started"))
.catch(() => resolve("scan error"))
});
)"));
// Navigate away.
ASSERT_TRUE(NavigateToURL(web_contents(),
https_server()->GetURL("b.test", "/title1.html")));
// The page started scanning so it should be deleted.
EXPECT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebBluetooth}, {}, {}, {},
FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebBluetoothTest,
DoesCacheIfGetAvailabilityWasCalled) {
// WebBluetooth requires HTTPS.
ASSERT_TRUE(CreateHttpsServer()->Start());
// Navigate to an empty page.
GURL url(https_server()->GetURL("a.test", "/back_forward_cache/empty.html"));
ASSERT_TRUE(NavigateToURL(web_contents(), url));
RenderFrameHostWrapper rfh_a(current_frame_host());
// Call getAvailability to check for a Bluetooth adapter.
EXPECT_TRUE(
ExecJs(current_frame_host(), "navigator.bluetooth.getAvailability()"));
// Navigate away.
ASSERT_TRUE(NavigateToURL(web_contents(),
https_server()->GetURL("b.test", "/title1.html")));
ASSERT_FALSE(rfh_a.IsDestroyed());
EXPECT_TRUE(
static_cast<RenderFrameHostImpl*>(rfh_a.get())->IsInBackForwardCache());
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(current_frame_host(), rfh_a.get());
ExpectRestored(FROM_HERE);
}
enum class SerialContext {
kDocument,
kWorker,
kNestedWorker,
};
enum class SerialType {
kSerial,
kWebUsb,
};
class BackForwardCacheBrowserWebUsbTest
: public BackForwardCacheBrowserTest,
public ::testing::WithParamInterface<
std::tuple<SerialContext, SerialType>> {
public:
std::string GetJsToUseSerial(SerialContext context, SerialType serial_type) {
switch (serial_type) {
case SerialType::kSerial:
switch (context) {
case SerialContext::kDocument:
return R"(
new Promise(async resolve => {
let ports = await navigator.serial.getPorts();
resolve("Found " + ports.length + " ports");
});
)";
case SerialContext::kWorker:
return R"(
new Promise(async resolve => {
const worker = new Worker(
"/back_forward_cache/serial/worker.js");
worker.onmessage = message => resolve(message.data);
worker.postMessage("Run");
});
)";
case SerialContext::kNestedWorker:
return R"(
new Promise(async resolve => {
const worker = new Worker(
"/back_forward_cache/serial/nested-worker.js");
worker.onmessage = message => resolve(message.data);
worker.postMessage("Run");
});
)";
}
case SerialType::kWebUsb:
switch (context) {
case SerialContext::kDocument:
return R"(
new Promise(async resolve => {
let devices = await navigator.usb.getDevices();
resolve("Found " + devices.length + " devices");
});
)";
case SerialContext::kWorker:
return R"(
new Promise(async resolve => {
const worker = new Worker(
"/back_forward_cache/webusb/worker.js");
worker.onmessage = message => resolve(message.data);
worker.postMessage("Run");
});
)";
case SerialContext::kNestedWorker:
return R"(
new Promise(async resolve => {
const worker = new Worker(
"/back_forward_cache/webusb/nested-worker.js");
worker.onmessage = message => resolve(message.data);
worker.postMessage("Run");
});
)";
}
}
}
};
// Check the BackForwardCache is disabled when the WebUSB feature is used.
// TODO(crbug.com/40849874): Consider testing in a subframe. This will
// require adjustments to Permissions Policy.
IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserWebUsbTest, Serials) {
// WebUSB requires HTTPS.
ASSERT_TRUE(CreateHttpsServer()->Start());
SerialContext context;
SerialType serial_type;
std::tie(context, serial_type) = GetParam();
content::BackForwardCacheDisabledTester tester;
GURL url(https_server()->GetURL(
"a.test", "/cross_site_iframe_factory.html?a.test(a.test)"));
ASSERT_TRUE(NavigateToURL(shell(), url));
// Check that the frames we care about are cacheable.
RenderFrameHostImplWrapper main_rfh(current_frame_host());
RenderFrameHostImplWrapper sub_rfh(
current_frame_host()->child_at(0)->current_frame_host());
ASSERT_FALSE(main_rfh->IsBackForwardCacheDisabled());
ASSERT_FALSE(sub_rfh->IsBackForwardCacheDisabled());
// Execute script to use WebUSB.
ASSERT_EQ(
serial_type == SerialType::kSerial ? "Found 0 ports" : "Found 0 devices",
content::EvalJs(main_rfh.get(), GetJsToUseSerial(context, serial_type)));
// Verify that the correct frames are now uncacheable.
EXPECT_TRUE(main_rfh->IsBackForwardCacheDisabled());
EXPECT_FALSE(sub_rfh->IsBackForwardCacheDisabled());
auto expected_reason =
serial_type == SerialType::kSerial
? BackForwardCacheDisable::DisabledReasonId::kSerial
: BackForwardCacheDisable::DisabledReasonId::kWebUSB;
EXPECT_TRUE(tester.IsDisabledForFrameWithReason(
main_rfh->GetProcess()->GetDeprecatedID(), main_rfh->GetRoutingID(),
BackForwardCacheDisable::DisabledReason(expected_reason)));
}
INSTANTIATE_TEST_SUITE_P(
All,
BackForwardCacheBrowserWebUsbTest,
testing::Combine(testing::Values(SerialContext::kDocument,
SerialContext::kWorker,
SerialContext::kNestedWorker),
testing::Values(SerialType::kWebUsb
#if !BUILDFLAG(IS_ANDROID)
,
SerialType::kSerial
#endif // !BUILDFLAG(IS_ANDROID)
)));
// Check that an audio suspends when the page goes to the cache and can resume
// after restored.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, AudioSuspendAndResume) {
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();
EXPECT_TRUE(ExecJs(rfh_a, R"(
var audio = document.createElement('audio');
document.body.appendChild(audio);
audio.testObserverEvents = [];
let event_list = [
'canplaythrough',
'pause',
'play',
'error',
];
for (event_name of event_list) {
let result = event_name;
audio.addEventListener(event_name, event => {
document.title = result;
audio.testObserverEvents.push(result);
});
}
audio.src = 'media/bear-opus.ogg';
var timeOnFrozen = 0.0;
audio.addEventListener('pause', () => {
timeOnFrozen = audio.currentTime;
});
)"));
// Load the media.
{
TitleWatcher title_watcher(shell()->web_contents(), u"canplaythrough");
title_watcher.AlsoWaitForTitle(u"error");
EXPECT_EQ(u"canplaythrough", title_watcher.WaitAndGetTitle());
}
EXPECT_EQ(42, EvalJs(rfh_a, R"(
new Promise(async resolve => {
audio.play();
while (audio.currentTime === 0) {
await new Promise(r => setTimeout(r, 1));
}
resolve(42);
});
)"));
// 2) Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
// Check that the media position is not changed when the page is in cache.
double duration1 = EvalJs(rfh_a, "timeOnFrozen;").ExtractDouble();
double duration2 = EvalJs(rfh_a, "audio.currentTime;").ExtractDouble();
EXPECT_LE(0.0, duration2 - duration1);
EXPECT_GT(0.01, duration2 - duration1);
// Resume the media.
EXPECT_TRUE(ExecJs(rfh_a, "audio.play();"));
// Confirm that the media pauses automatically when going to the cache.
// TODO(hajimehoshi): Confirm that this media automatically resumes if
// autoplay attribute exists.
EXPECT_EQ(ListValueOf("canplaythrough", "play", "pause", "play"),
EvalJs(rfh_a, "audio.testObserverEvents"));
}
// Check that a video suspends when the page goes to the cache and can resume
// after restored.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, VideoSuspendAndResume) {
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();
EXPECT_TRUE(ExecJs(rfh_a, R"(
var video = document.createElement('video');
document.body.appendChild(video);
video.testObserverEvents = [];
let event_list = [
'canplaythrough',
'pause',
'play',
'error',
];
for (event_name of event_list) {
let result = event_name;
video.addEventListener(event_name, event => {
document.title = result;
// Ignore 'canplaythrough' event as we can randomly get extra
// 'canplaythrough' events after playing here.
if (result != 'canplaythrough')
video.testObserverEvents.push(result);
});
}
video.src = 'media/bear.webm';
// Android bots can be very slow and the video is only 1s long.
// This gives the first part of the test time to run before reaching
// the end of the video.
video.playbackRate = 0.1;
var timeOnPagehide;
window.addEventListener('pagehide', () => {
timeOnPagehide = video.currentTime;
});
var timeOnPageshow;
window.addEventListener('pageshow', () => {
timeOnPageshow = video.currentTime;
});
)"));
// Load the media.
{
TitleWatcher title_watcher(shell()->web_contents(), u"canplaythrough");
title_watcher.AlsoWaitForTitle(u"error");
EXPECT_EQ(u"canplaythrough", title_watcher.WaitAndGetTitle());
}
EXPECT_EQ(42, EvalJs(rfh_a, R"(
new Promise(async resolve => {
video.play();
while (video.currentTime == 0)
await new Promise(r => setTimeout(r, 1));
resolve(42);
});
)"));
// Navigate to B.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Sleep for 1s so that playing in BFCache can be detected.
base::PlatformThread::Sleep(base::Seconds(1));
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
const double timeOnPagehide =
EvalJs(rfh_a, "timeOnPagehide;").ExtractDouble();
const double timeOnPageshow = EvalJs(rfh_a, "timeOnPageshow").ExtractDouble();
// Make sure the video did not reach the end. If it did, our test is not
// reliable.
ASSERT_GT(1.0, timeOnPageshow);
// Check that the duration of video played between pagehide and pageshow is
// small. We waited for 1s so if it didn't stop in BFCache, it should be much
// longer than this.
const double playedDuration = timeOnPageshow - timeOnPagehide;
EXPECT_LE(0.0, playedDuration);
EXPECT_GT(0.02, playedDuration);
// Resume the media.
EXPECT_TRUE(ExecJs(rfh_a, R"(
// Ensure that the video does not auto-pause when it completes as that
// would add an unexpected pause event.
video.loop = true;
video.play();
)"));
// Confirm that the media pauses automatically when going to the cache.
// TODO(hajimehoshi): Confirm that this media automatically resumes if
// autoplay attribute exists.
EXPECT_EQ(ListValueOf("play", "pause", "play"),
EvalJs(rfh_a, "video.testObserverEvents"));
}
class SensorBackForwardCacheBrowserTest
: public BackForwardCacheBrowserTest,
public testing::WithParamInterface<bool> {
protected:
SensorBackForwardCacheBrowserTest() {
WebContentsSensorProviderProxy::OverrideSensorProviderBinderForTesting(
base::BindRepeating(
&SensorBackForwardCacheBrowserTest::BindSensorProvider,
base::Unretained(this)));
}
~SensorBackForwardCacheBrowserTest() override {
WebContentsSensorProviderProxy::OverrideSensorProviderBinderForTesting(
base::NullCallback());
}
void SetUpOnMainThread() override {
provider_ = std::make_unique<device::FakeSensorProvider>();
provider_->SetAccelerometerData(1.0, 2.0, 3.0);
BackForwardCacheBrowserTest::SetUpOnMainThread();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
EnableFeatureAndSetParams(features::kAllowSensorsToEnterBfcache, "", "");
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
std::unique_ptr<device::FakeSensorProvider> provider_;
private:
void BindSensorProvider(
mojo::PendingReceiver<device::mojom::SensorProvider> receiver) {
provider_->Bind(std::move(receiver));
}
base::OnceClosure quit_closure_;
};
// Tests that Accelerometer sensor is suspended while in bfcache. Note that
// we are only testing FakeSensor::Suspend() and FakeSensor::Resume() are
// called, and they have no implementation.
//
// TODO(crbug.com/364143617): Focus not retrieved on Android bots and thus
// sensors are not automatically resumed.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_AccelerometerPausedWhileCached \
DISABLED_AccelerometerPausedWhileCached
#else
#define MAYBE_AccelerometerPausedWhileCached AccelerometerPausedWhileCached
#endif
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
MAYBE_AccelerometerPausedWhileCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(
https_server()->GetURL("a.test", "/back_forward_cache/sensor.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// JS to cause a page to listen to, capture and validate accelerometer events.
const std::string accelerometer_js = R"(
sensor = new Accelerometer({ frequency: 60 });
sensor.addEventListener('reading', handleEvent);
sensor.start();
)";
provider_->SetAccelerometerData(1.0, 2.0, 3.0);
ASSERT_TRUE(ExecJs(rfh_a.get(), accelerometer_js));
ASSERT_EQ(1, EvalJs(rfh_a.get(), "waitForEventsPromise(1)"));
provider_->UpdateAccelerometerData(1.0, 2.0, 3.1);
ASSERT_EQ(2, EvalJs(rfh_a.get(), "waitForEventsPromise(2)"));
provider_->UpdateAccelerometerData(1.0, 2.0, 3.2);
ASSERT_EQ(3, EvalJs(rfh_a.get(), "waitForEventsPromise(3)"));
// We should have 3 events with x=1.0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(1.0)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
ASSERT_NE(rfh_a.get(), rfh_b.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
ASSERT_TRUE(provider_->WaitForAccelerometerSuspend(/*suspend=*/true));
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_EQ(rfh_a.get(), current_frame_host());
// Sensor must be activated once coming back to the page.
ASSERT_TRUE(provider_->WaitForAccelerometerSuspend(/*suspend=*/false));
ASSERT_EQ(true, EvalJs(rfh_a.get(), "sensor.activated"));
// New update should arrive.
provider_->UpdateAccelerometerData(1.0, 2.0, 3.4);
// 4 to 5 events should arrive.
ASSERT_TRUE(ExecJs(rfh_a.get(), "waitForEventsPromise(4)"));
}
// Tests that Ambient Light sensor is suspended while in bfcache. Note that
// we are only testing FakeSensor::Suspend() and FakeSensor::Resume() are
// called, and they have no implementation.
//
// TODO(crbug.com/364143617): Focus not retrieved on Android bots and thus
// sensors are not automatically resumed.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_AmbientLightPausedWhileCached \
DISABLED_AmbientLightPausedWhileCached
#else
#define MAYBE_AmbientLightPausedWhileCached AmbientLightPausedWhileCached
#endif
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
MAYBE_AmbientLightPausedWhileCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(
https_server()->GetURL("a.test", "/back_forward_cache/sensor.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
const std::string ambient_light_js = R"(
sensor = new AmbientLightSensor();
sensor.addEventListener('reading', handleEvent);
sensor.start();
)";
provider_->SetAmbientLightSensorData(1.0);
ASSERT_TRUE(ExecJs(rfh_a.get(), ambient_light_js));
ASSERT_EQ(1, EvalJs(rfh_a.get(), "waitForEventsPromise(1)"));
provider_->UpdateAmbientLightSensorData(1.0);
ASSERT_EQ(2, EvalJs(rfh_a.get(), "waitForEventsPromise(2)"));
provider_->UpdateAmbientLightSensorData(1.0);
ASSERT_EQ(3, EvalJs(rfh_a.get(), "waitForEventsPromise(3)"));
// We should have 3 events with value=1.0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(1.0)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
ASSERT_NE(rfh_a.get(), rfh_b.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
ASSERT_TRUE(provider_->WaitForAmbientLightSensorSuspend(/*suspend=*/true));
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_EQ(rfh_a.get(), current_frame_host());
ASSERT_TRUE(provider_->WaitForAmbientLightSensorSuspend(/*suspend=*/false));
// Sensor must be activated once coming back to the page.
ASSERT_EQ(true, EvalJs(rfh_a.get(), "sensor.activated"));
// New update should arrive.
provider_->UpdateAmbientLightSensorData(1.0);
// 4 to 5 events should arrive.
ASSERT_TRUE(ExecJs(rfh_a.get(), "waitForEventsPromise(4)"));
}
// Tests that Linear Acceleration sensor is suspended while in bfcache.
// Note that we are only testing FakeSensor::Suspend() and
// FakeSensor::Resume() are called, and they have no implementation.
//
// TODO(crbug.com/364143617): Focus not retrieved on Android bots and thus
// sensors are not automatically resumed.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_LinearAccelerationPausedWhileCached \
DISABLED_LinearAccelerationPausedWhileCached
#else
#define MAYBE_LinearAccelerationPausedWhileCached \
LinearAccelerationPausedWhileCached
#endif
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
MAYBE_LinearAccelerationPausedWhileCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(
https_server()->GetURL("a.test", "/back_forward_cache/sensor.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
const std::string la_js = R"(
sensor = new LinearAccelerationSensor({ frequency: 60 });
sensor.addEventListener('reading', handleEvent);
sensor.start();
)";
provider_->SetLinearAccelerationSensorData(1.0, 2.0, 3.0);
ASSERT_TRUE(ExecJs(rfh_a.get(), la_js));
ASSERT_EQ(1, EvalJs(rfh_a.get(), "waitForEventsPromise(1)"));
provider_->UpdateLinearAccelerationSensorData(1.0, 2.0, 3.1);
ASSERT_EQ(2, EvalJs(rfh_a.get(), "waitForEventsPromise(2)"));
provider_->UpdateLinearAccelerationSensorData(1.0, 2.0, 3.2);
ASSERT_EQ(3, EvalJs(rfh_a.get(), "waitForEventsPromise(3)"));
// We should have 3 events with value=1.0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(1.0)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
ASSERT_NE(rfh_a.get(), rfh_b.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
ASSERT_TRUE(
provider_->WaitForLinearAccelerationSensorSuspend(/*suspend=*/true));
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_EQ(rfh_a.get(), current_frame_host());
ASSERT_TRUE(
provider_->WaitForLinearAccelerationSensorSuspend(/*suspend=*/false));
// Sensor must be activated once coming back to the page.
ASSERT_EQ(true, EvalJs(rfh_a.get(), "sensor.activated"));
// New update should arrive.
provider_->UpdateLinearAccelerationSensorData(1.0, 2.0, 3.4);
// 4 to 5 events should arrive.
ASSERT_TRUE(ExecJs(rfh_a.get(), "waitForEventsPromise(4)"));
}
// Tests that Gravity sensor is suspended while in bfcache.
//
// TODO(crbug.com/364143617): Focus not retrieved on Android bots and thus
// sensors are not automatically resumed.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_GravityPausedWhileCached DISABLED_GravityPausedWhileCached
#else
#define MAYBE_GravityPausedWhileCached GravityPausedWhileCached
#endif
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
MAYBE_GravityPausedWhileCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(
https_server()->GetURL("a.test", "/back_forward_cache/sensor.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
const std::string gravity_js = R"(
sensor = new GravitySensor({ frequency: 60 });
sensor.addEventListener('reading', handleEvent);
sensor.start();
)";
provider_->SetGravitySensorData(1.0, 2.0, 3.0);
ASSERT_TRUE(ExecJs(rfh_a.get(), gravity_js));
ASSERT_EQ(1, EvalJs(rfh_a.get(), "waitForEventsPromise(1)"));
provider_->UpdateGravitySensorData(1.0, 2.0, 3.1);
ASSERT_EQ(2, EvalJs(rfh_a.get(), "waitForEventsPromise(2)"));
provider_->UpdateGravitySensorData(1.0, 2.0, 3.2);
ASSERT_EQ(3, EvalJs(rfh_a.get(), "waitForEventsPromise(3)"));
// We should have 3 events with value=1.0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(1.0)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
ASSERT_NE(rfh_a.get(), rfh_b.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
ASSERT_TRUE(provider_->WaitForGravitySensorSuspend(/*suspend=*/true));
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_EQ(rfh_a.get(), current_frame_host());
ASSERT_TRUE(provider_->WaitForGravitySensorSuspend(/*suspend=*/false));
// Sensor must be activated once coming back to the page.
ASSERT_EQ(true, EvalJs(rfh_a.get(), "sensor.activated"));
// New update should arrive.
provider_->UpdateGravitySensorData(1.0, 2.0, 3.4);
// 4 to 5 events should arrive.
ASSERT_TRUE(ExecJs(rfh_a.get(), "waitForEventsPromise(4)"));
}
// Tests that Gyroscope sensor is suspended while in bfcache. Note that
// we are only testing FakeSensor::Suspend() and FakeSensor::Resume() are
// called, and they have no implementation.
//
// TODO(crbug.com/364143617): Focus not retrieved on Android bots and thus
// sensors are not automatically resumed.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_GyroscopePausedWhileCached DISABLED_GyroscopePausedWhileCached
#else
#define MAYBE_GyroscopePausedWhileCached GyroscopePausedWhileCached
#endif
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
MAYBE_GyroscopePausedWhileCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(
https_server()->GetURL("a.test", "/back_forward_cache/sensor.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
const std::string gyro_js = R"(
sensor = new Gyroscope({ frequency: 60 });
sensor.addEventListener('reading', handleEvent);
sensor.start();
)";
provider_->SetGyroscopeData(1.0, 2.0, 3.0);
ASSERT_TRUE(ExecJs(rfh_a.get(), gyro_js));
ASSERT_EQ(1, EvalJs(rfh_a.get(), "waitForEventsPromise(1)"));
provider_->UpdateGyroscopeData(1.0, 2.0, 3.1);
ASSERT_EQ(2, EvalJs(rfh_a.get(), "waitForEventsPromise(2)"));
provider_->UpdateGyroscopeData(1.0, 2.0, 3.2);
ASSERT_EQ(3, EvalJs(rfh_a.get(), "waitForEventsPromise(3)"));
// We should have 3 events with value=1.0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(1.0)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
ASSERT_NE(rfh_a.get(), rfh_b.get());
ASSERT_TRUE(rfh_a->IsInBackForwardCache());
ASSERT_TRUE(provider_->WaitForGyroscopeSuspend(/*suspend=*/true));
// 3) Go back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_EQ(rfh_a.get(), current_frame_host());
ASSERT_TRUE(provider_->WaitForGyroscopeSuspend(/*suspend=*/false));
// Sensor must be activated once coming back to the page.
ASSERT_EQ(true, EvalJs(rfh_a.get(), "sensor.activated"));
// New update should arrive.
provider_->UpdateGyroscopeData(1.0, 2.0, 3.4);
// 4 to 5 events should arrive.
ASSERT_TRUE(ExecJs(rfh_a.get(), "waitForEventsPromise(4)"));
}
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest, OrientationCached) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
EXPECT_TRUE(ExecJs(rfh_a, R"(
window.addEventListener("deviceorientation", () => {});
)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_THAT(rfh_a, InBackForwardCache());
}
// Tests that the orientation sensor is suspended while in bfcache.
//
// This sets some JS functions in the pages to enable the sensors, capture and
// validate the events. The a-page should only receive events with alpha=0, the
// b-page is allowed to receive any alpha value. The test captures 3 events in
// the a-page, then navigates to the b-page and changes the reading to have
// alpha=1. While on the b-page it captures 3 more events. If the a-page is
// still receiving events it should receive one or more of these. Finally it
// resets the reading back to have alpha=0 and navigates back to the a-page and
// captures 3 more events and verifies that all events on the a-page have
// alpha=0.
// TODO(crbug.com/330801676): Flaky on macOS.
#if BUILDFLAG(IS_MAC)
#define MAYBE_SensorPausedWhileCached DISABLED_SensorPausedWhileCached
#else
#define MAYBE_SensorPausedWhileCached SensorPausedWhileCached
#endif
IN_PROC_BROWSER_TEST_F(SensorBackForwardCacheBrowserTest,
MAYBE_SensorPausedWhileCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(
https_server()->GetURL("a.test", "/back_forward_cache/sensor.html"));
GURL url_b(
https_server()->GetURL("b.test", "/back_forward_cache/sensor.html"));
provider_->SetRelativeOrientationSensorData(0, 0, 0);
const std::string orientation_js = R"(
// Override the function.
function handleEvent(event) {
values.push(event.alpha);
if (pendingResolve !== null) {
pendingResolve('event');
pendingResolve = null;
}
}
window.addEventListener('deviceorientation', handleEvent);
)";
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a.get());
ASSERT_TRUE(ExecJs(rfh_a.get(), orientation_js));
// Collect 3 orientation events.
ASSERT_EQ(1, EvalJs(rfh_a.get(), "waitForEventsPromise(1)"));
provider_->UpdateRelativeOrientationSensorData(0, 0, 0.2);
ASSERT_EQ(2, EvalJs(rfh_a.get(), "waitForEventsPromise(2)"));
provider_->UpdateRelativeOrientationSensorData(0, 0, 0.4);
ASSERT_EQ(3, EvalJs(rfh_a.get(), "waitForEventsPromise(3)"));
// We should have 3 events with alpha=0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(0)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImplWrapper rfh_b(current_frame_host());
ASSERT_FALSE(delete_observer_rfh_a.deleted());
ASSERT_THAT(rfh_a.get(), InBackForwardCache());
ASSERT_NE(rfh_a.get(), rfh_b.get());
// Change the orientation data before executing |orientation_js|, otherwise a
// deviceorientation event might be fired before the call below and the first
// registered event will have the previous data (0 0 0.4).
provider_->SetRelativeOrientationSensorData(1, 0, 0);
ASSERT_TRUE(ExecJs(rfh_b.get(), orientation_js));
// Collect 3 orientation events.
ASSERT_EQ(1, EvalJs(rfh_b.get(), "waitForEventsPromise(1)"));
provider_->UpdateRelativeOrientationSensorData(1, 0, 0.2);
ASSERT_EQ(2, EvalJs(rfh_b.get(), "waitForEventsPromise(2)"));
provider_->UpdateRelativeOrientationSensorData(1, 0, 0.4);
ASSERT_EQ(3, EvalJs(rfh_b.get(), "waitForEventsPromise(3)"));
// We should have 3 events with alpha=1.
ASSERT_EQ("pass", EvalJs(rfh_b.get(), "validateEvents(1)"));
// 3) Go back to A.
provider_->UpdateRelativeOrientationSensorData(0, 0, 0);
ASSERT_TRUE(HistoryGoBack(web_contents()));
ASSERT_EQ(rfh_a.get(), current_frame_host());
// Collect 3 orientation events.
provider_->UpdateRelativeOrientationSensorData(0, 0, 0);
// There are 2 processes so, it's possible that more events crept in. So we
// capture how many there are at this point and uses to wait for at least 3
// more.
int count = EvalJs(rfh_a.get(), "waitForEventsPromise(4)").ExtractInt();
provider_->UpdateRelativeOrientationSensorData(0, 0, 0.2);
count++;
ASSERT_EQ(count, EvalJs(rfh_a.get(), base::StringPrintf(
"waitForEventsPromise(%d)", count)));
provider_->UpdateRelativeOrientationSensorData(0, 0, 0.4);
count++;
ASSERT_EQ(count, EvalJs(rfh_a.get(), base::StringPrintf(
"waitForEventsPromise(%d)", count)));
// We should have the earlier 3 plus another 3 events with alpha=0.
ASSERT_EQ("pass", EvalJs(rfh_a.get(), "validateEvents(0)"));
}
// This tests that even if a page initializes WebRTC, tha page can be cached as
// long as it doesn't make a connection.
// On the Android test environments, the test might fail due to IP restrictions.
// See the discussion at http://crrev.com/c/2564926.
#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/40183520): The test is consistently failing on some Mac
// bots.
#if BUILDFLAG(IS_MAC)
#define MAYBE_TrivialRTCPeerConnectionCached \
DISABLED_TrivialRTCPeerConnectionCached
#else
#define MAYBE_TrivialRTCPeerConnectionCached TrivialRTCPeerConnectionCached
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_TrivialRTCPeerConnectionCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
// Create an RTCPeerConnection without starting a connection.
EXPECT_TRUE(ExecJs(rfh_a, "const pc1 = new RTCPeerConnection()"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// RTCPeerConnection object, that is created before being put into the cache,
// is still available.
EXPECT_EQ("success", EvalJs(rfh_a, R"(
new Promise(async resolve => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
pc1.onicecandidate = e => {
if (e.candidate)
pc2.addIceCandidate(e.candidate);
}
pc2.onicecandidate = e => {
if (e.candidate)
pc1.addIceCandidate(e.candidate);
}
pc1.addTransceiver("audio");
const connectionEstablished = new Promise((resolve, reject) => {
pc1.oniceconnectionstatechange = () => {
const state = pc1.iceConnectionState;
switch (state) {
case "connected":
case "completed":
resolve();
break;
case "failed":
case "disconnected":
case "closed":
reject(state);
break;
}
}
});
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
try {
await connectionEstablished;
} catch (e) {
resolve("fail " + e);
return;
}
resolve("success");
});
)"));
}
#endif // !BUILDFLAG(IS_ANDROID)
// This tests that a page using WebRTC and creating actual connections cannot be
// cached.
// On the Android test environments, the test might fail due to IP restrictions.
// See the discussion at http://crrev.com/c/2564926.
#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/40183520): The test is consistently failing on some Mac
// bots.
// This test uses MediaStreamTrack, so the test class is
// `BackForwardCacheMediaTest`.
#if BUILDFLAG(IS_MAC)
#define MAYBE_NonTrivialRTCPeerConnectionNotCached \
DISABLED_NonTrivialRTCPeerConnectionNotCached
#else
#define MAYBE_NonTrivialRTCPeerConnectionNotCached \
NonTrivialRTCPeerConnectionNotCached
#endif
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_NonTrivialRTCPeerConnectionNotCached) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Create an RTCPeerConnection with starting a connection.
EXPECT_EQ("success", EvalJs(rfh_a, R"(
new Promise(async resolve => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
pc1.onicecandidate = e => {
if (e.candidate)
pc2.addIceCandidate(e.candidate);
}
pc2.onicecandidate = e => {
if (e.candidate)
pc1.addIceCandidate(e.candidate);
}
pc1.addTransceiver("audio");
const connectionEstablished = new Promise(resolve => {
pc1.oniceconnectionstatechange = () => {
const state = pc1.iceConnectionState;
switch (state) {
case "connected":
case "completed":
resolve();
break;
case "failed":
case "disconnected":
case "closed":
reject(state);
break;
}
}
});
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await connectionEstablished;
try {
await connectionEstablished;
} catch (e) {
resolve("fail " + e);
return;
}
resolve("success");
});
)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// - Page A should not be in the cache.
delete_observer_rfh_a.WaitUntilDeleted();
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// A live MediaStreamTrack blocks BFCache.
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebRTC,
blink::scheduler::WebSchedulerTrackedFeature::kLiveMediaStreamTrack},
{}, {}, {}, FROM_HERE);
}
#endif // !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, WebLocksNotCached) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Wait for the page to acquire a lock and ensure that it continues to do so.
EXPECT_TRUE(ExecJs(rfh_a, R"(
const never_resolved = new Promise(resolve => {});
new Promise(continue_test => {
navigator.locks.request('test', async () => {
continue_test();
await never_resolved;
});
})
)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// - Page A should not be in the cache.
delete_observer_rfh_a.WaitUntilDeleted();
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebLocks},
{}, {}, {}, FROM_HERE);
}
enum TestAuthenticatorBehavior {
kErrorOut,
kStallRequest,
};
// An implementation of blink::mojom::Authenticator that stalls all requests.
class TestAuthenticator : public blink::mojom::Authenticator {
public:
explicit TestAuthenticator(TestAuthenticatorBehavior behavior)
: behavior_(behavior) {
OverrideAuthenticatorBinderForTesting(base::BindRepeating(
&TestAuthenticator::BindAuthenticator, base::Unretained(this)));
}
~TestAuthenticator() override {
OverrideVibrationManagerBinderForTesting(base::NullCallback());
}
void BindAuthenticator(
mojo::PendingReceiver<blink::mojom::Authenticator> receiver) {
receiver_.Bind(std::move(receiver));
}
private:
// blink::mojom::Authenticator:
void MakeCredential(
blink::mojom::PublicKeyCredentialCreationOptionsPtr options,
MakeCredentialCallback callback) override {
if (behavior_ == kStallRequest) {
pending_make_credential_callback_ = std::move(callback);
} else {
std::move(callback).Run(blink::mojom::AuthenticatorStatus::ABORT_ERROR,
nullptr, nullptr);
}
}
void GetCredential(blink::mojom::PublicKeyCredentialRequestOptionsPtr options,
GetCredentialCallback callback) override {
if (behavior_ == kStallRequest) {
pending_get_credential_callback_ = std::move(callback);
} else {
auto get_assertion_response = blink::mojom::GetAssertionResponse::New(
blink::mojom::AuthenticatorStatus::ABORT_ERROR, nullptr, nullptr);
auto get_credential_response =
blink::mojom::GetCredentialResponse::NewGetAssertionResponse(
std::move(get_assertion_response));
std::move(callback).Run(std::move(get_credential_response));
}
}
void GetClientCapabilities(GetClientCapabilitiesCallback callback) override {}
void Report(blink::mojom::PublicKeyCredentialReportOptionsPtr options,
ReportCallback callback) override {}
void IsUserVerifyingPlatformAuthenticatorAvailable(
IsUserVerifyingPlatformAuthenticatorAvailableCallback callback) override {
}
void IsConditionalMediationAvailable(
IsConditionalMediationAvailableCallback callback) override {}
void Cancel() override {}
MakeCredentialCallback pending_make_credential_callback_;
GetCredentialCallback pending_get_credential_callback_;
TestAuthenticatorBehavior behavior_;
mojo::Receiver<blink::mojom::Authenticator> receiver_{this};
};
// Tests that an ongoing WebAuthn get assertion request disables BFcache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
WebAuthnGetAssertion_NoCachingDuringRequest) {
TestAuthenticator test_authenticator(kStallRequest);
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Leave a WebAuthn get assertion request pending.
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
navigator.credentials.get({ publicKey: {
challenge: new TextEncoder().encode("speedrun a game"),
userVerification: "discouraged",
allowCredentials: [{type: "public-key", id: Uint8Array.from([1, 2, 3])}],
}});
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// - Page A should not be in the cache.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebAuthentication}, {},
{}, {}, FROM_HERE);
}
// Tests that after a WebAuthn get assertion request completes, BFcache is not
// disabled.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
WebAuthnGetAssertion_CacheAfterRequest) {
TestAuthenticator test_authenticator(kErrorOut);
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Complete a WebAuthn get assertion request.
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
navigator.credentials.get({ publicKey: {
challenge: new TextEncoder().encode("speedrun a game"),
userVerification: "discouraged",
allowCredentials: [{type: "public-key", id: Uint8Array.from([1, 2, 3])}],
}}).catch(() => {});
)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Tests that an ongoing WebAuthn make credential request disables BFcache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
WebAuthnMakeCredential_NoCachingDuringRequest) {
TestAuthenticator test_authenticator(kStallRequest);
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Leave a WebAuthn make credential request pending.
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
navigator.credentials.create({ publicKey: {
challenge: new TextEncoder().encode("speedrun a game"),
userVerification: "discouraged",
rp: { name: "Acme"},
user: {
id: new TextEncoder().encode("1234"),
name: "fox",
displayName: "Fox McCloud"
},
pubKeyCredParams: [{ type: "public-key", alg: -7}],
}});
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// - Page A should not be in the cache.
ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebAuthentication}, {},
{}, {}, FROM_HERE);
}
// Tests that after a WebAuthn make credential request completes, BFcache is not
// disabled.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
WebAuthnMakeCredential_CacheAfterRequest) {
TestAuthenticator test_authenticator(kErrorOut);
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Leave a WebAuthn make credential request pending.
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
navigator.credentials.create({ publicKey: {
challenge: new TextEncoder().encode("speedrun a game"),
userVerification: "discouraged",
rp: { name: "Acme"},
user: {
id: new TextEncoder().encode("1234"),
name: "fox",
displayName: "Fox McCloud"
},
pubKeyCredParams: [{ type: "public-key", alg: -7}],
}}).catch(() => {});
)"));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// TODO(crbug.com/40937711): Reenable. This is flaky because we block on
// the permission request, not on API usage.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, DISABLED_WebMidiNotCached) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// Request access to MIDI. This should prevent the page from entering the
// BackForwardCache.
EXPECT_TRUE(ExecJs(rfh_a, "navigator.requestMIDIAccess()",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// 2) Navigate to B.
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// - Page A should not be in the cache.
delete_observer_rfh_a.WaitUntilDeleted();
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kRequestedMIDIPermission},
{}, {}, {}, FROM_HERE);
}
// https://crbug.com/1410441
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DISABLED_PresentationConnectionClosed) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL(
"a.test", "/back_forward_cache/presentation_controller.html"));
// Navigate to A (presentation controller page).
ASSERT_TRUE(NavigateToURL(shell(), url_a));
auto* rfh_a = current_frame_host();
// Start a presentation connection in A.
MockPresentationServiceDelegate mock_presentation_service_delegate;
auto& presentation_service = rfh_a->GetPresentationServiceForTesting();
presentation_service.SetControllerDelegateForTesting(
&mock_presentation_service_delegate);
EXPECT_CALL(mock_presentation_service_delegate, StartPresentation(_, _, _));
EXPECT_TRUE(ExecJs(rfh_a, "presentationRequest.start().then(setConnection)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// Ensure that the above script runs before continuing.
EXPECT_TRUE(ExecJs(rfh_a, "var foo = 42;"));
// Send a mock connection to the renderer.
MockPresentationConnection mock_controller_connection;
mojo::Receiver<PresentationConnection> controller_connection_receiver(
&mock_controller_connection);
mojo::Remote<PresentationConnection> receiver_connection;
const std::string presentation_connection_id = "foo";
presentation_service.OnStartPresentationSucceeded(
presentation_service.start_presentation_request_id_,
PresentationConnectionResult::New(
blink::mojom::PresentationInfo::New(GURL("fake-url"),
presentation_connection_id),
controller_connection_receiver.BindNewPipeAndPassRemote(),
receiver_connection.BindNewPipeAndPassReceiver()));
// Navigate to B, make sure that the connection started in A is closed.
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
EXPECT_CALL(
mock_controller_connection,
DidClose(blink::mojom::PresentationConnectionCloseReason::WENT_AWAY));
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
ASSERT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// Navigate back to A. Ensure that connection state has been updated
// accordingly.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_FALSE(rfh_a->IsInBackForwardCache());
EXPECT_EQ(presentation_connection_id, EvalJs(rfh_a, "connection.id"));
EXPECT_EQ("closed", EvalJs(rfh_a, "connection.state"));
EXPECT_TRUE(EvalJs(rfh_a, "connectionClosed").ExtractBool());
// Try to start another connection, should successfully reach the browser side
// PresentationServiceDelegate.
EXPECT_CALL(mock_presentation_service_delegate,
ReconnectPresentation(_, presentation_connection_id, _, _));
EXPECT_TRUE(ExecJs(rfh_a, "presentationRequest.reconnect(connection.id);",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
base::RunLoop().RunUntilIdle();
// Reset |presentation_service|'s controller delegate so that it won't try to
// call Reset() on it on destruction time.
presentation_service.OnDelegateDestroyed();
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheIfSpeechRecognitionIsStarted) {
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 url_a.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// 2) Start SpeechRecognition.
EXPECT_EQ(42, EvalJs(rfh_a, R"(
new Promise(async resolve => {
var r = new webkitSpeechRecognition();
r.start();
resolve(42);
});
)"));
// 3) Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 4) The page uses SpeechRecognition so it should be deleted.
delete_observer_rfh_a.WaitUntilDeleted();
// 5) Go back to the page with SpeechRecognition.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kSpeechRecognizer}, {}, {},
{}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
CanCacheIfSpeechRecognitionIsNotStarted) {
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 url_a.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a);
// 2) Initialise SpeechRecognition but don't start it yet.
EXPECT_EQ(42, EvalJs(rfh_a, R"(
new Promise(async resolve => {
var r = new webkitSpeechRecognition();
resolve(42);
});
)"));
// 3) Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 4) The page didn't start using SpeechRecognition so it shouldn't be deleted
// and enter BackForwardCache.
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 5) Go back to the page with SpeechRecognition.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
ExpectRestored(FROM_HERE);
}
// This test is not important for Chrome OS if TTS is called in content. For
// more details refer (content/browser/speech/tts_platform_impl.cc).
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_CacheIfUsingSpeechSynthesis DISABLED_CacheIfUsingSpeechSynthesis
#else
#define MAYBE_CacheIfUsingSpeechSynthesis CacheIfUsingSpeechSynthesis
#endif // BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
MAYBE_CacheIfUsingSpeechSynthesis) {
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 page and start using SpeechSynthesis.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
EXPECT_EQ(42, EvalJs(rfh_a.get(), R"(
new Promise(async resolve => {
var u = new SpeechSynthesisUtterance(" ");
speechSynthesis.speak(u);
resolve(42);
});
)"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// 3) Go back to the page with SpeechSynthesis and ensure the page is
// restored if the flag is on.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// TODO(crbug.com/40254716): Test that onend callback is fired upon restore.
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
DoesNotCacheIfRunFileChooserIsInvoked) {
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 url_a and open file chooser.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted_rfh_a(rfh_a);
content::BackForwardCacheDisabledTester tester;
// 2) Bind FileChooser to RenderFrameHost.
mojo::Remote<blink::mojom::FileChooser> chooser =
FileChooserImpl::CreateBoundForTesting(rfh_a);
auto quit_run_loop = [](base::OnceClosure callback,
blink::mojom::FileChooserResultPtr result) {
std::move(callback).Run();
};
// 3) Run OpenFileChooser and wait till its run.
base::RunLoop run_loop;
chooser->OpenFileChooser(
blink::mojom::FileChooserParams::New(),
base::BindOnce(quit_run_loop, run_loop.QuitClosure()));
run_loop.Run();
// 4) rfh_a should be disabled for BackForwardCache after opening file
// chooser.
EXPECT_TRUE(rfh_a->IsBackForwardCacheDisabled());
auto reason = BackForwardCacheDisable::DisabledReason(
BackForwardCacheDisable::DisabledReasonId::kFileChooser);
EXPECT_TRUE(tester.IsDisabledForFrameWithReason(
rfh_a->GetProcess()->GetDeprecatedID(), rfh_a->GetRoutingID(), reason));
// 5) Navigate to B having the file chooser open.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
// The page uses FileChooser so it should be deleted.
deleted_rfh_a.WaitUntilDeleted();
// 6) Go back to the page with FileChooser.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kDisableForRenderFrameHostCalled}, {},
{}, {reason}, {}, FROM_HERE);
}
// TODO(crbug.com/40285326): This fails with the field trial testing config.
class BackForwardCacheBrowserTestNoTestingConfig
: public BackForwardCacheBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch("disable-field-trial-config");
}
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestNoTestingConfig,
CacheWithMediaSession) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page using MediaSession.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
navigator.mediaSession.metadata = new MediaMetadata({
artwork: [
{src: "test_image.jpg", sizes: "1x1", type: "image/jpeg"},
{src: "test_image.jpg", sizes: "10x10", type: "image/jpeg"}
]
});
)"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a.get(), current_frame_host());
ExpectRestored(FROM_HERE);
// Check the media session state is reserved.
EXPECT_EQ("10x10", EvalJs(rfh_a.get(), R"(
navigator.mediaSession.metadata.artwork[1].sizes;
)"));
}
class BackForwardCacheBrowserTestWithSupportedFeatures
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
EnableFeatureAndSetParams(features::kBackForwardCache, "supported_features",
"broadcastchannel,keyboardlock");
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithSupportedFeatures,
CacheWithSpecifiedFeatures) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.test", "/title1.html"));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to the page A with BroadcastChannel.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a = current_frame_host();
RenderFrameDeletedObserver deleted(rfh_a);
EXPECT_TRUE(ExecJs(rfh_a, "window.foo = new BroadcastChannel('foo');"));
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back to the page A
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
ExpectRestored(FROM_HERE);
// 4) Use KeyboardLock.
AcquireKeyboardLock(rfh_a);
// 5) Navigate away again.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_FALSE(deleted.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 6) Go back to the page A again.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a, current_frame_host());
ExpectRestored(FROM_HERE);
}
class BackForwardCacheBrowserTestWithNoSupportedFeatures
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
// Specify empty supported features explicitly.
EnableFeatureAndSetParams(features::kBackForwardCache, "supported_features",
"");
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithNoSupportedFeatures,
DontCache) {
ASSERT_TRUE(CreateHttpsServer()->Start());
GURL url_a(https_server()->GetURL("a.test", kBlockingPagePath));
GURL url_b(https_server()->GetURL("b.test", "/title1.html"));
// 1) Navigate to the page A with a blocking feature.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
RenderFrameHostImpl* rfh_a1 = current_frame_host();
RenderFrameDeletedObserver deleted_a1(rfh_a1);
// 2) Navigate away.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
deleted_a1.WaitUntilDeleted();
// 3) Go back to the page A
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{kBlockingReasonEnum}, {}, {}, {}, FROM_HERE);
RenderFrameHostImpl* rfh_a2 = current_frame_host();
RenderFrameDeletedObserver deleted_a2(rfh_a2);
// 4) Use KeyboardLock.
AcquireKeyboardLock(rfh_a2);
// 5) Navigate away again.
EXPECT_TRUE(NavigateToURL(shell(), url_b));
deleted_a2.WaitUntilDeleted();
// 6) Go back to the page A again.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored(
{NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kKeyboardLock,
kBlockingReasonEnum},
{}, {}, {}, FROM_HERE);
}
class BackForwardCacheBrowserTestWithMediaSession
: public BackForwardCacheBrowserTest {
protected:
void PlayVideoNavigateAndGoBack() {
MediaSession* media_session = MediaSession::Get(shell()->web_contents());
ASSERT_TRUE(media_session);
content::MediaStartStopObserver start_observer(
shell()->web_contents(), MediaStartStopObserver::Type::kStart);
EXPECT_TRUE(ExecJs(current_frame_host(),
"document.querySelector('#long-video').play();"));
start_observer.Wait();
content::MediaStartStopObserver stop_observer(
shell()->web_contents(), MediaStartStopObserver::Type::kStop);
media_session->Suspend(MediaSession::SuspendType::kSystem);
stop_observer.Wait();
// Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
// Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
}
};
class BackForwardCacheBrowserTestWithMediaSessionNoTestingConfig
: public BackForwardCacheBrowserTestWithMediaSession {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
DisableFeature(features::kBackForwardCacheMediaSessionService);
// The MediaSessionEnterPictureInPicture feature depends on the
// BackForwardCacheMediaSessionService feature, so we need to also disable
// it here.
// TODO(crbug.com/41483582): Remove these tests since the
// BackForwardCacheMediaSessionService feature has been launched.
DisableFeature(blink::features::kMediaSessionEnterPictureInPicture);
BackForwardCacheBrowserTestWithMediaSession::SetUpCommandLine(command_line);
}
};
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTestWithMediaSessionNoTestingConfig,
CacheWhenMediaSessionPlaybackStateIsChanged) {
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
// 2) Update the playback state change.
EXPECT_TRUE(ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), R"(
navigator.mediaSession.playbackState = 'playing';
)"));
// 3) Navigate away.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// 4) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The page is restored since a MediaSession service is not used.
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithMediaSession,
CacheWhenMediaSessionServiceIsNotUsed) {
// There are sometimes unexpected messages from a renderer to the browser,
// which caused test flakiness.
// TODO(crbug.com/40793577): Fix the test flakiness.
DoNotFailForUnexpectedMessagesWhileCached();
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Navigate to a page using MediaSession.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.test", "/media/session/media-session.html")));
// Play the media once, but without setting any callbacks to the MediaSession.
// In this case, a MediaSession service is not used.
PlayVideoNavigateAndGoBack();
// The page is restored since a MediaSession service is not used.
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(
BackForwardCacheBrowserTestWithMediaSessionNoTestingConfig,
DontCacheWhenMediaSessionServiceIsUsed) {
ASSERT_TRUE(embedded_test_server()->Start());
// Navigate to a page using MediaSession.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.test", "/media/session/media-session.html")));
RenderFrameHostWrapper rfh_a(current_frame_host());
// Register a callback explicitly to use a MediaSession service.
EXPECT_TRUE(ExecJs(rfh_a.get(), R"(
navigator.mediaSession.setActionHandler('play', () => {});
)"));
PlayVideoNavigateAndGoBack();
// The page is not restored since a MediaSession service is used.
auto reason = BackForwardCacheDisable::DisabledReason(
BackForwardCacheDisable::DisabledReasonId::kMediaSessionService);
ExpectNotRestored({NotRestoredReason::kDisableForRenderFrameHostCalled}, {},
{}, {reason}, {}, FROM_HERE);
}
#if BUILDFLAG(ENABLE_VR)
class BackForwardCacheBrowserTestWithWebXr
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
EnableFeatureAndSetParams(features::kWebXr, "", "");
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
};
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithWebXr,
DoesCacheIfXrAttributeWasAccessed) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
// 1) Navigate to an empty page.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("a.com", "/title1.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Access navigator.xr without calling any methods.
EXPECT_TRUE(ExecJs(rfh_a.get(), "navigator.xr"));
// 2) Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.com", "/title1.html")));
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
EXPECT_EQ(rfh_a.get(), current_frame_host());
ExpectRestored(FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithWebXr,
DoesNotCacheIfXrIsSessionSupportedWasCalled) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
// 1) Navigate to an empty page.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("a.com", "/title1.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call isSessionSupported.
EXPECT_TRUE(ExecJs(rfh_a.get(), "navigator.xr.isSessionSupported('inline')"));
// 2) Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.com", "/title1.html")));
// The page called a WebXR method so it should be deleted.
EXPECT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebXR}, {},
{}, {}, FROM_HERE);
}
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithWebXr,
DoesNotCacheIfXrRequestSessionWasCalled) {
CreateHttpsServer();
ASSERT_TRUE(https_server()->Start());
// 1) Navigate to an empty page.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("a.com", "/title1.html")));
RenderFrameHostImplWrapper rfh_a(current_frame_host());
// Call requestSession.
EXPECT_TRUE(ExecJs(rfh_a.get(), "navigator.xr.requestSession('inline')"));
// 2) Navigate away.
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("b.com", "/title1.html")));
// The page called a WebXR method so it should be deleted.
EXPECT_TRUE(rfh_a.WaitUntilRenderFrameDeleted());
// 3) Go back.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
{blink::scheduler::WebSchedulerTrackedFeature::kWebXR}, {},
{}, {}, FROM_HERE);
}
#endif
} // namespace content