// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/site_per_process_browsertest.h"

#include <list>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/cross_process_frame_connector.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigator.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/common/url_constants.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_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/render_document_feature.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::ElementsAre;
using testing::WhenSorted;

namespace content {

namespace {

void UnloadPrint(const ToRenderFrameHost& target, const char* message) {
  EXPECT_TRUE(
      ExecJs(target, JsReplace("window.onunload = function() { "
                               "  window.domAutomationController.send($1);"
                               "}",
                               message)));
}

}  // namespace

// Tests that there are no crashes if a subframe is detached in its unload
// handler. See https://crbug.com/590054.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, DetachInUnloadHandler) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(b))"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "        +--Site B -- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = http://b.com/",
      DepictFrameTree(root));

  EXPECT_EQ(1, EvalJs(root->child_at(0), "frames.length;"));

  RenderFrameDeletedObserver deleted_observer(
      root->child_at(0)->child_at(0)->current_frame_host());

  // Add an unload handler to the grandchild that causes it to be synchronously
  // detached, then navigate it.
  EXPECT_TRUE(
      ExecJs(root->child_at(0)->child_at(0),
             "window.onunload=function(e){\n"
             "    window.parent.document.getElementById('child-0').remove();\n"
             "};\n"));
  auto script = JsReplace("window.document.getElementById('child-0').src = $1",
                          embedded_test_server()->GetURL(
                              "c.com", "/cross_site_iframe_factory.html?c"));
  EXPECT_TRUE(ExecJs(root->child_at(0), script));

  deleted_observer.WaitUntilDeleted();

  EXPECT_EQ(0, EvalJs(root->child_at(0), "frames.length;"));

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = http://b.com/",
      DepictFrameTree(root));
}

// Tests that trying to navigate in the unload handler doesn't crash the
// browser.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, NavigateInUnloadHandler) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(b))"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "        +--Site B -- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = http://b.com/",
      DepictFrameTree(root));

  EXPECT_EQ(1,
            EvalJs(root->child_at(0)->current_frame_host(), "frames.length;"));

  // Add an unload handler to B's subframe.
  EXPECT_TRUE(ExecJs(root->child_at(0)->child_at(0)->current_frame_host(),
                     "window.onunload=function(e){\n"
                     "    window.location = '#navigate';\n"
                     "};\n"));

  // Navigate B's subframe to a cross-site C.
  RenderFrameDeletedObserver deleted_observer(
      root->child_at(0)->child_at(0)->current_frame_host());
  auto script = JsReplace("window.document.getElementById('child-0').src = $1",
                          embedded_test_server()->GetURL(
                              "c.com", "/cross_site_iframe_factory.html"));
  EXPECT_TRUE(ExecJs(root->child_at(0)->current_frame_host(), script));

  // Wait until B's subframe RenderFrameHost is destroyed.
  deleted_observer.WaitUntilDeleted();

  // Check that C's subframe is alive and the navigation in the unload handler
  // was ignored.
  EXPECT_EQ(0, EvalJs(root->child_at(0)->child_at(0)->current_frame_host(),
                      "frames.length;"));

  EXPECT_EQ(
      " Site A ------------ proxies for B C\n"
      "   +--Site B ------- proxies for A C\n"
      "        +--Site C -- proxies for A B\n"
      "Where A = http://a.com/\n"
      "      B = http://b.com/\n"
      "      C = http://c.com/",
      DepictFrameTree(root));
}

// Verifies that when navigating an OOPIF to same site and then canceling
// navigation from beforeunload handler popup will not remove the
// RemoteFrameView from OOPIF's owner element in the parent process. This test
// uses OOPIF visibility to make sure RemoteFrameView exists after beforeunload
// is handled.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       CanceledBeforeUnloadShouldNotClearRemoteFrameView) {
  GURL a_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), a_url));

  FrameTreeNode* child_node =
      web_contents()->GetPrimaryFrameTree().root()->child_at(0);
  GURL b_url(embedded_test_server()->GetURL(
      "b.com", "/render_frame_host/beforeunload.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(child_node, b_url));
  CrossProcessFrameConnector* frame_connector_delegate =
      static_cast<RenderWidgetHostViewChildFrame*>(
          child_node->current_frame_host()->GetView())
          ->FrameConnectorForTesting();

  // Need user gesture for 'beforeunload' to fire.
  PrepContentsForBeforeUnloadTest(web_contents());

  // Simulate user choosing to stay on the page after beforeunload fired.
  SetShouldProceedOnBeforeUnload(shell(), true /* proceed */,
                                 false /* success */);

  // First, hide the <iframe>. This goes through RemoteFrameView::Hide() and
  // eventually updates the CrossProcessFrameConnector. Also,
  // RemoteFrameView::self_visible_ will be set to false which can only be
  // undone by calling RemoteFrameView::Show. Therefore, potential calls to
  // RemoteFrameView::SetParentVisible(true) would not update the visibility at
  // the browser side.
  ASSERT_TRUE(
      ExecJs(web_contents(),
             "document.querySelector('iframe').style.visibility = 'hidden';"));
  while (!frame_connector_delegate->IsHidden()) {
    base::RunLoop run_loop;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
    run_loop.Run();
  }

  // Now we navigate the child to about:blank, but since we do not proceed with
  // the navigation, the OOPIF should stay alive and RemoteFrameView intact.
  AppModalDialogWaiter dialog_waiter(shell());
  ASSERT_TRUE(ExecJs(web_contents(),
                     "document.querySelector('iframe').src = 'about:blank';"));
  dialog_waiter.Wait();

  // Sanity check: We should still have an OOPIF and hence a RWHVCF.
  ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(
                  child_node->current_frame_host()->GetView())
                  ->IsRenderWidgetHostViewChildFrame());

  // Now make the <iframe> visible again. This calls RemoteFrameView::Show()
  // only if the RemoteFrameView is the EmbeddedContentView of the corresponding
  // HTMLFrameOwnerElement.
  ASSERT_TRUE(
      ExecJs(web_contents(),
             "document.querySelector('iframe').style.visibility = 'visible';"));
  while (frame_connector_delegate->IsHidden()) {
    base::RunLoop run_loop;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
    run_loop.Run();
  }
}

// Ensure that after a main frame with an OOPIF is navigated cross-site, the
// unload handler in the OOPIF sees correct main frame origin, namely the old
// and not the new origin.  See https://crbug.com/825283.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       ParentOriginDoesNotChangeInUnloadHandler) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Open a popup on b.com.  The b.com subframe on the main frame will use this
  // in its unload handler.
  GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));

  // Save the WebContents instance created via the popup to be able to listen
  // for messages that occur in it.
  auto* popup_shell = OpenPopup(shell()->web_contents(), b_url, "popup");
  WebContents* popup_web_contents = popup_shell->web_contents();

  // Add an unload handler to b.com subframe, which will look up the top
  // frame's origin and send it via domAutomationController.  Unfortunately,
  // the subframe's browser-side state will have been torn down when it runs
  // the unload handler, so to ensure that the message can be received, send it
  // through the popup.
  EXPECT_TRUE(ExecJs(root->child_at(0),
                     "window.onunload = function(e) {"
                     "  window.open('','popup').domAutomationController.send("
                     "      'top-origin ' + location.ancestorOrigins[0]);"
                     "};"));

  // Navigate the main frame to c.com and wait for the message from the
  // subframe's unload handler.
  GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));

  // NOTE: The message occurs in the WebContents for the popup.
  DOMMessageQueue msg_queue(popup_web_contents);
  EXPECT_TRUE(NavigateToURL(shell(), c_url));
  std::string message, top_origin;
  while (msg_queue.WaitForMessage(&message)) {
    base::TrimString(message, "\"", &message);
    auto message_parts = base::SplitString(message, " ", base::TRIM_WHITESPACE,
                                           base::SPLIT_WANT_NONEMPTY);
    if (message_parts[0] == "top-origin") {
      top_origin = message_parts[1];
      break;
    }
  }

  // The top frame's origin should be a.com, not c.com.
  EXPECT_EQ(top_origin + "/", main_url.DeprecatedGetOriginAsURL().spec());
}

