
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}
1778 lines
74 KiB
C++
1778 lines
74 KiB
C++
// 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
|