// 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 <algorithm>

#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/back_forward_cache/back_forward_cache_disable.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/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/embedded_test_server/install_default_websocket_handlers.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;

namespace {

template <typename T>
struct GetInterfaceFromBinder;

template <typename T, typename Interface>
struct GetInterfaceFromBinder<void (T::*)(mojo::PendingReceiver<Interface>)> {
  using type = Interface;
};

template <typename T>
class TestReceiverContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  TestReceiverContentBrowserClient() = default;
  ~TestReceiverContentBrowserClient() override = default;

  using TInterface =
      typename GetInterfaceFromBinder<decltype(&T::BindReceiver)>::type;

  void RegisterBrowserInterfaceBindersForFrame(
      content::RenderFrameHost* render_frame_host,
      mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
    ContentBrowserTestContentBrowserClient::
        RegisterBrowserInterfaceBindersForFrame(render_frame_host, map);
    map->Add<TInterface>(base::BindRepeating(
        &TestReceiverContentBrowserClient::Bind, weak_factory_.GetWeakPtr()));
  }

  T& Manager() { return manager_; }

 private:
  void Bind(content::RenderFrameHost* render_frame_host,
            mojo::PendingReceiver<TInterface> receiver) {
    manager_.BindReceiver(std::move(receiver));
  }

  T manager_;
  base::WeakPtrFactory<TestReceiverContentBrowserClient<T>> weak_factory_{this};
};

template <typename T>
class BackForwardCacheBinderBrowserTest : public BackForwardCacheBrowserTest {
 protected:
  using BrowserClient = TestReceiverContentBrowserClient<T>;

  void SetUpOnMainThread() override {
    BackForwardCacheBrowserTest::SetUpOnMainThread();
    browser_client_ = std::make_unique<BrowserClient>();
    // Create a new renderer now that RegisterBrowserInterfaceBindersForFrame
    // is overridden.
    RecreateWindow();
  }

  T& Manager() { return browser_client_->Manager(); }

 private:
  std::unique_ptr<BrowserClient> browser_client_;
};

}  // namespace

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);
}

class BackForwardCacheWithDedicatedWorkerBrowserTest
    : public BackForwardCacheBrowserTest {
 public:
  const int kMaxBufferedBytesPerProcess = 10000;
  const base::TimeDelta kGracePeriodToFinishLoading = base::Seconds(5);

  BackForwardCacheWithDedicatedWorkerBrowserTest() { server_.Start(); }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // 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);
  }

  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_;
};