// Verify that when the last active frame in a process is going away as part of
// OnUnload, the mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame is
// received prior to the process starting to shut down, ensuring that any
// related unload work also happens before shutdown. See
// https://crbug.com/867274 and https://crbug.com/794625.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       UnloadACKArrivesPriorToProcessShutdownRequest) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  RenderFrameHostImpl* rfh = web_contents()->GetPrimaryMainFrame();
  rfh->DisableUnloadTimerForTesting();

  // Navigate cross-site.  Since the current frame is the last active frame in
  // the current process, the process will eventually shut down.  Once the
  // process goes away, ensure that the
  // mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame was received (i.e.,
  // that we didn't just simulate OnUnloaded() due to the process erroneously
  // going away before the mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame
  // was received, as in https://crbug.com/867274).
  RenderProcessHostWatcher watcher(
      rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  bool received_unload = false;
  auto unload_ack_filter = base::BindLambdaForTesting([&]() {
    received_unload = true;
    return false;
  });
  rfh->SetUnloadACKCallbackForTesting(unload_ack_filter);

  // Disable the BackForwardCache to ensure the old process is going to be
  // released.
  DisableBackForwardCacheForTesting(web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(shell(), cross_site_url));
  watcher.Wait();
  EXPECT_TRUE(received_unload);
  EXPECT_TRUE(watcher.did_exit_normally());
}

// This is a regression test for https://crbug.com/891423 in which tabs showing
// beforeunload dialogs stalled navigation and triggered the "hung process"
// dialog.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       NoCommitTimeoutWithBeforeUnloadDialog) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());

  // Navigate first tab to a.com.
  GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), a_url));
  RenderProcessHost* a_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  // Open b.com in a second tab.  Using a renderer-initiated navigation is
  // important to leave a.com and b.com SiteInstances in the same
  // BrowsingInstance (so the b.com -> a.com navigation in the next test step
  // will reuse the process associated with the first a.com tab).
  GURL b_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  Shell* new_shell = OpenPopup(web_contents, b_url, "newtab");
  WebContents* new_contents = new_shell->web_contents();
  EXPECT_TRUE(WaitForLoadStop(new_contents));
  RenderProcessHost* b_process =
      new_contents->GetPrimaryMainFrame()->GetProcess();
  EXPECT_NE(a_process, b_process);

  // Disable the beforeunload hang monitor (otherwise there will be a race
  // between the beforeunload dialog and the beforeunload hang timer) and give
  // the page a gesture to allow dialogs.
  web_contents->GetPrimaryMainFrame()
      ->DisableBeforeUnloadHangMonitorForTesting();
  web_contents->GetPrimaryMainFrame()->ExecuteJavaScriptWithUserGestureForTests(
      std::u16string(), base::NullCallback());

  // Hang the first contents in a beforeunload dialog.
  BeforeUnloadBlockingDelegate test_delegate(web_contents);
  EXPECT_TRUE(
      ExecJs(web_contents, "window.onbeforeunload=function(e){ return 'x' }"));
  EXPECT_TRUE(ExecJs(web_contents,
                     "setTimeout(function() { window.location.reload() }, 0)"));
  test_delegate.Wait();

  // Attempt to navigate the second tab to a.com.  This will attempt to reuse
  // the hung process.
  base::TimeDelta kTimeout = base::Milliseconds(100);
  NavigationRequest::SetCommitTimeoutForTesting(kTimeout);
  GURL hung_url(embedded_test_server()->GetURL("a.com", "/title3.html"));
  UnresponsiveRendererObserver unresponsive_renderer_observer(new_contents);
  EXPECT_TRUE(
      ExecJs(new_contents, JsReplace("window.location = $1", hung_url)));

  // Verify that we will not be notified about the unresponsive renderer.
  // Before changes in https://crrev.com/c/1089797, the test would get notified
  // and therefore |hung_process| would be non-null.
  RenderProcessHost* hung_process =
      unresponsive_renderer_observer.Wait(kTimeout * 10);
  EXPECT_FALSE(hung_process);

  // Reset the timeout.
  NavigationRequest::SetCommitTimeoutForTesting(base::TimeDelta());
}

// Test that unload handlers in iframes are run, even when the removed subtree
// is complicated with nested iframes in different processes.
//     A1                         A1
//    / \                        / \
//   B1  D  --- Navigate --->   E   D
//  / \
// C1  C2
// |   |
// B2  A2
//     |
//     C3
// TODO(crbug.com/1012185): Flaky timeouts on Linux and Mac.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC)
#define MAYBE_UnloadHandlerSubframes DISABLED_UnloadHandlerSubframes
#else
#define MAYBE_UnloadHandlerSubframes UnloadHandlerSubframes
#endif
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       MAYBE_UnloadHandlerSubframes) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(c(b),c(a(c))),d)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Add a unload handler to every frames. It notifies the browser using the
  // DomAutomationController it has been executed.
  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  UnloadPrint(root, "A1");
  UnloadPrint(root->child_at(0), "B1");
  UnloadPrint(root->child_at(0)->child_at(0), "C1");
  UnloadPrint(root->child_at(0)->child_at(1), "C2");
  UnloadPrint(root->child_at(0)->child_at(0)->child_at(0), "B2");
  UnloadPrint(root->child_at(0)->child_at(1)->child_at(0), "A2");
  UnloadPrint(root->child_at(0)->child_at(1)->child_at(0)->child_at(0), "C3");
  DOMMessageQueue dom_message_queue(
      WebContents::FromRenderFrameHost(web_contents()->GetPrimaryMainFrame()));

  // Disable the unload timer on B1.
  root->child_at(0)->current_frame_host()->DisableUnloadTimerForTesting();

  // Process B and C are expected to shutdown once every unload handler has
  // run.
  RenderProcessHostWatcher shutdown_B(
      root->child_at(0)->current_frame_host()->GetProcess(),
      RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  RenderProcessHostWatcher shutdown_C(
      root->child_at(0)->child_at(0)->current_frame_host()->GetProcess(),
      RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // Navigate B to E.
  GURL e_url(embedded_test_server()->GetURL("e.com", "/title1.html"));
  NavigateFrameToURL(root->child_at(0), e_url);

  // Collect unload handler messages.
  std::string message;
  std::vector<std::string> messages;
  for (int i = 0; i < 6; ++i) {
    EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
    base::TrimString(message, "\"", &message);
    messages.push_back(message);
  }
  EXPECT_FALSE(dom_message_queue.PopMessage(&message));

  // Check every frame in the replaced subtree has executed its unload handler.
  EXPECT_THAT(messages,
              WhenSorted(ElementsAre("A2", "B1", "B2", "C1", "C2", "C3")));

  // In every renderer process, check ancestors have executed their unload
  // handler before their children. This is a slightly less restrictive
  // condition than the specification which requires it to be global instead of
  // per process.
  // https://html.spec.whatwg.org/multipage/browsing-the-web.html#unloading-documents
  //
  // In process B:
  auto B1 = base::ranges::find(messages, "B1");
  auto B2 = base::ranges::find(messages, "B2");
  EXPECT_LT(B1, B2);

  // In process C:
  auto C2 = base::ranges::find(messages, "C2");
  auto C3 = base::ranges::find(messages, "C3");
  EXPECT_LT(C2, C3);

  // Make sure the processes are deleted at some point.
  shutdown_B.Wait();
  shutdown_C.Wait();
}

// Check that unload handlers in iframe don't prevents the main frame to be
// deleted after a timeout.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, SlowUnloadHandlerInIframe) {
  GURL initial_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  GURL next_url(embedded_test_server()->GetURL("c.com", "/title1.html"));

  // 1) Navigate on a page with an iframe.
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));

  // 2) Act as if there was an infinite unload handler in B.
  RenderFrameHostImpl* rfh_b = web_contents()
                                   ->GetPrimaryFrameTree()
                                   .root()
                                   ->child_at(0)
                                   ->current_frame_host();
  rfh_b->DoNotDeleteForTesting();

  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  DisableBackForwardCacheForTesting(web_contents(),
                                    BackForwardCache::TEST_USES_UNLOAD_EVENT);

  // 3) Navigate and check the old document is deleted after some time.
  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  RenderFrameDeletedObserver deleted_observer(root->current_frame_host());
  EXPECT_TRUE(NavigateToURL(shell(), next_url));
  deleted_observer.WaitUntilDeleted();
}

