| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/common/content_switches.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.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "net/dns/mock_host_resolver.h" |
| |
| namespace content { |
| |
| class MessagePortCloseEventBrowserTest : public ContentBrowserTest { |
| public: |
| MessagePortCloseEventBrowserTest() { |
| InitBackForwardCacheFeature(&feature_list_for_back_forward_cache_, |
| /*enable_back_forward_cache=*/true); |
| } |
| |
| ~MessagePortCloseEventBrowserTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| // Setup the server to allow serving separate sites, so we can perform |
| // cross-process navigation. |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, |
| "MessagePortCloseEvent"); |
| } |
| |
| RenderFrameHost* GetPrimaryMainFrame() { |
| return shell()->web_contents()->GetPrimaryMainFrame(); |
| } |
| |
| base::test::ScopedFeatureList feature_list_for_back_forward_cache_; |
| }; |
| |
| // Confirm the close event is fired when the page crashes. |
| IN_PROC_BROWSER_TEST_F(MessagePortCloseEventBrowserTest, |
| CloseEventHappensIfProcessCrashes) { |
| // If site isolation is turned off, A and B shares the same SiteInstance. |
| // So, when A crashes, B also crashes. |
| if (!SiteIsolationPolicy::UseDedicatedProcessesForAllSites()) { |
| GTEST_SKIP() << "test requires site isolation"; |
| } |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A and open a popup. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| WebContents* contents_a = shell()->web_contents(); |
| RenderFrameHostImplWrapper rfh_a(GetPrimaryMainFrame()); |
| Shell* popup = OpenPopup(rfh_a.get(), url_b, ""); |
| WebContents* contents_b = popup->web_contents(); |
| ASSERT_EQ(2u, rfh_a->GetSiteInstance()->GetRelatedActiveContentsCount()); |
| |
| // 2) Set up a MessageChannel between page A and page B. The message channel |
| // is created in page B and one of the ports is passed to page A. |
| ASSERT_TRUE(ExecJs(contents_a, R"( |
| window.onmessage = (e) => { |
| const port = e.ports[0]; |
| port.start(); |
| } |
| )")); |
| |
| ASSERT_TRUE(ExecJs(contents_b, R"( |
| const {port1, port2} = new MessageChannel(); |
| port1.start(); |
| var closeEventPromise = new Promise(resolve => port1.onclose = resolve); |
| window.opener.postMessage('', '*', [port2]); |
| )")); |
| |
| // 3) Crash the renderer A. |
| CrashTab(contents_a); |
| |
| // 4) Confirm the close event is fired on the port in page B. |
| EXPECT_TRUE(ExecJs(contents_b, "closeEventPromise")); |
| } |
| |
| // Confirm the close event is fired when the document stored in the BFCache |
| // is destroyed. |
| IN_PROC_BROWSER_TEST_F(MessagePortCloseEventBrowserTest, |
| CloseEventHappensIfPageEvictedFromBackForwardCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // 1) Load a page that registers a service worker. |
| ASSERT_TRUE(NavigateToURL(shell(), |
| embedded_test_server()->GetURL( |
| "/service_worker/create_service_worker.html"))); |
| ASSERT_EQ("DONE", |
| EvalJs(shell(), "register('message_port_close_event.js');")); |
| |
| // 2) Load the page again so we are controlled. |
| ASSERT_TRUE(NavigateToURL(shell(), |
| embedded_test_server()->GetURL( |
| "/service_worker/create_service_worker.html"))); |
| ASSERT_EQ(true, EvalJs(shell(), "!!navigator.serviceWorker.controller")); |
| |
| // 3) Establish message port connection with the service worker. |
| ASSERT_TRUE(ExecJs(shell(), R"( |
| const {port1, port2} = new MessageChannel(); |
| port1.start(); |
| const ctrl = navigator.serviceWorker.controller; |
| ctrl.postMessage("init", [port2]); |
| )")); |
| RenderFrameHostImplWrapper rfh_1(GetPrimaryMainFrame()); |
| WebContents* web_contents = shell()->web_contents(); |
| |
| // 4) Navigate to the empty page in the service worker's scope and confirm the |
| // previous page is in BackForwardCache. |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/service_worker/empty.html"))); |
| ASSERT_TRUE(rfh_1->IsInBackForwardCache()); |
| RenderFrameHostImplWrapper rfh_2(GetPrimaryMainFrame()); |
| |
| // 5) Ask the service worker to create the promise that resolves when |
| // it receives a close event from the BFCached page when that page gets |
| // evicted. |
| ASSERT_TRUE(ExecJs(rfh_2.get(), R"( |
| navigator.serviceWorker.controller.postMessage("wait for close event"); |
| var closeEventPromise = new Promise(resolve => { |
| navigator.serviceWorker.addEventListener('message', (event) => { |
| resolve(event.data); |
| })}); |
| )")); |
| |
| // 6) Flush the cache and evict the previously BFCached page. |
| web_contents->GetController().GetBackForwardCache().Flush(); |
| |
| // 7) Confirm the previous page is evicted. |
| ASSERT_TRUE(rfh_1.WaitUntilRenderFrameDeleted()); |
| |
| // 8) Confirm the close event is fired on the port in the service worker. |
| EXPECT_EQ("close event is fired", EvalJs(rfh_2.get(), "closeEventPromise")); |
| } |
| } // namespace content |