// Confirms that a page using a dedicated worker is cached.
IN_PROC_BROWSER_TEST_F(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_F(
    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 a closed WebTransport is
// cached as WebTransport is not a sticky feature.
IN_PROC_BROWSER_TEST_F(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/40821593): Disabled due to being flaky.
IN_PROC_BROWSER_TEST_F(
    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_F(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_F(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_F(
    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_F(
    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);
}

// TODO(crbug.com/469570289): enable the flaky test.
#if BUILDFLAG(IS_LINUX)
#define MAYBE_PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted \
  DISABLED_PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted
#else
#define MAYBE_PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted \
  PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted
#endif  // BUILDFLAG(IS_LINUX)
// 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_F(
    BackForwardCacheWithDedicatedWorkerBrowserTest,
    MAYBE_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.
//
// TODO(crbug.com/448724259): Flaky on MacOS.
#if BUILDFLAG(IS_MAC)
#define MAYBE_PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted_Nested \
  DISABLED_PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted_Nested
#else
#define MAYBE_PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted_Nested \
  PageWithDrainedDatapipeRequestsForFetchShouldBeEvicted_Nested
#endif
IN_PROC_BROWSER_TEST_F(
    BackForwardCacheWithDedicatedWorkerBrowserTest,
    MAYBE_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_F(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_F(
    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_F(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 and verify that we see the expected number
  // of worker clients.
  const int kExpectedWorkerClientCount = 1;
  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(kExpectedWorkerClientCount),
            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(kExpectedWorkerClientCount,
            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_F(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 and verify that we see the expected number
  // of worker clients.
  const int kExpectedWorkerClientCount = 2;
  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(kExpectedWorkerClientCount),
            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(kExpectedWorkerClientCount,
            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_F(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");
    });
  )")));

  EXPECT_EQ(1, CountWorkerClients(current_frame_host()));
}

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(),
          shell()->web_contents()->GetPrimaryMainFrame()->GetGlobalId(),
          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(
      std::ranges::contains(blocklist_values, sample, &base::Bucket::min));

  std::vector<base::Bucket> all_sites_blocklist_values =
      histogram_tester().GetAllSamples(
          "BackForwardCache.AllSites.HistoryNavigationOutcome."
          "BlocklistedFeature");

  EXPECT_TRUE(std::ranges::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);
}

// 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_F(BackForwardCacheBrowserTest,
                       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()));
  ExpectRestored(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.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
                       MultipleBlocksFromJavaScriptFile) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // 1) Navigate to a page with multiple WebRTC usage.
  GURL url_a(embedded_test_server()->GetURL(
      "a.com", "/back_forward_cache/page_with_webrtc_external_script.html"));
  GURL url_js(embedded_test_server()->GetURL(
      "a.com", "/back_forward_cache/webrtc_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 WebRTC connections.
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionA()"));
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionB()"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcA.signalingState"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcB.signalingState"));

  // 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::kWebRTC},
                    {}, {}, {}, FROM_HERE);
  auto& map = GetTreeResult()->GetBlockingDetailsMap();
  // Only WebRTC should be reported.
  EXPECT_EQ(map.size(), 1u);
  EXPECT_TRUE(
      map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC));
  // Both socketA and socketB's JavaScript locations should be reported.
  EXPECT_THAT(
      map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC),
      testing::UnorderedElementsAre(
          MatchesBlockingDetails(MatchesSourceLocation(url_js, "", 11, 9)),
          MatchesBlockingDetails(MatchesSourceLocation(url_js, "", 20, 9))));
}

// 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.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
                       BlockAndUnblockFromJavaScriptFile) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // 1) Navigate to a page with multiple WebRTC usage.
  GURL url_a(embedded_test_server()->GetURL(
      "a.com", "/back_forward_cache/page_with_webrtc_external_script.html"));
  GURL url_js(embedded_test_server()->GetURL(
      "a.com", "/back_forward_cache/webrtc_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 WebRTC connections pcA and pcB, but close pcA immediately.
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionA()"));
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionB()"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcA.signalingState"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcB.signalingState"));
  ASSERT_TRUE(ExecJs(rfh_a.get(), "closeConnectionA()"));
  ASSERT_EQ("closed", EvalJs(rfh_a.get(), "pcA.signalingState"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcB.signalingState"));

  // 2) Navigate to b.com.
  ASSERT_TRUE(NavigateToURL(shell(), url_b));

  // 3) Go back and ensure that the pcB's detail is captured.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ASSERT_EQ(url_a.spec(), current_frame_host()->GetLastCommittedURL());
  ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures},
                    {blink::scheduler::WebSchedulerTrackedFeature::kWebRTC},
                    {}, {}, {}, FROM_HERE);
  auto& map = GetTreeResult()->GetBlockingDetailsMap();
  // Only WebRTC should be reported.
  EXPECT_EQ(map.size(), 1u);
  EXPECT_TRUE(
      map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC));
  // Only pcB's JavaScript locations should be reported.
  EXPECT_THAT(map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC),
              testing::UnorderedElementsAre(MatchesBlockingDetails(
                  MatchesSourceLocation(url_js, "", 20, 9))));
}

// Use a blocklisted feature in multiple places from HTML file and make sure all
// the JavaScript locations detail are captured.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
                       MultipleBlocksFromHTMLFile) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // 1) Navigate to a page with multiple WebRTC usage.
  GURL url_a(embedded_test_server()->GetURL(
      "a.com", "/back_forward_cache/page_with_webrtc_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 WebRTC connections.
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionA()"));
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionB()"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcA.signalingState"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcB.signalingState"));

  // 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::kWebRTC},
                    {}, {}, {}, FROM_HERE);
  auto& map = GetTreeResult()->GetBlockingDetailsMap();
  // Only WebRTC should be reported.
  EXPECT_EQ(map.size(), 1u);
  EXPECT_TRUE(
      map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC));
  // Both pcA and pcB's JavaScript locations should be reported.
  EXPECT_THAT(
      map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC),
      testing::UnorderedElementsAre(
          MatchesBlockingDetails(MatchesSourceLocation(url_a, "", 12, 9)),
          MatchesBlockingDetails(MatchesSourceLocation(url_a, "", 21, 9))));
}