// Navigate from A(B(A(B)) to C. Check the unload handler are executed, executed
// in the right order and the processes for A and B are removed.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, Unload_ABAB) {
  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  web_contents()->GetController().GetBackForwardCache().DisableForTesting(
      content::BackForwardCache::TEST_USES_UNLOAD_EVENT);

  GURL initial_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(a(b)))"));
  GURL next_url(embedded_test_server()->GetURL("c.com", "/title1.html"));

  // 1) Navigate on a page with an iframe.
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));

  // 2) Add unload handler on every frame.
  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  UnloadPrint(root, "A1");
  UnloadPrint(root->child_at(0), "B1");
  UnloadPrint(root->child_at(0)->child_at(0), "A2");
  UnloadPrint(root->child_at(0)->child_at(0)->child_at(0), "B2");
  root->current_frame_host()->DisableUnloadTimerForTesting();

  DOMMessageQueue dom_message_queue(
      WebContents::FromRenderFrameHost(web_contents()->GetPrimaryMainFrame()));
  RenderProcessHostWatcher shutdown_A(
      root->current_frame_host()->GetProcess(),
      RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  RenderProcessHostWatcher shutdown_B(
      root->child_at(0)->current_frame_host()->GetProcess(),
      RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // 3) Navigate cross process.
  EXPECT_TRUE(NavigateToURL(shell(), next_url));

  // 4) Wait for unload handler messages and check they are sent in order.
  std::vector<std::string> messages;
  std::string message;
  for (int i = 0; i < 4; ++i) {
    EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
    base::TrimString(message, "\"", &message);
    messages.push_back(message);
  }
  EXPECT_FALSE(dom_message_queue.PopMessage(&message));

  EXPECT_THAT(messages, WhenSorted(ElementsAre("A1", "A2", "B1", "B2")));
  auto A1 = base::ranges::find(messages, "A1");
  auto A2 = base::ranges::find(messages, "A2");
  auto B1 = base::ranges::find(messages, "B1");
  auto B2 = base::ranges::find(messages, "B2");
  EXPECT_LT(A1, A2);
  EXPECT_LT(B1, B2);

  // Make sure the processes are deleted at some point.
  shutdown_A.Wait();
  shutdown_B.Wait();
}

// Start with A(B(C)), navigate C to D and then B to E. By emulating a slow
// unload handler in B,C and D, the end result is C is in pending deletion in B
// and B is in pending deletion in A.
//   (1)     (2)     (3)
//|       |       |       |
//|   A   |  A    |   A   |
//|   |   |  |    |    \  |
//|   B   |  B    |  B  E |
//|   |   |   \   |   \   |
//|   C   | C  D  | C  D  |
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, UnloadNestedPendingDeletion) {
  std::string onunload_script = "window.onunload = function(){}";
  GURL url_abc(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(c))"));
  GURL url_d(embedded_test_server()->GetURL("d.com", "/title1.html"));
  GURL url_e(embedded_test_server()->GetURL("e.com", "/title1.html"));

  // 1) Navigate to a page with an iframe.
  EXPECT_TRUE(NavigateToURL(shell(), url_abc));
  RenderFrameHostImpl* rfh_a = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* rfh_b = rfh_a->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_c = rfh_b->child_at(0)->current_frame_host();
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kActive,
            rfh_a->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kActive,
            rfh_b->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kActive,
            rfh_c->lifecycle_state());

  // Act as if there was a slow unload handler on rfh_b and rfh_c.
  // The navigating frames are waiting for
  // mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame.
  auto unload_ack_filter = base::BindRepeating([] { return true; });
  rfh_b->SetUnloadACKCallbackForTesting(unload_ack_filter);
  rfh_c->SetUnloadACKCallbackForTesting(unload_ack_filter);
  EXPECT_TRUE(ExecJs(rfh_b->frame_tree_node(), onunload_script));
  EXPECT_TRUE(ExecJs(rfh_c->frame_tree_node(), onunload_script));
  rfh_b->DisableUnloadTimerForTesting();
  rfh_c->DisableUnloadTimerForTesting();

  RenderFrameDeletedObserver delete_b(rfh_b), delete_c(rfh_c);

  // 2) Navigate rfh_c to D.
  EXPECT_TRUE(NavigateToURLFromRenderer(rfh_c->frame_tree_node(), url_d));
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kActive,
            rfh_a->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kActive,
            rfh_b->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            rfh_c->lifecycle_state());
  RenderFrameHostImpl* rfh_d = rfh_b->child_at(0)->current_frame_host();
  // Set an arbitrarily long timeout to ensure the subframe unload timer doesn't
  // fire before we call OnDetach().
  rfh_d->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));

  RenderFrameDeletedObserver delete_d(rfh_d);

  // Act as if there was a slow unload handler on rfh_d.
  // The non navigating frames are waiting for mojom::FrameHost::Detach.
  rfh_d->DoNotDeleteForTesting();
  EXPECT_TRUE(ExecJs(rfh_d->frame_tree_node(), onunload_script));

  // 3) Navigate rfh_b to E.
  EXPECT_TRUE(NavigateToURLFromRenderer(rfh_b->frame_tree_node(), url_e));
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kActive,
            rfh_a->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            rfh_b->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            rfh_c->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            rfh_d->lifecycle_state());

  // rfh_d completes its unload event. It deletes the frame, including rfh_c.
  EXPECT_FALSE(delete_c.deleted());
  EXPECT_FALSE(delete_d.deleted());
  rfh_d->DetachForTesting();
  EXPECT_TRUE(delete_c.deleted());
  EXPECT_TRUE(delete_d.deleted());

  // rfh_b completes its unload event.
  EXPECT_FALSE(delete_b.deleted());
  rfh_b->SetUnloadACKCallbackForTesting(base::NullCallback());
  rfh_b->OnUnloadACK();
  EXPECT_TRUE(delete_b.deleted());
}

