blob: 0923eb7f1c9a066161e4438c83a44a6babe98ee8 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// 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 <algorithm>
#include <list>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/scoped_observer.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.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 "components/network_session_configurator/common/network_switches.h"
#include "content/browser/frame_host/cross_process_frame_connector.h"
#include "content/browser/frame_host/frame_tree.h"
#include "content/browser/frame_host/navigation_controller_impl.h"
#include "content/browser/frame_host/navigator.h"
#include "content/browser/frame_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/frame_messages.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.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(FrameTreeNode* node, const char* message) {
EXPECT_TRUE(
ExecJs(node, 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_F(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())
->GetFrameTree()
->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(ExecuteScript(
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(ExecuteScript(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_F(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())
->GetFrameTree()
->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(
ExecuteScript(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(ExecuteScript(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_F(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()->GetFrameTree()->root()->child_at(0);
GURL b_url(embedded_test_server()->GetURL(
"b.com", "/render_frame_host/beforeunload.html"));
NavigateFrameToURL(child_node, b_url);
FrameConnectorDelegate* 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 FrameConnectorDelegate. 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(ExecuteScript(
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.
ASSERT_TRUE(ExecuteScript(
web_contents(), "document.querySelector('iframe').src = 'about:blank';"));
WaitForAppModalDialog(shell());
// 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(ExecuteScript(
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_F(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())
->GetFrameTree()
->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"));
EXPECT_TRUE(OpenPopup(shell()->web_contents(), b_url, "popup"));
// 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(
ExecuteScript(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"));
DOMMessageQueue msg_queue;
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.GetOrigin().spec());
}
// Verify that when the last active frame in a process is going away as part of
// OnSwapOut, the SwapOut ACK 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_F(SitePerProcessBrowserTest,
SwapOutACKArrivesPriorToProcessShutdownRequest) {
GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
RenderFrameHostImpl* rfh = web_contents()->GetMainFrame();
rfh->DisableSwapOutTimerForTesting();
// 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 SwapOut ACK was received (i.e., that we
// didn't just simulate OnSwappedOut() due to the process erroneously going
// away before the SwapOut ACK was received, as in https://crbug.com/867274).
RenderProcessHostWatcher watcher(
rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
auto swapout_ack_filter = base::MakeRefCounted<ObserveMessageFilter>(
FrameMsgStart, FrameHostMsg_SwapOut_ACK::ID);
rfh->GetProcess()->AddFilter(swapout_ack_filter.get());
GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), cross_site_url));
watcher.Wait();
EXPECT_TRUE(swapout_ack_filter->has_received_message());
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_F(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->GetMainFrame()->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->GetMainFrame()->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->GetMainFrame()->DisableBeforeUnloadHangMonitorForTesting();
web_contents->GetMainFrame()->ExecuteJavaScriptWithUserGestureForTests(
base::string16());
// 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::TimeDelta::FromMilliseconds(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 defined(OS_LINUX) || defined(OS_MACOSX)
#define MAYBE_UnloadHandlerSubframes DISABLED_UnloadHandlerSubframes
#else
#define MAYBE_UnloadHandlerSubframes UnloadHandlerSubframes
#endif
IN_PROC_BROWSER_TEST_F(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()->GetFrameTree()->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()->GetMainFrame()));
// Disable the swap out timer on B1.
root->child_at(0)->current_frame_host()->DisableSwapOutTimerForTesting();
// 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 = std::find(messages.begin(), messages.end(), "B1");
auto B2 = std::find(messages.begin(), messages.end(), "B2");
EXPECT_LT(B1, B2);
// In process C:
auto C2 = std::find(messages.begin(), messages.end(), "C2");
auto C3 = std::find(messages.begin(), messages.end(), "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_F(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.
auto filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
RenderFrameHost* rfh_b =
web_contents()->GetFrameTree()->root()->child_at(0)->current_frame_host();
rfh_b->GetProcess()->AddFilter(filter.get());
// 3) Navigate and check the old frame is deleted after some time.
FrameTreeNode* root = web_contents()->GetFrameTree()->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_F(SitePerProcessBrowserTest, Unload_ABAB) {
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()->GetFrameTree()->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()->DisableSwapOutTimerForTesting();
DOMMessageQueue dom_message_queue(
WebContents::FromRenderFrameHost(web_contents()->GetMainFrame()));
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 = std::find(messages.begin(), messages.end(), "A1");
auto A2 = std::find(messages.begin(), messages.end(), "A2");
auto B1 = std::find(messages.begin(), messages.end(), "B1");
auto B2 = std::find(messages.begin(), messages.end(), "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_F(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()->GetMainFrame();
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::UnloadState::NotRun, rfh_a->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::NotRun, rfh_b->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::NotRun, rfh_c->unload_state_);
// Act as if there was a slow unload handler on rfh_b and rfh_c.
// The navigating frames are waiting for FrameHostMsg_SwapoutACK.
auto swapout_ack_filter_b = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_SwapOut_ACK::ID);
auto swapout_ack_filter_c = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_SwapOut_ACK::ID);
rfh_b->GetProcess()->AddFilter(swapout_ack_filter_b.get());
rfh_c->GetProcess()->AddFilter(swapout_ack_filter_c.get());
EXPECT_TRUE(ExecuteScript(rfh_b->frame_tree_node(), onunload_script));
EXPECT_TRUE(ExecuteScript(rfh_c->frame_tree_node(), onunload_script));
rfh_b->DisableSwapOutTimerForTesting();
rfh_c->DisableSwapOutTimerForTesting();
RenderFrameDeletedObserver delete_b(rfh_b), delete_c(rfh_c);
// 2) Navigate rfh_c to D.
NavigateFrameToURL(rfh_c->frame_tree_node(), url_d);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::NotRun, rfh_a->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::NotRun, rfh_b->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, rfh_c->unload_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::TimeDelta::FromSeconds(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 FrameHostMsg_Detach.
auto detach_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
rfh_d->GetProcess()->AddFilter(detach_filter.get());
EXPECT_TRUE(ExecuteScript(rfh_d->frame_tree_node(), onunload_script));
// 3) Navigate rfh_b to E.
NavigateFrameToURL(rfh_b->frame_tree_node(), url_e);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::NotRun, rfh_a->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, rfh_b->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, rfh_c->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, rfh_d->unload_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->OnDetach();
EXPECT_TRUE(delete_c.deleted());
EXPECT_TRUE(delete_d.deleted());
// rfh_b completes its unload event.
EXPECT_FALSE(delete_b.deleted());
rfh_b->OnSwapOutACK();
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 receives FrameHostMsg_OnDetach before A2, it should not destroy itself
// and its children, but rather wait for A2.
IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, PartialUnloadHandler) {
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()->GetFrameTree()->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 Swapout ACK. They will be called manually.
auto swapout_ack_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_SwapOut_ACK::ID);
auto detach_filter_a = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
auto detach_filter_b = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
a1->GetProcess()->AddFilter(swapout_ack_filter.get());
a1->GetProcess()->AddFilter(detach_filter_a.get());
b1->GetProcess()->AddFilter(detach_filter_b.get());
a1->DisableSwapOutTimerForTesting();
// Set an arbitrarily long timeout to ensure the subframe unload timer doesn't
// fire before we call OnDetach().
b1->SetSubframeUnloadTimeoutForTesting(base::TimeDelta::FromSeconds(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()->GetMainFrame()));
// 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::UnloadState::InProgress, a1->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::Completed, b1->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, a2->unload_state_);
// 3) B1 receives confirmation it has been deleted. This has no effect,
// because it is still waiting on A2 to be deleted.
b1->OnDetach();
EXPECT_FALSE(delete_a1.deleted());
EXPECT_FALSE(delete_b1.deleted());
EXPECT_FALSE(delete_a2.deleted());
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, a1->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::Completed, b1->unload_state_);
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, a2->unload_state_);
// 4) A2 received confirmation that it has been deleted and destroy B1 and A2.
a2->OnDetach();
EXPECT_FALSE(delete_a1.deleted());
EXPECT_TRUE(delete_b1.deleted());
EXPECT_TRUE(delete_a2.deleted());
EXPECT_EQ(RenderFrameHostImpl::UnloadState::InProgress, a1->unload_state_);
// 5) A1 receives SwapOutACK and deletes itself.
a1->OnSwapOutACK();
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_F(SitePerProcessBrowserTest,
PendingDeletionCheckCompletedOnSubtree) {
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()->GetFrameTree()->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 Swapout ACK.
auto swapout_ack_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_SwapOut_ACK::ID);
auto detach_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
rfh_0->GetProcess()->AddFilter(swapout_ack_filter.get());
rfh_0->GetProcess()->AddFilter(detach_filter.get());
rfh_0->DisableSwapOutTimerForTesting();
// 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_F(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()->GetFrameTree()->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();
}
// 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_F(SitePerProcessBrowserTest,
DetachedIframeUnloadHandlerABCB) {
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()->GetFrameTree()->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_F(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()->GetFrameTree()->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_F(SitePerProcessBrowserTest,
RendererInitiatedWindowCloseWithUnload) {
GURL main_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = web_contents()->GetFrameTree()->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(
ExecuteScript(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.
{
base::string16 title_when_loaded = base::UTF8ToUTF16("loaded");
TitleWatcher title_watcher(shell()->web_contents(), title_when_loaded);
EXPECT_TRUE(
ExecuteScript(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.
{
base::string16 title_when_done = base::UTF8ToUTF16("unloaded");
TitleWatcher title_watcher(shell()->web_contents(), title_when_done);
EXPECT_TRUE(ExecuteScript(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_F(
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())
->GetFrameTree()
->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'"));
// Test sanity check.
EXPECT_EQ(true, EvalJs(node1, "!!top.node2.node3"));
EXPECT_EQ(true, EvalJs(node2, "!!top.node2.node3"));
EXPECT_EQ(true, EvalJs(node3, "!!top.node2.node3"));
EXPECT_EQ(true, EvalJs(node4, "!!top.node2.node3"));
// Simulate a long-running unload handler in |node3|.
auto detach_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
node3->GetProcess()->AddFilter(detach_filter.get());
node2->DisableSwapOutTimerForTesting();
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.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 ...');
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_F(
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())
->GetFrameTree()
->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'"));
// Test sanity check.
EXPECT_EQ(true, EvalJs(node1, "!!top.node2.node3"));
EXPECT_EQ(true, EvalJs(node2, "!!top.node2.node3"));
EXPECT_EQ(true, EvalJs(node3, "!!top.node2.node3"));
EXPECT_EQ(true, EvalJs(node4, "!!top.node2.node3"));
// Add a long-running unload handler to |node3|.
auto detach_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
node3->GetProcess()->AddFilter(detach_filter.get());
node2->DisableSwapOutTimerForTesting();
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.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 ...');
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_F(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()->GetMainFrame();
RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
RenderFrameHostImpl* C3 = A1->child_at(1)->current_frame_host();
FrameTree* frame_tree = A1->frame_tree_node()->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.
auto filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
B2->GetProcess()->AddFilter(filter.get());
B2->SetSubframeUnloadTimeoutForTesting(base::TimeDelta::FromSeconds(30));
EXPECT_FALSE(B2->GetSuddenTerminationDisablerState(
blink::mojom::SuddenTerminationDisablerType::kUnloadHandler));
EXPECT_TRUE(ExecJs(B2, "window.onunload = ()=>{};"));
EXPECT_TRUE(B2->GetSuddenTerminationDisablerState(
blink::mojom::SuddenTerminationDisablerType::kUnloadHandler));
EXPECT_TRUE(B2->is_active());
EXPECT_TRUE(ExecJs(A1, "document.querySelector('iframe').remove()"));
EXPECT_EQ(nullptr, frame_tree->GetFocusedFrame());
EXPECT_EQ(2u, A1->child_count());
EXPECT_FALSE(B2->is_active());
// 3. C3 navigates.
NavigateFrameToURL(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_F(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()->GetMainFrame();
RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
// Simulate the iframe being slow to unload by dropping the
// FrameHostMsg_Detach message sent from B2 to the browser.
EXPECT_TRUE(ExecJs(B2, "window.onunload = ()=>{};"));
auto detach_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
B2->GetProcess()->AddFilter(detach_filter.get());
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_F(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()->GetMainFrame();
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_F(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()->GetMainFrame();
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_F(SitePerProcessBrowserTest,
SameProcessNavigationResilientToDetachDropped) {
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()->GetMainFrame();
RenderFrameHostImpl* B1 = A1->child_at(0)->current_frame_host();
auto detach_filter = base::MakeRefCounted<DropMessageFilter>(
FrameMsgStart, FrameHostMsg_Detach::ID);
A1->GetProcess()->AddFilter(detach_filter.get());
RenderFrameDeletedObserver delete_B1(B1);
shell()->LoadURL(A3_url);
delete_B1.WaitUntilDeleted();
}
// 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
// kIgnoreCertificateErrors flag.
class SitePerProcessSSLBrowserTest : public SitePerProcessBrowserTest {
protected:
SitePerProcessSSLBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
void SetUpCommandLine(base::CommandLine* command_line) override {
SitePerProcessBrowserTest::SetUpCommandLine(command_line);
// This is necessary to use https with arbitrary hostnames.
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUpOnMainThread() override {
https_server()->AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(https_server()->Start());
SitePerProcessBrowserTest::SetUpOnMainThread();
}
net::EmbeddedTestServer* https_server() { return &https_server_; }
private:
net::EmbeddedTestServer https_server_;
};
// 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_F(SitePerProcessSSLBrowserTest,
UnloadHandlersArePowerful) {
// 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()->GetMainFrame();
RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
// Increase SwapOut/Unload timeout to prevent the previous document from
// being deleleted before it has finished running B2 unload handler.
A1->DisableSwapOutTimerForTesting();
B2->SetSubframeUnloadTimeoutForTesting(base::TimeDelta::FromSeconds(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.
ConsoleObserverDelegate console(web_contents(), "console.log() sent");
web_contents()->SetDelegate(&console);
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.Wait();
}
// Navigate back from A3 to A4(B5).
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* A4 = web_contents()->GetMainFrame();
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_F(SitePerProcessSSLBrowserTest,
UnloadHandlersArePowerfulGrandChild) {
// 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()->GetMainFrame();
RenderFrameHostImpl* B2 = A1->child_at(0)->current_frame_host();
RenderFrameHostImpl* C3 = B2->child_at(0)->current_frame_host();
// Increase SwapOut/Unload timeout to prevent the previous document from
// being deleleted before it has finished running C3 unload handler.
A1->DisableSwapOutTimerForTesting();
B2->SetSubframeUnloadTimeoutForTesting(base::TimeDelta::FromSeconds(30));
C3->SetSubframeUnloadTimeoutForTesting(base::TimeDelta::FromSeconds(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.
ConsoleObserverDelegate console(web_contents(), "console.log() sent");
web_contents()->SetDelegate(&console);
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.Wait();
}
// Navigate back from A4 to A5(B6(C7))
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* A5 = web_contents()->GetMainFrame();
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"));
}
}
} // namespace content