// 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.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
                       BlockAndUnblockFromHTMLFile) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // 1) Navigate to a page with multiple WebRTC channel usage.
  GURL url_a(embedded_test_server()->GetURL(
      "a.com", "/back_forward_cache/page_with_webrtc_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 WebRTC connections pcA and pcB, but close pcA immediately.
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionA()"));
  ASSERT_TRUE(ExecJs(rfh_a.get(), "openWebRTCConnectionB()"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcA.signalingState"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcB.signalingState"));
  ASSERT_TRUE(ExecJs(rfh_a.get(), "closeConnectionA()"));
  ASSERT_EQ("closed", EvalJs(rfh_a.get(), "pcA.signalingState"));
  ASSERT_EQ("stable", EvalJs(rfh_a.get(), "pcB.signalingState"));

  // 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::kWebRTC},
                    {}, {}, {}, FROM_HERE);
  auto& map = GetTreeResult()->GetBlockingDetailsMap();
  // Only WebRTC should be reported.
  EXPECT_EQ(map.size(), 1u);
  EXPECT_TRUE(
      map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC));
  // Only pcB's JavaScript locations should be reported.
  EXPECT_THAT(map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC),
              testing::UnorderedElementsAre(MatchesBlockingDetails(
                  MatchesSourceLocation(url_a, "", 21, 9))));
}

// Test that details for sticky feature are captured.
IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest,
                       StickyFeaturesWithDetails) {
  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 WebRTC connection.
  const char script[] = R"(
      new Promise(resolve => {
        const pc = new RTCPeerConnection();
        pc.addIceCandidate({ candidate: "test", sdpMLineIndex: 0 }).finally(()=>{
          resolve(42);
        });
      });)";
  ASSERT_EQ(42, EvalJs(rfh_a.get(), script));

  // 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::kWebRTC,
       blink::scheduler::WebSchedulerTrackedFeature::
           kMainResourceHasCacheControlNoStore,
       blink::scheduler::WebSchedulerTrackedFeature::kWebRTCSticky},
      {}, {}, {}, FROM_HERE);
  auto& map = GetTreeResult()->GetBlockingDetailsMap();
  EXPECT_EQ(map.size(), 3u);
  EXPECT_TRUE(
      map.contains(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC));
  EXPECT_TRUE(map.contains(
      blink::scheduler::WebSchedulerTrackedFeature::kWebRTCSticky));
  EXPECT_THAT(map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebRTC),
              testing::UnorderedElementsAre(MatchesBlockingDetails(
                  MatchesSourceLocation(GURL::EmptyGURL(), "", 4, 12))));
  EXPECT_THAT(
      map.at(blink::scheduler::WebSchedulerTrackedFeature::kWebRTCSticky),
      testing::UnorderedElementsAre(MatchesBlockingDetails(
          MatchesSourceLocation(GURL::EmptyGURL(), "", 4, 12))));
}

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.
//
// TODO(https://crbug.com/422753550): Reactivate test.
IN_PROC_BROWSER_TEST_F(
    BackForwardCacheBrowserTest,
    DISABLED_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);
}

// TODO(crbug.com/448785237): Flaky on MacOS.
#if BUILDFLAG(IS_MAC)
#define MAYBE_DoNotCacheIfIndexedDBTransactionHoldingLocksAndBlockingOthers \
  DISABLED_DoNotCacheIfIndexedDBTransactionHoldingLocksAndBlockingOthers