// A set of nested frames A1(B1(A2)) are pending deletion because of a
// navigation. This tests what happens if only A2 has an unload handler.
// If B1's mojom::FrameHost::Detach is called before A2, it should not destroy
// itself and its children, but rather wait for A2.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, PartialUnloadHandler) {
  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  web_contents()->GetController().GetBackForwardCache().DisableForTesting(
      content::BackForwardCache::TEST_USES_UNLOAD_EVENT);

  GURL url_aba(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(a))"));
  GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html"));

  // 1) Navigate to A1(B1(A2))
  EXPECT_TRUE(NavigateToURL(shell(), url_aba));

  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* a1 = root->current_frame_host();
  RenderFrameHostImpl* b1 = a1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* a2 = b1->child_at(0)->current_frame_host();
  RenderFrameDeletedObserver delete_a1(a1);
  RenderFrameDeletedObserver delete_a2(a2);
  RenderFrameDeletedObserver delete_b1(b1);

  // Disable Detach and mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame.
  // They will be called manually.
  auto unload_ack_filter = base::BindRepeating([] { return true; });
  a1->SetUnloadACKCallbackForTesting(unload_ack_filter);
  a1->DoNotDeleteForTesting();
  a2->DoNotDeleteForTesting();

  a1->DisableUnloadTimerForTesting();
  // Set an arbitrarily long timeout to ensure the subframe unload timer doesn't
  // fire before we call OnDetach().
  b1->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));

  // Add unload handler on A2, but not on the other frames.
  UnloadPrint(a2->frame_tree_node(), "A2");

  DOMMessageQueue dom_message_queue(
      WebContents::FromRenderFrameHost(web_contents()->GetPrimaryMainFrame()));

  // 2) Navigate cross process.
  EXPECT_TRUE(NavigateToURL(shell(), url_c));

  // Check that unload handlers are executed.
  std::string message, message_unused;
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
  EXPECT_FALSE(dom_message_queue.PopMessage(&message_unused));
  EXPECT_EQ("\"A2\"", message);

  // No RenderFrameHost are deleted so far.
  EXPECT_FALSE(delete_a1.deleted());
  EXPECT_FALSE(delete_b1.deleted());
  EXPECT_FALSE(delete_a2.deleted());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            a1->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kReadyToBeDeleted,
            b1->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            a2->lifecycle_state());

  // 3) B1 receives confirmation it has been deleted. This has no effect,
  //    because it is still waiting on A2 to be deleted.
  b1->DetachForTesting();
  EXPECT_FALSE(delete_a1.deleted());
  EXPECT_FALSE(delete_b1.deleted());
  EXPECT_FALSE(delete_a2.deleted());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            a1->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kReadyToBeDeleted,
            b1->lifecycle_state());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            a2->lifecycle_state());

  // 4) A2 received confirmation that it has been deleted and destroy B1 and A2.
  a2->DetachForTesting();
  EXPECT_FALSE(delete_a1.deleted());
  EXPECT_TRUE(delete_b1.deleted());
  EXPECT_TRUE(delete_a2.deleted());
  EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers,
            a1->lifecycle_state());

  // 5) A1 receives mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame and
  // deletes itself.
  a1->ResumeDeletionForTesting();
  a1->SetUnloadACKCallbackForTesting(base::NullCallback());
  a1->OnUnloadACK();
  EXPECT_TRUE(delete_a1.deleted());
}

// Test RenderFrameHostImpl::PendingDeletionCheckCompletedOnSubtree.
//
// After a navigation commit, some children with no unload handler may be
// eligible for immediate deletion. Several configurations are tested:
//
// Before navigation commit
//
//              0               |  N  : No unload handler
//   ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑      | [N] : Unload handler
//  |  |  |  |  |   |     |     |
// [1] 2 [3] 5  7   9     12    |
//        |  |  |  / \   / \    |
//        4 [6] 8 10 11 13 [14] |
//
// After navigation commit (expected)
//
//              0               |  N  : No unload handler
//   ---------------------      | [N] : Unload handler
//  |     |  |            |     |
// [1]   [3] 5            12    |
//           |             \    |
//          [6]            [14] |
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       PendingDeletionCheckCompletedOnSubtree) {
  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  web_contents()->GetController().GetBackForwardCache().DisableForTesting(
      content::BackForwardCache::TEST_USES_UNLOAD_EVENT);

  GURL url_1(embedded_test_server()->GetURL(
      "a.com",
      "/cross_site_iframe_factory.html?a(a,a,a(a),a(a),a(a),a(a,a),a(a,a))"));
  GURL url_2(embedded_test_server()->GetURL("b.com", "/title1.html"));

  // 1) Navigate to 0(1,2,3(4),5(6),7(8),9(10,11),12(13,14));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* rfh_0 = root->current_frame_host();
  RenderFrameHostImpl* rfh_1 = rfh_0->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_2 = rfh_0->child_at(1)->current_frame_host();
  RenderFrameHostImpl* rfh_3 = rfh_0->child_at(2)->current_frame_host();
  RenderFrameHostImpl* rfh_4 = rfh_3->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_5 = rfh_0->child_at(3)->current_frame_host();
  RenderFrameHostImpl* rfh_6 = rfh_5->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_7 = rfh_0->child_at(4)->current_frame_host();
  RenderFrameHostImpl* rfh_8 = rfh_7->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_9 = rfh_0->child_at(5)->current_frame_host();
  RenderFrameHostImpl* rfh_10 = rfh_9->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_11 = rfh_9->child_at(1)->current_frame_host();
  RenderFrameHostImpl* rfh_12 = rfh_0->child_at(6)->current_frame_host();
  RenderFrameHostImpl* rfh_13 = rfh_12->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_14 = rfh_12->child_at(1)->current_frame_host();

  RenderFrameDeletedObserver delete_a0(rfh_0), delete_a1(rfh_1),
      delete_a2(rfh_2), delete_a3(rfh_3), delete_a4(rfh_4), delete_a5(rfh_5),
      delete_a6(rfh_6), delete_a7(rfh_7), delete_a8(rfh_8), delete_a9(rfh_9),
      delete_a10(rfh_10), delete_a11(rfh_11), delete_a12(rfh_12),
      delete_a13(rfh_13), delete_a14(rfh_14);

  // Add the unload handlers.
  UnloadPrint(rfh_1->frame_tree_node(), "");
  UnloadPrint(rfh_3->frame_tree_node(), "");
  UnloadPrint(rfh_6->frame_tree_node(), "");
  UnloadPrint(rfh_14->frame_tree_node(), "");

  // Disable Detach and mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame.
  auto unload_ack_filter = base::BindRepeating([] { return true; });
  rfh_0->SetUnloadACKCallbackForTesting(unload_ack_filter);
  rfh_0->DoNotDeleteForTesting();
  rfh_1->DoNotDeleteForTesting();
  rfh_3->DoNotDeleteForTesting();
  rfh_5->DoNotDeleteForTesting();
  rfh_6->DoNotDeleteForTesting();
  rfh_12->DoNotDeleteForTesting();
  rfh_14->DoNotDeleteForTesting();
  rfh_0->DisableUnloadTimerForTesting();

  // 2) Navigate cross process and check the tree. See diagram above.
  EXPECT_TRUE(NavigateToURL(shell(), url_2));

  EXPECT_FALSE(delete_a0.deleted());
  EXPECT_FALSE(delete_a1.deleted());
  EXPECT_TRUE(delete_a2.deleted());
  EXPECT_FALSE(delete_a3.deleted());
  EXPECT_TRUE(delete_a4.deleted());
  EXPECT_FALSE(delete_a5.deleted());
  EXPECT_FALSE(delete_a6.deleted());
  EXPECT_TRUE(delete_a7.deleted());
  EXPECT_TRUE(delete_a8.deleted());
  EXPECT_TRUE(delete_a9.deleted());
  EXPECT_TRUE(delete_a10.deleted());
  EXPECT_TRUE(delete_a11.deleted());
  EXPECT_FALSE(delete_a12.deleted());
  EXPECT_TRUE(delete_a13.deleted());
  EXPECT_FALSE(delete_a14.deleted());
}

// When an iframe is detached, check that unload handlers execute in all of its
// child frames. Start from A(B(C)) and delete B from A.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       DetachedIframeUnloadHandlerABC) {
  GURL initial_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(c))"));

  // 1) Navigate to a(b(c))
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* rfh_a = root->current_frame_host();
  RenderFrameHostImpl* rfh_b = rfh_a->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_c = rfh_b->child_at(0)->current_frame_host();

  // 2) Add unload handlers on B and C.
  UnloadPrint(rfh_b->frame_tree_node(), "B");
  UnloadPrint(rfh_c->frame_tree_node(), "C");

  DOMMessageQueue dom_message_queue(web_contents());
  RenderProcessHostWatcher shutdown_B(
      rfh_b->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  RenderProcessHostWatcher shutdown_C(
      rfh_c->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // 3) Detach B from A.
  ExecuteScriptAsync(root, "document.querySelector('iframe').remove();");

  // 4) Wait for unload handler.
  std::vector<std::string> messages(2);
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[0]));
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[1]));
  std::string unused;
  EXPECT_FALSE(dom_message_queue.PopMessage(&unused));

  std::sort(messages.begin(), messages.end());
  EXPECT_EQ("\"B\"", messages[0]);
  EXPECT_EQ("\"C\"", messages[1]);

  // Make sure the processes are deleted at some point.
  shutdown_B.Wait();
  shutdown_C.Wait();
}

