0
Files
src/content/browser/site_per_process_unload_browsertest.cc
Peter Kasting ff5db66924 Change std::find() to use base:: functions: content/
Simplifies code slightly.

Bug: 1368812
Change-Id: Iecb3c9ce7d8da507ef4bc6cfce8233385c3ba5bb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3926100
Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
Auto-Submit: Peter Kasting <pkasting@chromium.org>
Commit-Queue: Dmitry Gozman <dgozman@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1055935}
2022-10-06 18:57:24 +00:00

1719 lines
71 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/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/threading/thread_task_runner_handle.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::ThreadTaskRunnerHandle::Get()->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::ThreadTaskRunnerHandle::Get()->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();
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();
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