#else
#define MAYBE_DoNotCacheIfIndexedDBTransactionHoldingLocksAndBlockingOthers \
  DoNotCacheIfIndexedDBTransactionHoldingLocksAndBlockingOthers
#endif
IN_PROC_BROWSER_TEST_F(
    BackForwardCacheBrowserTest,
    MAYBE_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) {
  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, net::test_server::GetWebSocketURL(
                                             *embedded_test_server(),
                                             "/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 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);
}

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() = default;
  ~TestVibrationManager() override = default;

  void BindReceiver(
      mojo::PendingReceiver<device::mojom::VibrationManager> receiver) {
    receiver_.Bind(std::move(receiver));
  }

  [[nodiscard]] 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};
};

class BackForwardCacheVibrationBrowserTest
    : public BackForwardCacheBinderBrowserTest<TestVibrationManager> {
 protected:
  [[nodiscard]] EvalJsResult TriggerVibrate(RenderFrameHostImpl* rfh,
                                            int duration) {
    return EvalJs(rfh, JsReplace("navigator.vibrate($1)", duration));
  }

  [[nodiscard]] EvalJsResult TriggerShortVibrationSequence(
      RenderFrameHostImpl* rfh) {
    return EvalJs(rfh, "navigator.vibrate([10] * 1000)");
  }
};

// Tests that vibration stops after the page enters bfcache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheVibrationBrowserTest,
                       VibrationStopsAfterEnteringCache) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // 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_EQ(true, TriggerVibrate(rfh_a, 10000));
  EXPECT_FALSE(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(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(BackForwardCacheVibrationBrowserTest,
                       ShortVibrationSequenceStopsAfterEnteringCache) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // 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_EQ(true, TriggerShortVibrationSequence(rfh_a));
  EXPECT_FALSE(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(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 {
 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();
  }

  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)

enum TestAuthenticatorBehavior {
  kErrorOut,
  kStallRequest,
};

// An implementation of blink::mojom::Authenticator that errors all requests,
// this can be modified to stall all requests through SetBehavior.
class TestAuthenticator : public blink::mojom::Authenticator {
 public:
  TestAuthenticator() = default;
  ~TestAuthenticator() override = default;

  void BindReceiver(
      mojo::PendingReceiver<blink::mojom::Authenticator> receiver) {
    receiver_.Bind(std::move(receiver));
  }

  void SetBehavior(TestAuthenticatorBehavior behavior) { behavior_ = behavior; }

 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::GetCredentialOptionsPtr 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_ = TestAuthenticatorBehavior::kErrorOut;
  mojo::Receiver<blink::mojom::Authenticator> receiver_{this};
};

class BackForwardCacheWebAuthnBrowserTest
    : public BackForwardCacheBinderBrowserTest<TestAuthenticator> {
 protected:
  void SetUpOnMainThread() override {
    BackForwardCacheBinderBrowserTest<TestAuthenticator>::SetUpOnMainThread();
    ASSERT_TRUE(CreateHttpsServer()->Start());
  }

  void SetBehavior(TestAuthenticatorBehavior behavior) {
    Manager().SetBehavior(behavior);
  }
};

// Tests that an ongoing WebAuthn get assertion request disables BFcache.
IN_PROC_BROWSER_TEST_F(BackForwardCacheWebAuthnBrowserTest,
                       GetAssertion_NoCachingDuringRequest) {
  SetBehavior(kStallRequest);
  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(BackForwardCacheWebAuthnBrowserTest,
                       GetAssertion_CacheAfterRequest) {
  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(BackForwardCacheWebAuthnBrowserTest,
                       MakeCredential_NoCachingDuringRequest) {
  SetBehavior(kStallRequest);
  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(BackForwardCacheWebAuthnBrowserTest,
                       MakeCredential_CacheAfterRequest) {
  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()));
  }
};

IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestWithMediaSession,
                       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);
}

#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