#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
// Too slow under sanitizers, even with increased timeout:
// https://crbug.com/1096612
#define MAYBE_DetachedIframeUnloadHandlerABCB \
  DISABLED_DetachedIframeUnloadHandlerABCB
#else
#define MAYBE_DetachedIframeUnloadHandlerABCB DetachedIframeUnloadHandlerABCB
#endif

// When an iframe is detached, check that unload handlers execute in all of its
// child frames. Start from A(B1(C(B2))) and delete B1 from A.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       MAYBE_DetachedIframeUnloadHandlerABCB) {
  // This test takes longer to run, because multiple processes are waiting on
  // each other's documents to execute unload handler before destroying their
  // documents. https://crbug.com/1311985
  base::test::ScopedRunLoopTimeout increase_timeout(
      FROM_HERE, TestTimeouts::action_max_timeout());

  GURL initial_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(c(b)))"));

  // 1) Navigate to a(b(c(b)))
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* rfh_a = root->current_frame_host();
  RenderFrameHostImpl* rfh_b1 = rfh_a->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_c = rfh_b1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_b2 = rfh_c->child_at(0)->current_frame_host();

  // 2) Add unload handlers on B1, B2 and C.
  UnloadPrint(rfh_b1->frame_tree_node(), "B1");
  UnloadPrint(rfh_b2->frame_tree_node(), "B2");
  UnloadPrint(rfh_c->frame_tree_node(), "C");

  DOMMessageQueue dom_message_queue(web_contents());
  RenderProcessHostWatcher shutdown_B(
      rfh_b1->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  RenderProcessHostWatcher shutdown_C(
      rfh_c->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // 3) Detach B from A.
  ExecuteScriptAsync(root, "document.querySelector('iframe').remove();");

  // 4) Wait for unload handler.
  std::vector<std::string> messages(3);
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[0]));
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[1]));
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[2]));
  std::string unused;
  EXPECT_FALSE(dom_message_queue.PopMessage(&unused));

  std::sort(messages.begin(), messages.end());
  EXPECT_EQ("\"B1\"", messages[0]);
  EXPECT_EQ("\"B2\"", messages[1]);
  EXPECT_EQ("\"C\"", messages[2]);

  // Make sure the processes are deleted at some point.
  shutdown_B.Wait();
  shutdown_C.Wait();
}

// When an iframe is detached, check that unload handlers execute in all of its
// child frames. Start from A1(A2(B)), delete A2 from itself.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       DetachedIframeUnloadHandlerAAB) {
  GURL initial_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a(b))"));

  // 1) Navigate to a(a(b)).
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* rfh_a1 = root->current_frame_host();
  RenderFrameHostImpl* rfh_a2 = rfh_a1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* rfh_b = rfh_a2->child_at(0)->current_frame_host();

  // 2) Add unload handlers on A2 ad B.
  UnloadPrint(rfh_a2->frame_tree_node(), "A2");
  UnloadPrint(rfh_b->frame_tree_node(), "B");

  DOMMessageQueue dom_message_queue(web_contents());
  RenderProcessHostWatcher shutdown_B(
      rfh_b->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // 3) A2 detaches itself.
  ExecuteScriptAsync(rfh_a2->frame_tree_node(),
                     "parent.document.querySelector('iframe').remove();");

  // 4) Wait for unload handler.
  std::vector<std::string> messages(2);
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[0]));
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&messages[1]));
  std::string unused;
  EXPECT_FALSE(dom_message_queue.PopMessage(&unused));

  std::sort(messages.begin(), messages.end());
  EXPECT_EQ("\"A2\"", messages[0]);
  EXPECT_EQ("\"B\"", messages[1]);

  // Make sure the process is deleted at some point.
  shutdown_B.Wait();
}

// Tests that running layout from an unload handler inside teardown of the
// RenderWidget (inside WidgetMsg_Close) can succeed.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       RendererInitiatedWindowCloseWithUnload) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();

  // We will window.open() another URL on the same domain so they share a
  // renderer. This window has an unload handler that forces layout to occur.
  // Then we (in a new stack) close that window causing that layout. If all
  // goes well the window closes. If it goes poorly, the renderer may crash.
  //
  // This path is special because the unload results from window.close() which
  // avoids the user-initiated close path through ViewMsg_ClosePage. In that
  // path the unload handlers are run early, before the actual teardown of
  // the closing RenderWidget.
  GURL open_url = embedded_test_server()->GetURL(
      "a.com", "/unload_handler_force_layout.html");

  // Listen for messages from the window that the test opens, and convert them
  // into the document title, which we can wait on in the main test window.
  EXPECT_TRUE(ExecJs(root,
                     "window.addEventListener('message', function(event) {\n"
                     "  document.title = event.data;\n"
                     "});"));

  // This performs window.open() and waits for the title of the original
  // document to change to signal that the unload handler has been registered.
  {
    std::u16string title_when_loaded = u"loaded";
    TitleWatcher title_watcher(shell()->web_contents(), title_when_loaded);
    EXPECT_TRUE(ExecJs(root, JsReplace("var w = window.open($1)", open_url)));
    EXPECT_EQ(title_watcher.WaitAndGetTitle(), title_when_loaded);
  }

  // The closes the window and waits for the title of the original document to
  // change again to signal that the unload handler has run.
  {
    std::u16string title_when_done = u"unloaded";
    TitleWatcher title_watcher(shell()->web_contents(), title_when_done);
    EXPECT_TRUE(ExecJs(root, "w.close()"));
    EXPECT_EQ(title_watcher.WaitAndGetTitle(), title_when_done);
  }
}

// Regression test for https://crbug.com/960006.
//
// 1. Navigate to a1(a2(b3),c4),
// 2. b3 has a slow unload handler.
// 3. a2 navigates same process.
// 4. When the new document is loaded, a message is sent to c4 to check it
//    cannot see b3 anymore, even if b3 is still unloading.
IN_PROC_BROWSER_TEST_P(
    SitePerProcessBrowserTest,
    IsDetachedSubframeObservableDuringUnloadHandlerSameProcess) {
  GURL page_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a(b),c)"));
  EXPECT_TRUE(NavigateToURL(shell(), page_url));
  RenderFrameHostImpl* node1 =
      static_cast<WebContentsImpl*>(shell()->web_contents())
          ->GetPrimaryFrameTree()
          .root()
          ->current_frame_host();
  RenderFrameHostImpl* node2 = node1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* node3 = node2->child_at(0)->current_frame_host();
  RenderFrameHostImpl* node4 = node1->child_at(1)->current_frame_host();
  ASSERT_TRUE(ExecJs(node1, "window.name = 'node1'"));
  ASSERT_TRUE(ExecJs(node2, "window.name = 'node2'"));
  ASSERT_TRUE(ExecJs(node3, "window.name = 'node3'"));
  ASSERT_TRUE(ExecJs(node4, "window.name = 'node4'"));

  ASSERT_TRUE(ExecJs(node1, "window.node2 = window[0]"));
  ASSERT_TRUE(ExecJs(node1, "window.node3 = window[0][0]"));
  ASSERT_TRUE(ExecJs(node1, "window.node4 = window[1]"));

  // Test sanity check.
  EXPECT_EQ(true, EvalJs(node1, "!!window.node2"));
  EXPECT_EQ(true, EvalJs(node1, "!!window.node3"));
  EXPECT_EQ(true, EvalJs(node1, "!!window.node4"));

  // Simulate a long-running unload handler in |node3|.
  node3->DoNotDeleteForTesting();
  node2->DisableUnloadTimerForTesting();
  ASSERT_TRUE(ExecJs(node3, "window.onunload = ()=>{}"));

  // Prepare |node4| to respond to postMessage with a report of whether it can
  // still find |node3|.
  const char* kPostMessageHandlerScript = R"(
      window.postMessageGotData == false;
      window.postMessageCallback = function() {};
      function receiveMessage(event) {
          console.log('node4 - receiveMessage...');

          var can_node3_be_found = false;
          try {
            can_node3_be_found = !!top[0][0];  // top.node2.node3
          } catch(e) {
            can_node3_be_found = false;
          }

          window.postMessageGotData = true;
          window.postMessageData = can_node3_be_found;
          window.postMessageCallback(window.postMessageData);
      }
      window.addEventListener("message", receiveMessage, false);
  )";
  ASSERT_TRUE(ExecJs(node4, kPostMessageHandlerScript));

  // Make |node1| navigate |node2| same process and after the navigation
  // succeeds, send a post message to |node4|. We expect that the effects of the
  // commit should be visible to |node4| by the time it receives the posted
  // message.
  const char* kNavigationScript = R"(
      var node2_frame = document.getElementsByTagName('iframe')[0];
      node2_frame.onload = function() {
          console.log('node2_frame.onload ...');
          window.node4.postMessage('try to find node3', '*');
      };
      node2_frame.src = $1;
  )";
  GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
  ASSERT_TRUE(ExecJs(node1, JsReplace(kNavigationScript, url)));

  // Check if |node4| has seen |node3| even after |node2| navigation finished
  // (no other frame should see |node3| after the navigation of its parent).
  const char* kPostMessageResultsScript = R"(
      new Promise(function (resolve, reject) {
          if (window.postMessageGotData)
            resolve(window.postMessageData);
          else
            window.postMessageCallback = resolve;
      });
  )";
  EXPECT_EQ(false, EvalJs(node4, kPostMessageResultsScript));
}

