| // 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 |