0
Files
src/content/browser/site_per_process_unload_browsertest.cc
Peter Kasting 1557e5fbe4 [cleanup] Replace base::ranges with std::ranges: content/
Done entirely with `git grep` and `sed` + `git cl format`, no
hand-editing.

Bug: 386918226
Change-Id: I8561612cb02c9d62c455836dba414ab402e40694
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6199140
Auto-Submit: Peter Kasting <pkasting@chromium.org>
Commit-Queue: Peter Kasting <pkasting@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Owners-Override: Peter Kasting <pkasting@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1412009}
2025-01-27 17:14:08 -08:00

1778 lines
74 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 <algorithm>
#include <list>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_timeouts.h"
#include "base/test/with_feature_override.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/fenced_frame/fenced_frame_url_mapping.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/site_per_process_browsertest.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/isolated_world_ids.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 AddPagehideHandler(const ToRenderFrameHost& target, const char* message) {
EXPECT_TRUE(
ExecJs(target, JsReplace("window.onpagehide = function() { "
" window.domAutomationController.send($1);"
"}",
message)));
}
} // namespace
// Tests that there are no crashes if a subframe is detached in its pagehide
// handler. See https://crbug.com/590054.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, DetachInPagehideHandler) {
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 a pagehide 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.onpagehide=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 pagehide handler doesn't crash the
// browser.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, NavigateInPagehideHandler) {
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 a pagehide handler to B's subframe.
EXPECT_TRUE(ExecJs(root->child_at(0)->child_at(0)->current_frame_host(),
"window.onpagehide=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 pagehide 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';"));
EXPECT_TRUE(base::test::RunUntil(
[&]() { return frame_connector_delegate->IsHidden(); }));
// 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';"));
EXPECT_TRUE(base::test::RunUntil(
[&]() { return !frame_connector_delegate->IsHidden(); }));
}
// Ensure that after a main frame with an OOPIF is navigated cross-site, the
// pagehide 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,
ParentOriginDoesNotChangeInPagehideHandler) {
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 pagehide 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 a pagehide 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 pagehide handler, so to ensure that the message can be received, send
// it through the popup.
EXPECT_TRUE(ExecJs(root->child_at(0),
"window.onpagehide = 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 pagehide 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(), ISOLATED_WORLD_ID_GLOBAL);
// 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 pagehide 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
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, PagehideHandlerSubframes) {
// The test expects the previous document to be deleted on navigation.
DisableBackForwardCacheForTesting(
web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
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 pagehide handler to every frames. It notifies the browser using the
// DomAutomationController it has been executed.
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
AddPagehideHandler(root, "A1");
AddPagehideHandler(root->child_at(0), "B1");
AddPagehideHandler(root->child_at(0)->child_at(0), "C1");
AddPagehideHandler(root->child_at(0)->child_at(1), "C2");
AddPagehideHandler(root->child_at(0)->child_at(0)->child_at(0), "B2");
AddPagehideHandler(root->child_at(0)->child_at(1)->child_at(0), "A2");
AddPagehideHandler(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 pagehide 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 pagehide
// 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::ranges::find(messages, "B1");
auto B2 = std::ranges::find(messages, "B2");
EXPECT_LT(B1, B2);
// In process C:
auto C2 = std::ranges::find(messages, "C2");
auto C3 = std::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 pagehide handler are executed,
// executed in the right order and the processes for A and B are removed.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, PagehideHandlerABAB) {
// The test expects the previous document to be deleted on navigation.
DisableBackForwardCacheForTesting(
web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
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 pagehide handler on every frame.
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
AddPagehideHandler(root, "A1");
AddPagehideHandler(root->child_at(0), "B1");
AddPagehideHandler(root->child_at(0)->child_at(0), "A2");
AddPagehideHandler(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 pagehide 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::ranges::find(messages, "A1");
auto A2 = std::ranges::find(messages, "A2");
auto B1 = std::ranges::find(messages, "B1");
auto B2 = std::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 a pagehide 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, PartialPagehideHandler) {
// The test expects the previous document to be deleted on navigation.
DisableBackForwardCacheForTesting(
web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
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 pagehide handler on A2, but not on the other frames.
AddPagehideHandler(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 pagehide 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 pagehide handler may be
// eligible for immediate deletion. Several configurations are tested:
//
// Before navigation commit
//
// 0 | N : No pagehide handler
// | [N] : Pagehide handler
// | | | | | | | |
// [1] 2 [3] 5 7 9 12 |
// | | | / \ / \ |
// 4 [6] 8 10 11 13 [14] |
//
// After navigation commit (expected)
//
// 0 | N : No pagehide handler
// --------------------- | [N] : Pagehide handler
// | | | | |
// [1] [3] 5 12 |
// | \ |
// [6] [14] |
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
PendingDeletionCheckCompletedOnSubtree) {
// The test expects the previous document to be deleted on navigation.
DisableBackForwardCacheForTesting(
web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
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 pagehide handlers.
AddPagehideHandler(rfh_1->frame_tree_node(), "");
AddPagehideHandler(rfh_3->frame_tree_node(), "");
AddPagehideHandler(rfh_6->frame_tree_node(), "");
AddPagehideHandler(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 pagehide handlers execute in all of
// its child frames. Start from A(B(C)) and delete B from A.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DetachedIframePagehideHandlerABC) {
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 pagehide handlers on B and C.
AddPagehideHandler(rfh_b->frame_tree_node(), "B");
AddPagehideHandler(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 pagehide 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) || \
!defined(NDEBUG) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_ANDROID)
// Too slow under sanitizers and debug builds, even with increased timeout:
// https://crbug.com/1096612
// Disabled for Linux/Android due to failures: https://crbug.com/1494811
#define MAYBE_DetachedIframePagehideHandlerABCB \
DISABLED_DetachedIframePagehideHandlerABCB
#else
#define MAYBE_DetachedIframePagehideHandlerABCB \
DetachedIframePagehideHandlerABCB
#endif
// When an iframe is detached, check that pagehide 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_DetachedIframePagehideHandlerABCB) {
// This test takes longer to run, because multiple processes are waiting on
// each other's documents to execute pagehide 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();
RenderFrameHostImplWrapper rfh_a(root->current_frame_host());
RenderFrameHostImplWrapper rfh_b1(rfh_a->child_at(0)->current_frame_host());
RenderFrameHostImplWrapper rfh_c(rfh_b1->child_at(0)->current_frame_host());
RenderFrameHostImplWrapper rfh_b2(rfh_c->child_at(0)->current_frame_host());
// 2) Add pagehide handlers on B1, B2 and C.
AddPagehideHandler(rfh_b1->frame_tree_node(), "B1");
rfh_b1->DisableUnloadTimerForTesting();
AddPagehideHandler(rfh_b2->frame_tree_node(), "B2");
rfh_b2->DisableUnloadTimerForTesting();
AddPagehideHandler(rfh_c->frame_tree_node(), "C");
rfh_c->DisableUnloadTimerForTesting();
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 pagehide 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 pagehide handlers execute in all of
// its child frames. Start from A1(A2(B)), delete A2 from itself.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DetachedIframePagehideHandlerAAB) {
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 pagehide handlers on A2 ad B.
AddPagehideHandler(rfh_a2->frame_tree_node(), "A2");
AddPagehideHandler(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 pagehide 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 pagehide handler inside teardown of the
// RenderWidget (inside WidgetMsg_Close) can succeed.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
RendererInitiatedWindowCloseWithPagehide) {
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 pagehide 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 pagehide handlers are run early, before the actual teardown of
// the closing RenderWidget.
GURL open_url = embedded_test_server()->GetURL(
"a.com", "/pagehide_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 pagehide 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 pagehide 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(crbug.com/41459857): 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("pagehide", 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.
// TODO(crbug.com/40914915): Fix flakes and re-enable test.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
DISABLED_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_NestedSubframeWithPagehideHandler \
DISABLED_NestedSubframeWithPagehideHandler
#else
#define MAYBE_NestedSubframeWithPagehideHandler \
NestedSubframeWithPagehideHandler
#endif
// After a same-origin iframe navigation, check that grandchild iframe are
// properly deleted and their pagehide handler executed.
IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest,
MAYBE_NestedSubframeWithPagehideHandler) {
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);
AddPagehideHandler(B2, "B2");
AddPagehideHandler(B3, "B3");
AddPagehideHandler(C4, "C4");
// Navigate the iframe same-process.
bool will_delete_b2 = B2->ShouldChangeRenderFrameHostOnSameSiteNavigation();
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 (will_delete_b2) {
delete_B2.WaitUntilDeleted();
}
delete_B3.WaitUntilDeleted();
delete_C4.WaitUntilDeleted();
// The pagehide 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};
};
// Pagehide 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 a pagehide handler.
// 2. Go to A3.
// 3. Go back to A4(B5).
//
// TODO(crbug.com/41457585): history.replaceState is broken in OOPIFs.
//
// This test is similar to PagehideHandlersArePowerfulGrandChild, but with a
// different frame hierarchy.
//
// TODO(crbug.com/40283595): investigate test flakes and re-enable test.
IN_PROC_BROWSER_TEST_P(SitePerProcessSSLBrowserTest,
DISABLED_PagehideHandlersArePowerful) {
// The test expects the previous document to be deleted on navigation.
DisableBackForwardCacheForTesting(
web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
// 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 deleted before it has finished running B2 pagehide handler.
A1->DisableUnloadTimerForTesting();
B2->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));
// Add a pagehide 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("pagehide", function() {
// Waiting for 100ms, to give more time for browser-side things to go bad
// and delete RenderFrameHostImpl prematurely.
var start = (new Date()).getTime();
do {
curr = (new Date()).getTime();
} while (start + 100 > curr);
// Test that various RFHI-dependent things work fine in an unload handler.
stateObj = { "history_test_key": "history_test_value" }
history.replaceState(stateObj, 'title', window.location.href);
console.log('console.log() sent');
// As a sanity check, test that RFHI-independent things also work fine.
localStorage.localstorage_test_key = 'localstorage_test_value';
document.cookie = 'cookie_test_key=' +
'cookie_test_value; SameSite=none; Secure';
});
)"));
// Navigate A1(B2) to A3.
{
// Prepare observers.
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern("console.log() sent");
RenderFrameDeletedObserver B2_deleted(B2);
// Navigate
GURL away_url(https_server()->GetURL("a.com", "/title1.html"));
ASSERT_TRUE(ExecJs(A1, JsReplace("location = $1", away_url)));
// Observers must be reached.
B2_deleted.WaitUntilDeleted();
ASSERT_TRUE(console_observer.Wait());
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(away_url, web_contents()->GetLastCommittedURL());
}
// Navigate back from A3 to A4(B5).
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Temporary extra expectations to investigate:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1215493
EXPECT_EQ(url, web_contents()->GetLastCommittedURL());
EXPECT_EQ(
2u, CollectAllRenderFrameHosts(web_contents()->GetPrimaryPage()).size());
RenderFrameHostImpl* A4 = web_contents()->GetPrimaryMainFrame();
RenderFrameHostImpl* B5 = A4->child_at(0)->current_frame_host();
// Verify that we can recover the data that should have been persisted by the
// pagehide 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"));
}
}
// Pagehide 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(crbug.com/41457585): history.replaceState is broken in OOPIFs.
//
// This test is similar to PagehideHandlersArePowerful, but with a different
// frame hierarchy.
//
// TODO(crbug.com/40283595): investigate test flakes and re-enable test.
IN_PROC_BROWSER_TEST_P(SitePerProcessSSLBrowserTest,
DISABLED_PagehideHandlersArePowerfulGrandChild) {
// The test expects the previous document to be deleted on navigation.
DisableBackForwardCacheForTesting(
web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
// 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 a pagehide 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("pagehide", function() {
// Waiting for 100ms, to give more time for browser-side things to go bad
// and delete RenderFrameHostImpl prematurely.
var start = (new Date()).getTime();
do {
curr = (new Date()).getTime();
} while (start + 100 > curr);
// Test that various RFHI-dependent things work fine in an unload handler.
stateObj = { "history_test_key": "history_test_value" }
history.replaceState(stateObj, 'title', window.location.href);
console.log('console.log() sent');
// As a sanity check, test that RFHI-independent things also work fine.
localStorage.localstorage_test_key = 'localstorage_test_value';
document.cookie = 'cookie_test_key=' +
'cookie_test_value; SameSite=none; Secure';
});
)"));
// Navigate A1(B2(C3) to A4.
{
// Prepare observers.
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern("console.log() sent");
RenderFrameDeletedObserver B2_deleted(B2);
RenderFrameDeletedObserver C3_deleted(C3);
// Navigate
GURL away_url(https_server()->GetURL("a.com", "/title1.html"));
ASSERT_TRUE(ExecJs(A1, JsReplace("location = $1", away_url)));
// Observers must be reached.
B2_deleted.WaitUntilDeleted();
C3_deleted.WaitUntilDeleted();
ASSERT_TRUE(console_observer.Wait());
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(away_url, web_contents()->GetLastCommittedURL());
}
// Navigate back from A4 to A5(B6(C7))
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Temporary extra expectations to investigate:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1215493
EXPECT_EQ(url, web_contents()->GetLastCommittedURL());
EXPECT_EQ(
3u, CollectAllRenderFrameHosts(web_contents()->GetPrimaryPage()).size());
RenderFrameHostImpl* A5 = web_contents()->GetPrimaryMainFrame();
RenderFrameHostImpl* B6 = A5->child_at(0)->current_frame_host();
RenderFrameHostImpl* C7 = B6->child_at(0)->current_frame_host();
// Verify that we can recover the data that should have been persisted by the
// pagehide 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 a pagehide handler from the initial empty document.
//
// Start from A1(B2(B3)).
// B3 is the initial empty document created by B2. A pagehide 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.onpagehide = () => {
window.domAutomationController.send("B3 unloaded");
}
)"));
ASSERT_EQ(1u, b2->child_count());
RenderFrameHostImpl* b3 = b2->child_at(0)->current_frame_host();
auto has_pagehide_handler = [](RenderFrameHostImpl* rfh) {
return rfh->GetSuddenTerminationDisablerState(
blink::mojom::SuddenTerminationDisablerType::kPageHideHandler);
};
EXPECT_FALSE(has_pagehide_handler(a1));
EXPECT_FALSE(has_pagehide_handler(b2));
EXPECT_TRUE(has_pagehide_handler(b3));
// 3. A1 deletes B2. This triggers the pagehide handler from B3.
DOMMessageQueue dom_message_queue(
WebContents::FromRenderFrameHost(web_contents()->GetPrimaryMainFrame()));
ExecuteScriptAsync(a1, "document.querySelector('iframe').remove();");
// Check the pagehide 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()));
// This test sets up a main frame which has an OOPIF. The main frame commits a
// same-site navigation. The test then stops at the stage where the unload
// handler of the OOPIF is running and the main frame RenderFrameHost's
// `DocumentAssociatedData` is retrieved from the OOPIF. The test shows that
// the `DocumentAssociatedData` is different from the one before navigation if
// RenderDocument feature is not enabled for all frames. One place we have seen
// this issue is in Protected Audience auctions. Please see crbug.com/1422301.
IN_PROC_BROWSER_TEST_P(
SitePerProcessBrowserTest,
MainFrameDocumentAssociatedDataChangesOnSameSiteNavigation) {
GURL initial_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
GURL next_url(embedded_test_server()->GetURL("login.a.com", "/title1.html"));
// 1) Navigate on a page with an OOPIF.
EXPECT_TRUE(NavigateToURL(shell(), initial_url));
FrameTreeNode* root_ftn = web_contents()->GetPrimaryFrameTree().root();
RenderFrameHostImpl* main_rfh = root_ftn->current_frame_host();
// 2) Act as if there was an infinite unload handler in the OOPIF.
RenderFrameHostImpl* child_rfh = root_ftn->child_at(0)->current_frame_host();
child_rfh->DoNotDeleteForTesting();
// Set an arbitrarily long timeout to ensure the subframe unload timer doesn't
// fire before we call OnDetach().
child_rfh->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));
// 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) Retrieve the weak pointer to the owned page by the main
// RenderFrameHost's `DocumentAssociatedData`.
base::WeakPtr<PageImpl> weak_ptr_page = child_rfh->GetPage().GetWeakPtrImpl();
// 4) Navigate the main frame to a same-site url. The unload handler of the
// OOPIF is running.
EXPECT_TRUE(NavigateToURL(shell(), next_url));
EXPECT_TRUE(child_rfh->IsPendingDeletion());
// 5) If RenderDocument feature is not enabled for all frames, the main frame
// RenderFrameHost will be the same.
EXPECT_EQ(
main_rfh ==
web_contents()->GetPrimaryFrameTree().root()->current_frame_host(),
GetRenderDocumentLevel() < RenderDocumentLevel::kAllFrames);
// 6) If RenderDocument feature is not enabled for all frames, verify
// `PageImpl` has changed by checking the weak pointer.
EXPECT_EQ(weak_ptr_page == nullptr,
GetRenderDocumentLevel() < RenderDocumentLevel::kAllFrames);
}
} // namespace content