// Regression test for https://crbug.com/960006.
//
// 1. Navigate to a1(a2(b3),c4),
// 2. b3 has a slow unload handler.
// 3. a2 navigates cross process.
// 4. When the new document is loaded, a message is sent to c4 to check it
//    cannot see b3 anymore, even if b3 is still unloading.
//
// Note: This test is the same as the above, except it uses a cross-process
// navigation at step 3.
IN_PROC_BROWSER_TEST_P(
    SitePerProcessBrowserTest,
    IsDetachedSubframeObservableDuringUnloadHandlerCrossProcess) {
  GURL page_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a(b),c)"));
  EXPECT_TRUE(NavigateToURL(shell(), page_url));
  RenderFrameHostImpl* node1 =
      static_cast<WebContentsImpl*>(shell()->web_contents())
          ->GetPrimaryFrameTree()
          .root()
          ->current_frame_host();
  RenderFrameHostImpl* node2 = node1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* node3 = node2->child_at(0)->current_frame_host();
  RenderFrameHostImpl* node4 = node1->child_at(1)->current_frame_host();
  ASSERT_TRUE(ExecJs(node1, "window.name = 'node1'"));
  ASSERT_TRUE(ExecJs(node2, "window.name = 'node2'"));
  ASSERT_TRUE(ExecJs(node3, "window.name = 'node3'"));
  ASSERT_TRUE(ExecJs(node4, "window.name = 'node4'"));

  ASSERT_TRUE(ExecJs(node1, "window.node2 = window[0]"));
  ASSERT_TRUE(ExecJs(node1, "window.node3 = window[0][0]"));
  ASSERT_TRUE(ExecJs(node1, "window.node4 = window[1]"));

  // Test sanity check.
  EXPECT_EQ(true, EvalJs(node1, "!!window.node2"));
  EXPECT_EQ(true, EvalJs(node1, "!!window.node3"));
  EXPECT_EQ(true, EvalJs(node1, "!!window.node4"));

  // Add a long-running unload handler to |node3|.
  node3->DoNotDeleteForTesting();
  node2->DisableUnloadTimerForTesting();
  ASSERT_TRUE(ExecJs(node3, "window.onunload = ()=>{}"));

  // Prepare |node4| to respond to postMessage with a report of whether it can
  // still find |node3|.
  const char* kPostMessageHandlerScript = R"(
      window.postMessageGotData == false;
      window.postMessageCallback = function() {};
      function receiveMessage(event) {
          console.log('node4 - receiveMessage...');

          var can_node3_be_found = false;
          try {
            can_node3_be_found = !!top[0][0];  // top.node2.node3
          } catch(e) {
            can_node3_be_found = false;
          }

          window.postMessageGotData = true;
          window.postMessageData = can_node3_be_found;
          window.postMessageCallback(window.postMessageData);
      }
      window.addEventListener("message", receiveMessage, false);
  )";
  ASSERT_TRUE(ExecJs(node4, kPostMessageHandlerScript));

  // Make |node1| navigate |node2| cross process and after the navigation
  // succeeds, send a post message to |node4|. We expect that the effects of the
  // commit should be visible to |node4| by the time it receives the posted
  // message.
  const char* kNavigationScript = R"(
      var node2_frame = document.getElementsByTagName('iframe')[0];
      node2_frame.onload = function() {
          console.log('node2_frame.onload ...');
          window.node4.postMessage('try to find node3', '*');
      };
      node2_frame.src = $1;
  )";
  GURL url = embedded_test_server()->GetURL("d.com", "/title1.html");
  ASSERT_TRUE(ExecJs(node1, JsReplace(kNavigationScript, url)));

  // Check if |node4| has seen |node3| even after |node2| navigation finished
  // (no other frame should see |node3| after the navigation of its parent).
  const char* kPostMessageResultsScript = R"(
      new Promise(function (resolve, reject) {
          if (window.postMessageGotData)
            resolve(window.postMessageData);
          else
            window.postMessageCallback = resolve;
      });
  )";
  EXPECT_EQ(false, EvalJs(node4, kPostMessageResultsScript));
}

// Regression test. https://crbug.com/963330
// 1. Start from A1(B2,C3)
// 2. B2 is the "focused frame", is deleted and starts unloading.
// 3. C3 commits a new navigation before B2 has completed its unload.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, FocusedFrameUnload) {
  // 1) Start from A1(B2,C3)
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(b,c)")));
  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* C3 = A1->child_at(1)->current_frame_host();
  FrameTree* frame_tree = A1->frame_tree();

  // 2.1) Make B2 to be the focused frame.
  EXPECT_EQ(A1->frame_tree_node(), frame_tree->GetFocusedFrame());
  EXPECT_TRUE(ExecJs(A1, "document.querySelector('iframe').focus()"));
  EXPECT_EQ(B2->frame_tree_node(), frame_tree->GetFocusedFrame());

  // 2.2 Unload B2. Drop detach message to simulate a long unloading.
  B2->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));

  EXPECT_FALSE(B2->GetSuddenTerminationDisablerState(
      blink::mojom::SuddenTerminationDisablerType::kUnloadHandler));
  B2->DoNotDeleteForTesting();
  EXPECT_TRUE(ExecJs(B2, "window.onunload = ()=>{};"));
  EXPECT_TRUE(B2->GetSuddenTerminationDisablerState(
      blink::mojom::SuddenTerminationDisablerType::kUnloadHandler));

  EXPECT_TRUE(B2->IsActive());
  EXPECT_TRUE(ExecJs(A1, "document.querySelector('iframe').remove()"));
  EXPECT_EQ(nullptr, frame_tree->GetFocusedFrame());
  EXPECT_EQ(2u, A1->child_count());
  EXPECT_TRUE(B2->IsPendingDeletion());

  // 3. C3 navigates.
  EXPECT_TRUE(NavigateToURLFromRenderer(
      C3->frame_tree_node(),
      embedded_test_server()->GetURL("d.com", "/title1.html")));
  EXPECT_TRUE(WaitForLoadStop(web_contents()));
  EXPECT_EQ(2u, A1->child_count());
}

// Test the unload timeout is effective.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, UnloadTimeout) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();

  // Simulate the iframe being slow to unload by dropping the
  // mojom::FrameHost::Detach API sent from B2 to the browser.
  EXPECT_TRUE(ExecJs(B2, "window.onunload = ()=>{};"));
  B2->DoNotDeleteForTesting();

  RenderFrameDeletedObserver delete_B2(B2);
  EXPECT_TRUE(ExecJs(A1, "document.querySelector('iframe').remove()"));
  delete_B2.WaitUntilDeleted();
}

// Test that an unloading child can PostMessage its cross-process parent.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       UnloadPostMessageToParentCrossProcess) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
  RenderFrameDeletedObserver delete_B2(B2);
  EXPECT_TRUE(ExecJs(B2, R"(
    window.addEventListener("unload", function() {
      window.parent.postMessage("B2 message", "*");
    });
  )"));
  EXPECT_TRUE(ExecJs(A1, R"(
    window.received_message = "nothing received";
    var received = false;
    window.addEventListener('message', function(event) {
      received_message = event.data;
    });
    document.querySelector('iframe').remove();
  )"));
  delete_B2.WaitUntilDeleted();
  // TODO(https://crbug.com/964950): PostMessage called from an unloading frame
  // must work. A1 must received 'B2 message'. This is not the case here.
  EXPECT_EQ("nothing received", EvalJs(A1, "received_message"));
}

// Test that an unloading child can PostMessage its same-process parent.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       UnloadPostMessageToParentSameProcess) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* A2 = A1->child_at(0)->current_frame_host();
  RenderFrameDeletedObserver delete_A1(A2);
  EXPECT_TRUE(ExecJs(A2, R"(
    window.addEventListener("unload", function() {
      window.parent.postMessage("A2 message", "*");
    });
  )"));
  EXPECT_TRUE(ExecJs(A1, R"(
    window.received_message = "nothing received";
    var received = false;
    window.addEventListener('message', function(event) {
      received_message = event.data;
    });
    document.querySelector('iframe').remove();
  )"));
  delete_A1.WaitUntilDeleted();
  EXPECT_EQ("A2 message", EvalJs(A1, "received_message"));
}

// Related to issue https://crbug.com/950625.
//
// 1. Start from A1(B1)
// 2. Navigate A1 to A3, same-process.
// 3. A1 requests the browser to detach B1, but this message is dropped.
// 4. The browser must be resilient and detach B1 when A3 commits.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       SameProcessNavigationResilientToDetachDropped) {
  // The test assumes the previous page gets deleted after navigation. Disable
  // back-forward cache to ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  GURL A1_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  GURL A3_url(embedded_test_server()->GetURL("a.com", "/title1.html"));

  EXPECT_TRUE(NavigateToURL(shell(), A1_url));
  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B1 = A1->child_at(0)->current_frame_host();

  B1->DoNotDeleteForTesting();
  RenderFrameDeletedObserver delete_B1(B1);
  shell()->LoadURL(A3_url);
  delete_B1.WaitUntilDeleted();
}

#if BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER)
// See crbug.com/1275848.
#define MAYBE_NestedSubframeWithUnloadHandler \
  DISABLED_NestedSubframeWithUnloadHandler
#else
#define MAYBE_NestedSubframeWithUnloadHandler NestedSubframeWithUnloadHandler
#endif
// After a same-origin iframe navigation, check that gradchild iframe are
// properly deleted and their unload handler executed.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       MAYBE_NestedSubframeWithUnloadHandler) {
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(b,c))");
  GURL iframe_new_url = embedded_test_server()->GetURL("b.com", "/title1.html");
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // In the document tree: A1(B2(B3,C4)) navigate B2 to B5.
  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* B3 = B2->child_at(0)->current_frame_host();
  RenderFrameHostImpl* C4 = B2->child_at(1)->current_frame_host();

  RenderFrameDeletedObserver delete_B2(B2);
  RenderFrameDeletedObserver delete_B3(B3);
  RenderFrameDeletedObserver delete_C4(C4);

  UnloadPrint(B2, "B2");
  UnloadPrint(B3, "B3");
  UnloadPrint(C4, "C4");

  // Navigate the iframe same-process.
  ExecuteScriptAsync(B2, JsReplace("location.href = $1", iframe_new_url));

  DOMMessageQueue dom_message_queue(
      WebContents::FromRenderFrameHost(web_contents()->GetPrimaryMainFrame()));

  // All the documents must be properly deleted:
  if (ShouldCreateNewHostForSameSiteSubframe())
    delete_B2.WaitUntilDeleted();
  delete_B3.WaitUntilDeleted();
  delete_C4.WaitUntilDeleted();

  // The unload handlers must have run:
  std::string message;
  std::vector<std::string> messages;
  for (int i = 0; i < 3; ++i) {
    EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
    base::TrimString(message, "\"", &message);
    messages.push_back(message);
  }
  EXPECT_FALSE(dom_message_queue.PopMessage(&message));
  EXPECT_THAT(messages, WhenSorted(ElementsAre("B2", "B3", "C4")));
}

// Some tests need an https server because third-party cookies are used, and
// SameSite=None cookies must be Secure. This is a separate fixture due to
// use the ContentMockCertVerifier.
class SitePerProcessSSLBrowserTest : public SitePerProcessBrowserTest {
 protected:
  SitePerProcessSSLBrowserTest() = default;
  net::EmbeddedTestServer* https_server() { return &https_server_; }

 private:
  void SetUpOnMainThread() override {
    SitePerProcessBrowserTest::SetUpOnMainThread();
    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
    https_server()->AddDefaultHandlers(GetTestDataFilePath());
    ASSERT_TRUE(https_server()->Start());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    SitePerProcessBrowserTest::SetUpCommandLine(command_line);
    mock_cert_verifier_.SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    SitePerProcessBrowserTest::SetUpInProcessBrowserTestFixture();
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    SitePerProcessBrowserTest::TearDownInProcessBrowserTestFixture();
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  content::ContentMockCertVerifier mock_cert_verifier_;
  net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};

// Unload handlers should be able to do things that might require for instance
// the RenderFrameHostImpl to stay alive.
// - use console.log (handled via RFHI::DidAddMessageToConsole).
// - use history.replaceState (handled via RFHI::OnUpdateState).
// - use document.cookie
// - use localStorage
//
// Test case:
//  1. Start on A1(B2). B2 has an unload handler.
//  2. Go to A3.
//  3. Go back to A4(B5).
//
// TODO(https://crbug.com/960976): history.replaceState is broken in OOPIFs.
//
// This test is similar to UnloadHandlersArePowerfulGrandChild, but with a
// different frame hierarchy.
IN_PROC_BROWSER_TEST_P(SitePerProcessSSLBrowserTest,
                       UnloadHandlersArePowerful) {
  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  DisableBackForwardCacheForTesting(
      web_contents(), content::BackForwardCache::TEST_USES_UNLOAD_EVENT);
  // Navigate to a page hosting a cross-origin frame.
  GURL url =
      https_server()->GetURL("a.com", "/cross_site_iframe_factory.html?a(b)");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();

  // Increase Unload timeout to prevent the previous document from
  // being deleleted before it has finished running B2 unload handler.
  A1->DisableUnloadTimerForTesting();
  B2->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));

  // Add an unload handler to the subframe and try in that handler to preserve
  // state that we will try to recover later.
  ASSERT_TRUE(ExecJs(B2, R"(
    window.addEventListener("unload", function() {
      // Waiting for 100ms, to give more time for browser-side things to go bad
      // and delete RenderFrameHostImpl prematurely.
      var start = (new Date()).getTime();
      do {
        curr = (new Date()).getTime();
      } while (start + 100 > curr);

      // Test that various RFHI-dependent things work fine in an unload handler.
      stateObj = { "history_test_key": "history_test_value" }
      history.replaceState(stateObj, 'title', window.location.href);
      console.log('console.log() sent');

      // As a sanity check, test that RFHI-independent things also work fine.
      localStorage.localstorage_test_key = 'localstorage_test_value';
      document.cookie = 'cookie_test_key=' +
                        'cookie_test_value; SameSite=none; Secure';
    });
  )"));

  // Navigate A1(B2) to A3.
  {
    // Prepare observers.
    WebContentsConsoleObserver console_observer(web_contents());
    console_observer.SetPattern("console.log() sent");
    RenderFrameDeletedObserver B2_deleted(B2);

    // Navigate
    GURL away_url(https_server()->GetURL("a.com", "/title1.html"));
    ASSERT_TRUE(ExecJs(A1, JsReplace("location = $1", away_url)));

    // Observers must be reached.
    B2_deleted.WaitUntilDeleted();
    ASSERT_TRUE(console_observer.Wait());

    EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
    EXPECT_EQ(away_url, web_contents()->GetLastCommittedURL());
  }

  // Navigate back from A3 to A4(B5).
  web_contents()->GetController().GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // Temporary extra expectations to investigate:
  // https://bugs.chromium.org/p/chromium/issues/detail?id=1215493
  EXPECT_EQ(url, web_contents()->GetLastCommittedURL());
  EXPECT_EQ(
      2u, CollectAllRenderFrameHosts(web_contents()->GetPrimaryPage()).size());

  RenderFrameHostImpl* A4 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B5 = A4->child_at(0)->current_frame_host();

  // Verify that we can recover the data that should have been persisted by the
  // unload handler.
  EXPECT_EQ("localstorage_test_value",
            EvalJs(B5, "localStorage.localstorage_test_key"));
  EXPECT_EQ("cookie_test_key=cookie_test_value", EvalJs(B5, "document.cookie"));

  // TODO(lukasza): https://crbug.com/960976: Make the verification below
  // unconditional, once the bug is fixed.
  if (!AreAllSitesIsolatedForTesting()) {
    EXPECT_EQ("history_test_value",
              EvalJs(B5, "history.state.history_test_key"));
  }
}

// Unload handlers should be able to do things that might require for instance
// the RenderFrameHostImpl to stay alive.
// - use console.log (handled via RFHI::DidAddMessageToConsole).
// - use history.replaceState (handled via RFHI::OnUpdateState).
// - use document.cookie
// - use localStorage
//
// Test case:
//  1. Start on A1(B2(C3)). C3 has an unload handler.
//  2. Go to A4.
//  3. Go back to A5(B6(C7)).
//
// TODO(https://crbug.com/960976): history.replaceState is broken in OOPIFs.
//
// This test is similar to UnloadHandlersArePowerful, but with a different frame
// hierarchy.
IN_PROC_BROWSER_TEST_P(SitePerProcessSSLBrowserTest,
                       UnloadHandlersArePowerfulGrandChild) {
  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  DisableBackForwardCacheForTesting(
      web_contents(), content::BackForwardCache::TEST_USES_UNLOAD_EVENT);
  // Navigate to a page hosting a cross-origin frame.
  GURL url = https_server()->GetURL("a.com",
                                    "/cross_site_iframe_factory.html?a(b(c))");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderFrameHostImpl* A1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
  RenderFrameHostImpl* C3 = B2->child_at(0)->current_frame_host();

  // Increase Unload timeout to prevent the previous document from
  // being deleleted before it has finished running C3 unload handler.
  A1->DisableUnloadTimerForTesting();
  B2->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));
  C3->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));

  // Add an unload handler to the subframe and try in that handler to preserve
  // state that we will try to recover later.
  ASSERT_TRUE(ExecJs(C3, R"(
    window.addEventListener("unload", function() {
      // Waiting for 100ms, to give more time for browser-side things to go bad
      // and delete RenderFrameHostImpl prematurely.
      var start = (new Date()).getTime();
      do {
        curr = (new Date()).getTime();
      } while (start + 100 > curr);

      // Test that various RFHI-dependent things work fine in an unload handler.
      stateObj = { "history_test_key": "history_test_value" }
      history.replaceState(stateObj, 'title', window.location.href);
      console.log('console.log() sent');

      // As a sanity check, test that RFHI-independent things also work fine.
      localStorage.localstorage_test_key = 'localstorage_test_value';
      document.cookie = 'cookie_test_key=' +
                        'cookie_test_value; SameSite=none; Secure';
    });
  )"));

  // Navigate A1(B2(C3) to A4.
  {
    // Prepare observers.
    WebContentsConsoleObserver console_observer(web_contents());
    console_observer.SetPattern("console.log() sent");
    RenderFrameDeletedObserver B2_deleted(B2);
    RenderFrameDeletedObserver C3_deleted(C3);

    // Navigate
    GURL away_url(https_server()->GetURL("a.com", "/title1.html"));
    ASSERT_TRUE(ExecJs(A1, JsReplace("location = $1", away_url)));

    // Observers must be reached.
    B2_deleted.WaitUntilDeleted();
    C3_deleted.WaitUntilDeleted();
    ASSERT_TRUE(console_observer.Wait());

    EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
    EXPECT_EQ(away_url, web_contents()->GetLastCommittedURL());
  }

  // Navigate back from A4 to A5(B6(C7))
  web_contents()->GetController().GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // Temporary extra expectations to investigate:
  // https://bugs.chromium.org/p/chromium/issues/detail?id=1215493
  EXPECT_EQ(url, web_contents()->GetLastCommittedURL());
  EXPECT_EQ(
      3u, CollectAllRenderFrameHosts(web_contents()->GetPrimaryPage()).size());

  RenderFrameHostImpl* A5 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* B6 = A5->child_at(0)->current_frame_host();
  RenderFrameHostImpl* C7 = B6->child_at(0)->current_frame_host();

  // Verify that we can recover the data that should have been persisted by the
  // unload handler.
  EXPECT_EQ("localstorage_test_value",
            EvalJs(C7, "localStorage.localstorage_test_key"));
  EXPECT_EQ("cookie_test_key=cookie_test_value", EvalJs(C7, "document.cookie"));

  // TODO(lukasza): https://crbug.com/960976: Make the verification below
  // unconditional, once the bug is fixed.
  if (!AreAllSitesIsolatedForTesting()) {
    EXPECT_EQ("history_test_value",
              EvalJs(C7, "history.state.history_test_key"));
  }
}

// Execute an unload handler from the initial empty document.
//
// Start from A1(B2(B3)).
// B3 is the initial empty document created by B2. An unload handler is added to
// B3. A1 deletes B2.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
                       UnloadInInitialEmptyDocument) {
  // 1. Start from A1(B2).
  GURL url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)");
  EXPECT_TRUE(NavigateToURL(shell(), url));
  RenderFrameHostImpl* a1 = web_contents()->GetPrimaryMainFrame();
  RenderFrameHostImpl* b2 = a1->child_at(0)->current_frame_host();

  // 2. Create a new frame without navigating it. It stays on the initial empty
  //    document B3. Current state is with A1(B2(B3)).
  ASSERT_EQ(0u, b2->child_count());
  EXPECT_TRUE(ExecJs(b2, R"(
    let iframe = document.createElement("iframe");
    document.body.appendChild(iframe);
    iframe.contentWindow.onunload = () => {
      window.domAutomationController.send("B3 unloaded");
    }
  )"));
  ASSERT_EQ(1u, b2->child_count());
  RenderFrameHostImpl* b3 = b2->child_at(0)->current_frame_host();

  auto has_unload_handler = [](RenderFrameHostImpl* rfh) {
    return rfh->GetSuddenTerminationDisablerState(
        blink::mojom::SuddenTerminationDisablerType::kUnloadHandler);
  };
  EXPECT_FALSE(has_unload_handler(a1));
  EXPECT_FALSE(has_unload_handler(b2));
  EXPECT_TRUE(has_unload_handler(b3));

  // 3. A1 deletes B2. This triggers the unload handler from B3.
  DOMMessageQueue dom_message_queue(
      WebContents::FromRenderFrameHost(web_contents()->GetPrimaryMainFrame()));
  ExecuteScriptAsync(a1, "document.querySelector('iframe').remove();");

  // Check the unload handler is executed.
  std::string message;
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
  EXPECT_EQ("\"B3 unloaded\"", message);
}

INSTANTIATE_TEST_SUITE_P(All,
                         SitePerProcessSSLBrowserTest,
                         testing::ValuesIn(RenderDocumentFeatureLevelValues()));
}  // namespace content