| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| |
| #include "base/base_switches.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/test/trace_event_analyzer.h" |
| #include "base/trace_event/trace_config.h" |
| #include "build/build_config.h" |
| #include "components/download/public/common/download_item.h" |
| #include "components/viz/host/host_frame_sink_manager.h" |
| #include "content/browser/compositor/surface_utils.h" |
| #include "content/browser/portal/portal.h" |
| #include "content/browser/renderer_host/input/synthetic_smooth_scroll_gesture.h" |
| #include "content/browser/renderer_host/input/synthetic_tap_gesture.h" |
| #include "content/browser/renderer_host/input/synthetic_touchpad_pinch_gesture.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/renderer_host/render_frame_host_manager.h" |
| #include "content/browser/renderer_host/render_frame_proxy_host.h" |
| #include "content/browser/renderer_host/render_widget_host_input_event_router.h" |
| #include "content/browser/renderer_host/render_widget_host_view_base.h" |
| #include "content/browser/renderer_host/render_widget_host_view_child_frame.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/frame.mojom-test-utils.h" |
| #include "content/common/input/synthetic_pinch_gesture_params.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/download_manager.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/browser/tracing_controller.h" |
| #include "content/public/browser/web_contents_delegate.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/page_type.h" |
| #include "content/public/test/accessibility_notification_waiter.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/public/test/fenced_frame_test_util.h" |
| #include "content/public/test/hit_test_region_observer.h" |
| #include "content/public/test/navigation_handle_observer.h" |
| #include "content/public/test/render_frame_host_test_support.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/shell/browser/shell_browser_context.h" |
| #include "content/shell/browser/shell_content_browser_client.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "content/test/portal/portal_activated_observer.h" |
| #include "content/test/portal/portal_created_observer.h" |
| #include "content/test/portal/portal_interceptor_for_testing.h" |
| #include "content/test/test_render_frame_host_factory.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/url_util.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/input/focus_type.mojom.h" |
| #include "third_party/blink/public/mojom/portal/portal.mojom.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| |
| using ::testing::_; |
| using trace_analyzer::Query; |
| using trace_analyzer::TraceAnalyzer; |
| using trace_analyzer::TraceEventVector; |
| |
| namespace content { |
| |
| class PortalBrowserTest : public ContentBrowserTest { |
| protected: |
| PortalBrowserTest() { |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/{blink::features::kPortals, |
| blink::features::kPortalsCrossOrigin}, |
| /*disabled_features=*/{}); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitch(switches::kValidateInputEventStream); |
| command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, |
| "OverscrollCustomization"); |
| } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ContentBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| Portal* CreatePortalToUrl(WebContentsImpl* host_contents, |
| GURL portal_url, |
| int number_of_navigations = 1, |
| bool expected_to_succeed = true) { |
| EXPECT_GE(number_of_navigations, 1); |
| RenderFrameHostImpl* main_frame = host_contents->GetPrimaryMainFrame(); |
| |
| // Create portal and wait for navigation. |
| PortalCreatedObserver portal_created_observer(main_frame); |
| TestNavigationObserver navigation_observer(nullptr, number_of_navigations); |
| navigation_observer.set_wait_event( |
| TestNavigationObserver::WaitEvent::kNavigationFinished); |
| navigation_observer.StartWatchingNewWebContents(); |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("{" |
| " let portal = document.createElement('portal');" |
| " portal.src = $1;" |
| " document.body.appendChild(portal);" |
| "}", |
| portal_url), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| Portal* portal = portal_created_observer.WaitUntilPortalCreated(); |
| navigation_observer.StopWatchingNewWebContents(); |
| |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| EXPECT_TRUE(portal_contents); |
| |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(expected_to_succeed, WaitForLoadStop(portal_contents)); |
| |
| return portal; |
| } |
| |
| protected: |
| // Adapted from metric_integration_test.cc. |
| void StartTracing() { |
| base::RunLoop wait_for_tracing; |
| content::TracingController::GetInstance()->StartTracing( |
| base::trace_event::TraceConfig( |
| "{\"included_categories\": [\"navigation\"]}"), |
| wait_for_tracing.QuitClosure()); |
| wait_for_tracing.Run(); |
| } |
| |
| std::string StopTracing() { |
| base::RunLoop wait_for_tracing; |
| std::string trace_output; |
| content::TracingController::GetInstance()->StopTracing( |
| content::TracingController::CreateStringEndpoint( |
| base::BindLambdaForTesting( |
| [&](std::unique_ptr<std::string> trace_str) { |
| trace_output = std::move(*trace_str); |
| wait_for_tracing.Quit(); |
| }))); |
| wait_for_tracing.Run(); |
| return trace_output; |
| } |
| |
| void VerifyActivationTraceEvents(const std::string& trace_str) { |
| std::unique_ptr<TraceAnalyzer> analyzer(TraceAnalyzer::Create(trace_str)); |
| TraceEventVector events; |
| auto query = Query::EventNameIs("LocalFrame::OnPortalActivated") || |
| Query::EventNameIs("RenderFrameHostImpl::OnPortalActivated") || |
| Query::EventNameIs("PortalContents::Activate"); |
| size_t num_events = analyzer->FindEvents(query, &events); |
| EXPECT_EQ(7UL, num_events); |
| char phases[] = { |
| TRACE_EVENT_PHASE_COMPLETE, TRACE_EVENT_PHASE_FLOW_BEGIN, |
| TRACE_EVENT_PHASE_COMPLETE, TRACE_EVENT_PHASE_FLOW_END, |
| TRACE_EVENT_PHASE_FLOW_BEGIN, TRACE_EVENT_PHASE_COMPLETE, |
| TRACE_EVENT_PHASE_FLOW_END, |
| }; |
| |
| // TODO(crbug.com/1139541): the predecessor may terminate before all trace |
| // events are processed. Until this as addressed, the trace event for the |
| // start of activation may not be closed. If this happens, it will be |
| // TRACE_EVENT_PHASE_BEGIN rather than _COMPLETE. Will accept either |
| // TRACE_EVENT_PHASE_COMPLETE or _BEGIN to avoid flake. |
| for (size_t i = 0; i < events.size(); ++i) { |
| if (phases[i] == TRACE_EVENT_PHASE_COMPLETE) { |
| EXPECT_TRUE(events[i]->phase == TRACE_EVENT_PHASE_COMPLETE || |
| events[i]->phase == TRACE_EVENT_PHASE_BEGIN) |
| << "mismatch at " << i; |
| } else { |
| EXPECT_EQ(phases[i], events[i]->phase) << "mismatch at " << i; |
| } |
| } |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that the renderer can create a Portal. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, CreatePortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* primary_rfh = web_contents_impl->GetPrimaryMainFrame(); |
| |
| PortalCreatedObserver portal_created_observer(primary_rfh); |
| EXPECT_TRUE( |
| ExecJs(primary_rfh, |
| "document.body.appendChild(document.createElement('portal'));")); |
| Portal* portal = portal_created_observer.WaitUntilPortalCreated(); |
| EXPECT_NE(nullptr, portal); |
| |
| RenderFrameHostImpl* portal_rfh = |
| portal->GetPortalContents()->GetPrimaryMainFrame(); |
| EXPECT_NE(&primary_rfh->GetPage(), &portal_rfh->GetPage()); |
| EXPECT_TRUE(primary_rfh->GetPage().IsPrimary()); |
| EXPECT_TRUE(portal_rfh->GetPage().IsPrimary()); |
| } |
| |
| // Tests the the renderer can navigate a Portal. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, NavigatePortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Tests that a portal can navigate by setting its src before appending it to |
| // the DOM. |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContents* portal_contents = portal->GetPortalContents(); |
| EXPECT_EQ(portal_contents->GetLastCommittedURL(), a_url); |
| |
| // Tests that a portal can navigate by setting its src. |
| { |
| TestNavigationObserver navigation_observer(portal_contents); |
| |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("document.querySelector('portal').src = $1;", b_url))); |
| navigation_observer.Wait(); |
| EXPECT_EQ(navigation_observer.last_navigation_url(), b_url); |
| EXPECT_EQ(portal_contents->GetLastCommittedURL(), b_url); |
| } |
| |
| // Tests that a portal can navigate by setting the attribute src. |
| { |
| TestNavigationObserver navigation_observer(portal_contents); |
| |
| GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html")); |
| EXPECT_TRUE(ExecJs( |
| main_frame, |
| JsReplace("document.querySelector('portal').setAttribute('src', $1);", |
| c_url))); |
| navigation_observer.Wait(); |
| EXPECT_EQ(navigation_observer.last_navigation_url(), c_url); |
| EXPECT_EQ(portal_contents->GetLastCommittedURL(), c_url); |
| } |
| } |
| |
| #if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64) |
| // Bulk disabled as part of arm64 bot stabilization: https://crbug.com/1154345 |
| #define MAYBE_ActivatePortal DISABLED_ActivatePortal |
| #else |
| #define MAYBE_ActivatePortal ActivatePortal |
| #endif |
| |
| // Tests that a portal can be activated. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, MAYBE_ActivatePortal) { |
| StartTracing(); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| |
| // Ensure that the portal WebContents exists and is different from the tab's |
| // WebContents. |
| WebContents* portal_contents = portal->GetPortalContents(); |
| EXPECT_NE(nullptr, portal_contents); |
| EXPECT_NE(portal_contents, shell()->web_contents()); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| activated_observer.WaitForActivate(); |
| |
| // After activation, the shell's WebContents should be the previous portal's |
| // WebContents. |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWillUnload, |
| activated_observer.WaitForActivateResult()); |
| |
| // Verify that we logged the correct trace events. |
| VerifyActivationTraceEvents(StopTracing()); |
| } |
| |
| // Test that portal uses own UKM source id during navigation. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, GetPageUkmSourceId) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* primary_rfh = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| RenderFrameHostImpl* portal_rfh = |
| portal->GetPortalContents()->GetPrimaryMainFrame(); |
| EXPECT_TRUE(portal_rfh); |
| |
| // Ensure that portal uses own UKM source id, not from the primary main frame. |
| // TODO(crbug.com/1254770): Modify this test to check the source UKM ID during |
| // navigation as once portals are migrated to MPArch. |
| EXPECT_NE(primary_rfh->GetPageUkmSourceId(), |
| portal_rfh->GetPageUkmSourceId()); |
| } |
| |
| // This fixture enables PortalsDefaultActivation |
| class PortalDefaultActivationBrowserTest : public PortalBrowserTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| PortalBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, |
| "PortalsDefaultActivation"); |
| } |
| }; |
| |
| #if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64) |
| // Bulk disabled as part of arm64 bot stabilization: https://crbug.com/1154345 |
| #define MAYBE_DefaultActivatePortal DISABLED_DefaultActivatePortal |
| #else |
| #define MAYBE_DefaultActivatePortal DefaultActivatePortal |
| #endif |
| |
| // Tests the correct trace events are generated when a portal is default |
| // activated. |
| IN_PROC_BROWSER_TEST_F(PortalDefaultActivationBrowserTest, |
| MAYBE_DefaultActivatePortal) { |
| StartTracing(); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| |
| // Ensure that the portal WebContents exists and is different from the tab's |
| // WebContents. |
| WebContents* portal_contents = portal->GetPortalContents(); |
| EXPECT_NE(nullptr, portal_contents); |
| EXPECT_NE(portal_contents, shell()->web_contents()); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, "document.querySelector('portal').click();"); |
| activated_observer.WaitForActivate(); |
| |
| // After activation, the shell's WebContents should be the previous portal's |
| // WebContents. |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWillUnload, |
| activated_observer.WaitForActivateResult()); |
| |
| // Verify that we logged the correct trace events. |
| VerifyActivationTraceEvents(StopTracing()); |
| } |
| |
| #if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64) |
| // https://crbug.com/1222682 |
| #define MAYBE_AdoptPredecessor DISABLED_AdoptPredecessor |
| #else |
| #define MAYBE_AdoptPredecessor AdoptPredecessor |
| #endif |
| // Tests if a portal can be activated and the predecessor can be adopted. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, MAYBE_AdoptPredecessor) { |
| StartTracing(); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| |
| // Ensure that the portal WebContents exists and is different from the tab's |
| // WebContents. |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| EXPECT_NE(nullptr, portal_contents); |
| EXPECT_NE(portal_contents, shell()->web_contents()); |
| |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "window.addEventListener('portalactivate', e => { " |
| " var portal = e.adoptPredecessor(); " |
| " document.body.appendChild(portal); " |
| "});")); |
| |
| { |
| PortalActivatedObserver activated_observer(portal); |
| PortalCreatedObserver adoption_observer(portal_frame); |
| ExecuteScriptAsync(main_frame, |
| "let portal = document.querySelector('portal');" |
| "portal.activate().then(() => { " |
| " document.body.removeChild(portal); " |
| "});"); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWasAdopted, |
| activated_observer.WaitForActivateResult()); |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| |
| VerifyActivationTraceEvents(StopTracing()); |
| |
| // After activation, the shell's WebContents should be the previous portal's |
| // WebContents. |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| // The original predecessor WebContents should be adopted as a portal. |
| EXPECT_TRUE(web_contents_impl->IsPortal()); |
| EXPECT_EQ(web_contents_impl->GetOuterWebContents(), portal_contents); |
| } |
| |
| // Tests that the RenderFrameProxyHost is created and initialized when the |
| // portal is initialized. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, RenderFrameProxyHostCreated) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameProxyHost* proxy_host = portal_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->GetProxyToOuterDelegate(); |
| EXPECT_TRUE(proxy_host->is_render_frame_proxy_live()); |
| } |
| |
| // Tests that the portal's outer delegate frame tree node and any iframes |
| // inside the portal are deleted when the portal element is removed from the |
| // document. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, DetachPortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a)")); |
| // Wait for a second navigation for the inner iframe. |
| Portal* portal = CreatePortalToUrl(web_contents, a_url, 2); |
| |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| FrameTreeNode* portal_main_frame_node = |
| portal_contents->GetPrimaryFrameTree().root(); |
| |
| // Remove portal from document and wait for frames to be deleted. |
| FrameDeletedObserver fdo1(portal_main_frame_node->render_manager() |
| ->GetOuterDelegateNode() |
| ->current_frame_host()); |
| FrameDeletedObserver fdo2( |
| portal_main_frame_node->child_at(0)->current_frame_host()); |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| "document.body.removeChild(document.querySelector('portal'));")); |
| fdo1.Wait(); |
| fdo2.Wait(); |
| } |
| |
| // Test that FrameTree::CollectNodesForIsLoading doesn't include inner |
| // WebContents nodes like portals. |
| // |
| // TODO(crbug.com/1254770): Modify this test accordingly once portals are |
| // migrated to MPArch. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, NodesForIsLoading) { |
| // 1. Navigate to an initial primary page. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* primary_rfh = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // 2. Create a portal. |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| RenderFrameHostImpl* portal_rfh = |
| portal->GetPortalContents()->GetPrimaryMainFrame(); |
| EXPECT_TRUE(portal_rfh); |
| |
| // 3. FrameTree::CollectNodesForIsLoading should only include primary_rfh but |
| // not portal_rfh. |
| std::vector<RenderFrameHostImpl*> outer_web_contents_frames; |
| for (auto* ftn : |
| web_contents_impl->GetPrimaryFrameTree().CollectNodesForIsLoading()) { |
| outer_web_contents_frames.push_back(ftn->current_frame_host()); |
| } |
| EXPECT_EQ(outer_web_contents_frames.size(), 1u); |
| EXPECT_THAT(outer_web_contents_frames, |
| testing::UnorderedElementsAre(primary_rfh)); |
| } |
| |
| // This is for testing how portals interact with input hit testing. It is |
| // parameterized on the kind of viz hit testing used. |
| class PortalHitTestBrowserTest : public PortalBrowserTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| PortalBrowserTest::SetUpCommandLine(command_line); |
| IsolateAllSitesForTesting(command_line); |
| } |
| }; |
| |
| namespace { |
| |
| // Fails the test if an input event is sent to the given RenderWidgetHost. |
| class FailOnInputEvent : public RenderWidgetHost::InputEventObserver { |
| public: |
| explicit FailOnInputEvent(RenderWidgetHostImpl* rwh) |
| : rwh_(rwh->GetWeakPtr()) { |
| rwh->AddInputEventObserver(this); |
| } |
| |
| ~FailOnInputEvent() override { |
| if (rwh_) |
| rwh_->RemoveInputEventObserver(this); |
| } |
| |
| void OnInputEvent(const blink::WebInputEvent& event) override { |
| FAIL() << "Unexpected " << blink::WebInputEvent::GetName(event.GetType()); |
| } |
| |
| private: |
| base::WeakPtr<RenderWidgetHostImpl> rwh_; |
| }; |
| |
| } // namespace |
| |
| // Tests that input events targeting the portal are only received by the parent |
| // renderer. |
| IN_PROC_BROWSER_TEST_F(PortalHitTestBrowserTest, DispatchInputEvent) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(static_cast<RenderWidgetHostViewBase*>(portal_frame->GetView()) |
| ->IsRenderWidgetHostViewChildFrame()); |
| RenderWidgetHostViewChildFrame* portal_view = |
| static_cast<RenderWidgetHostViewChildFrame*>(portal_frame->GetView()); |
| WaitForHitTestData(portal_frame); |
| |
| FailOnInputEvent no_input_to_portal_frame( |
| portal_frame->GetRenderWidgetHost()); |
| EXPECT_TRUE(ExecJs( |
| main_frame, |
| "var clicked = false;" |
| "document.querySelector('portal').onmousedown = _ => clicked = true;")); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "var clicked = false;" |
| "document.body.onmousedown = _ => clicked = true;")); |
| EXPECT_EQ(false, EvalJs(main_frame, "clicked")); |
| EXPECT_EQ(false, EvalJs(portal_frame, "clicked")); |
| |
| // Route the mouse event. |
| gfx::Point root_location = |
| portal_view->TransformPointToRootCoordSpace(gfx::Point(5, 5)); |
| InputEventAckWaiter waiter(main_frame->GetRenderWidgetHost(), |
| blink::WebInputEvent::Type::kMouseDown); |
| SimulateMouseEvent(web_contents_impl, blink::WebInputEvent::Type::kMouseDown, |
| blink::WebPointerProperties::Button::kLeft, root_location); |
| waiter.Wait(); |
| |
| // Check that the click event was only received by the main frame. |
| EXPECT_EQ(true, EvalJs(main_frame, "clicked")); |
| EXPECT_EQ(false, EvalJs(portal_frame, "clicked")); |
| } |
| |
| // Tests that input events performed over on OOPIF inside a portal are targeted |
| // to the portal's parent. |
| IN_PROC_BROWSER_TEST_F(PortalHitTestBrowserTest, NoInputToOOPIFInPortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Create portal and wait for navigation. |
| // In the case of crbug.com/1002228 , this does not appear to reproduce if the |
| // portal element is too small, so we give it an explicit size. |
| Portal* portal = nullptr; |
| { |
| PortalCreatedObserver portal_created_observer(main_frame); |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| TestNavigationObserver navigation_observer(a_url); |
| navigation_observer.StartWatchingNewWebContents(); |
| EXPECT_TRUE(ExecJs( |
| main_frame, JsReplace("var portal = document.createElement('portal');" |
| "portal.src = $1;" |
| "portal.style.width = '500px';" |
| "portal.style.height = '500px';" |
| "portal.style.border = 'solid';" |
| "document.body.appendChild(portal);", |
| a_url))); |
| portal = portal_created_observer.WaitUntilPortalCreated(); |
| navigation_observer.Wait(); |
| } |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| WaitForHitTestData(portal_frame); |
| |
| // Add an out-of-process iframe to the portal. |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| TestNavigationObserver iframe_navigation_observer(portal_contents); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| JsReplace("var iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| b_url))); |
| iframe_navigation_observer.Wait(); |
| EXPECT_EQ(b_url, iframe_navigation_observer.last_navigation_url()); |
| RenderFrameHostImpl* portal_iframe = |
| portal_frame->child_at(0)->current_frame_host(); |
| EXPECT_TRUE(static_cast<RenderWidgetHostViewBase*>(portal_iframe->GetView()) |
| ->IsRenderWidgetHostViewChildFrame()); |
| RenderWidgetHostViewChildFrame* oopif_view = |
| static_cast<RenderWidgetHostViewChildFrame*>(portal_iframe->GetView()); |
| EXPECT_NE(portal_frame->GetSiteInstance(), portal_iframe->GetSiteInstance()); |
| WaitForHitTestData(portal_iframe); |
| |
| FailOnInputEvent no_input_to_portal_frame( |
| portal_frame->GetRenderWidgetHost()); |
| FailOnInputEvent no_input_to_oopif(portal_iframe->GetRenderWidgetHost()); |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var clicked = false;" |
| "portal.onmousedown = _ => clicked = true;")); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "var clicked = false;" |
| "document.body.onmousedown = _ => clicked = true;")); |
| EXPECT_TRUE(ExecJs(portal_iframe, |
| "var clicked = false;" |
| "document.body.onmousedown = _ => clicked = true;")); |
| |
| // Route the mouse event. |
| gfx::Point root_location = |
| oopif_view->TransformPointToRootCoordSpace(gfx::Point(5, 5)); |
| InputEventAckWaiter waiter(main_frame->GetRenderWidgetHost(), |
| blink::WebInputEvent::Type::kMouseDown); |
| SimulateMouseEvent(web_contents_impl, blink::WebInputEvent::Type::kMouseDown, |
| blink::WebPointerProperties::Button::kLeft, root_location); |
| waiter.Wait(); |
| |
| // Check that the click event was only received by the main frame. |
| EXPECT_EQ(true, EvalJs(main_frame, "clicked")); |
| EXPECT_EQ(false, EvalJs(portal_frame, "clicked")); |
| EXPECT_EQ(false, EvalJs(portal_iframe, "clicked")); |
| } |
| |
| // Tests that an OOPIF inside a portal receives input events after the portal is |
| // activated. |
| // Flaky on macOS: https://crbug.com/1042703 |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_InputToOOPIFAfterActivation DISABLED_InputToOOPIFAfterActivation |
| #else |
| #define MAYBE_InputToOOPIFAfterActivation InputToOOPIFAfterActivation |
| #endif |
| IN_PROC_BROWSER_TEST_F(PortalHitTestBrowserTest, |
| MAYBE_InputToOOPIFAfterActivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Create portal. |
| // TODO(crbug.com/1029330): We currently need to give portal a large enough |
| // size to prevent overlap with iframe as this results in the test becoming |
| // flaky. |
| Portal* portal = nullptr; |
| { |
| PortalCreatedObserver portal_created_observer(main_frame); |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| TestNavigationObserver navigation_observer(a_url); |
| navigation_observer.StartWatchingNewWebContents(); |
| EXPECT_TRUE(ExecJs( |
| main_frame, JsReplace("let portal = document.createElement('portal');" |
| "portal.src = $1;" |
| "portal.style.width = '500px';" |
| "portal.style.height = '500px';" |
| "document.body.appendChild(portal);", |
| a_url))); |
| portal = portal_created_observer.WaitUntilPortalCreated(); |
| navigation_observer.Wait(); |
| } |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| WaitForHitTestData(portal_frame); |
| |
| // Add an out-of-process iframe to the portal. |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| TestNavigationObserver iframe_navigation_observer(portal_contents); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| JsReplace("var iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| b_url))); |
| iframe_navigation_observer.Wait(); |
| EXPECT_EQ(b_url, iframe_navigation_observer.last_navigation_url()); |
| |
| RenderFrameHostImpl* oopif = portal_frame->child_at(0)->current_frame_host(); |
| RenderWidgetHostViewBase* oopif_view = |
| static_cast<RenderWidgetHostViewBase*>(oopif->GetView()); |
| EXPECT_TRUE(oopif_view->IsRenderWidgetHostViewChildFrame()); |
| EXPECT_NE(portal_frame->GetSiteInstance(), oopif->GetSiteInstance()); |
| WaitForHitTestData(oopif); |
| EXPECT_TRUE(ExecJs(oopif, |
| "var clicked = false;" |
| "document.body.onmousedown = _ => clicked = true;")); |
| |
| // Activate the portal. |
| { |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "let portal = document.querySelector('portal');" |
| "portal.activate().then(() => { " |
| " document.body.removeChild(portal); " |
| "});"); |
| activated_observer.WaitForActivate(); |
| |
| RenderWidgetHostViewBase* view = |
| portal_frame->GetRenderWidgetHost()->GetView(); |
| viz::FrameSinkId root_frame_sink_id = view->GetRootFrameSinkId(); |
| HitTestRegionObserver hit_test_observer(root_frame_sink_id); |
| |
| // The hit test region for the portal frame should be at index 1 after |
| // activation, so we wait for the hit test data to update until it's in |
| // this state. |
| auto hit_test_index = [&]() -> absl::optional<size_t> { |
| const auto& display_hit_test_query_map = |
| GetHostFrameSinkManager()->display_hit_test_query(); |
| auto it = display_hit_test_query_map.find(root_frame_sink_id); |
| // On Mac, we create a new root layer after activation, so the hit test |
| // data may not have anything for the new layer yet. |
| if (it == display_hit_test_query_map.end()) |
| return absl::nullopt; |
| CHECK_EQ(portal_frame->GetRenderWidgetHost()->GetView(), view); |
| size_t index; |
| if (!it->second->FindIndexOfFrameSink(view->GetFrameSinkId(), &index)) |
| return absl::nullopt; |
| return index; |
| }; |
| hit_test_observer.WaitForHitTestData(); |
| while (hit_test_index() != 1u) |
| hit_test_observer.WaitForHitTestDataChange(); |
| } |
| EXPECT_EQ(shell()->web_contents(), portal_contents); |
| |
| // Send a mouse event to the OOPIF. |
| gfx::Point root_location = |
| oopif_view->TransformPointToRootCoordSpace(gfx::Point(10, 10)); |
| InputEventAckWaiter waiter(oopif->GetRenderWidgetHost(), |
| blink::WebInputEvent::Type::kMouseDown); |
| SimulateMouseEvent(shell()->web_contents(), |
| blink::WebInputEvent::Type::kMouseDown, |
| blink::WebPointerProperties::Button::kLeft, root_location); |
| waiter.Wait(); |
| |
| // Check that the click event was received by the iframe. |
| EXPECT_EQ(true, EvalJs(oopif, "clicked")); |
| } |
| |
| // Tests that async hit testing does not target portals. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, AsyncEventTargetingIgnoresPortals) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(portal_frame->GetView()) |
| ->IsRenderWidgetHostViewChildFrame()); |
| RenderWidgetHostViewChildFrame* portal_view = |
| static_cast<RenderWidgetHostViewChildFrame*>(portal_frame->GetView()); |
| WaitForHitTestData(portal_frame); |
| |
| viz::mojom::InputTargetClient* target_client = |
| main_frame->GetRenderWidgetHost()->input_target_client().get(); |
| ASSERT_TRUE(target_client); |
| |
| gfx::PointF root_location = |
| portal_view->TransformPointToRootCoordSpaceF(gfx::PointF(5, 5)); |
| |
| // Query the renderer for the target widget. The root should claim the point |
| // for itself, not the portal. |
| base::RunLoop run_loop; |
| base::OnceClosure quit_closure = run_loop.QuitClosure(); |
| viz::FrameSinkId received_frame_sink_id; |
| target_client->FrameSinkIdAt( |
| root_location, 0, |
| base::BindLambdaForTesting( |
| [&](const viz::FrameSinkId& id, const gfx::PointF& point) { |
| received_frame_sink_id = id; |
| std::move(quit_closure).Run(); |
| })); |
| run_loop.Run(); |
| |
| viz::FrameSinkId root_frame_sink_id = |
| static_cast<RenderWidgetHostViewBase*>(main_frame->GetView()) |
| ->GetFrameSinkId(); |
| EXPECT_EQ(root_frame_sink_id, received_frame_sink_id) |
| << "Note: The portal's FrameSinkId is " << portal_view->GetFrameSinkId(); |
| } |
| |
| // Tests that trying to navigate to a chrome:// URL kills the renderer. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, NavigateToChrome) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Create portal. |
| PortalCreatedObserver portal_created_observer(main_frame); |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var portal = document.createElement('portal');" |
| "document.body.appendChild(portal);")); |
| Portal* portal = portal_created_observer.WaitUntilPortalCreated(); |
| PortalInterceptorForTesting* portal_interceptor = |
| PortalInterceptorForTesting::From(portal); |
| |
| // Try to navigate to chrome://settings and wait for the process to die. |
| portal_interceptor->SetNavigateCallback(base::BindRepeating( |
| [](Portal* portal, const GURL& url, blink::mojom::ReferrerPtr referrer, |
| blink::mojom::Portal::NavigateCallback callback) { |
| GURL chrome_url("chrome://settings"); |
| portal->Navigate(chrome_url, std::move(referrer), std::move(callback)); |
| }, |
| portal)); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| std::ignore = ExecJs(main_frame, JsReplace("portal.src = $1;", a_url)); |
| |
| EXPECT_EQ(bad_message::RPH_MOJO_PROCESS_ERROR, kill_waiter.Wait()); |
| } |
| |
| // Regression test for crbug.com/969714. Tests that receiving a touch ack |
| // from the predecessor after portal activation doesn't cause a crash. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, TouchAckAfterActivate) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("document.body.addEventListener('touchstart', e => {" |
| " document.querySelector('portal').activate();" |
| "}, {passive: false});"))); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| RenderWidgetHostViewChildFrame* portal_view = |
| static_cast<RenderWidgetHostViewChildFrame*>(portal_frame->GetView()); |
| InputEventAckWaiter input_event_ack_waiter( |
| render_widget_host, blink::WebInputEvent::Type::kTouchStart); |
| WaitForHitTestData(portal_frame); |
| |
| SyntheticTapGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.position = |
| portal_view->TransformPointToRootCoordSpaceF(gfx::PointF(5, 5)); |
| |
| PortalActivatedObserver activated_observer(portal); |
| std::unique_ptr<SyntheticTapGesture> gesture = |
| std::make_unique<SyntheticTapGesture>(params); |
| render_widget_host->QueueSyntheticGesture(std::move(gesture), |
| base::DoNothing()); |
| activated_observer.WaitForActivate(); |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| |
| // Wait for a touch ack to be sent from the predecessor. |
| input_event_ack_waiter.Wait(); |
| } |
| |
| // Regression test for crbug.com/973647. Tests that receiving a touch ack |
| // after activation and predecessor adoption doesn't cause a crash. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, TouchAckAfterActivateAndAdopt) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| const int TOUCH_ACK_DELAY_IN_MILLISECONDS = 500; |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("document.body.addEventListener('touchstart', e => {" |
| " document.querySelector('portal').activate();" |
| " var stop = performance.now() + $1;" |
| " while (performance.now() < stop) {}" |
| "}, {passive: false});", |
| TOUCH_ACK_DELAY_IN_MILLISECONDS))); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "window.addEventListener('portalactivate', e => {" |
| " var portal = e.adoptPredecessor();" |
| " document.body.appendChild(portal);" |
| "});")); |
| WaitForHitTestData(portal_frame); |
| |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| InputEventAckWaiter input_event_ack_waiter( |
| render_widget_host, blink::WebInputEvent::Type::kTouchStart); |
| |
| SyntheticTapGestureParams params; |
| RenderWidgetHostViewChildFrame* portal_view = |
| static_cast<RenderWidgetHostViewChildFrame*>(portal_frame->GetView()); |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.position = |
| portal_view->TransformPointToRootCoordSpaceF(gfx::PointF(5, 5)); |
| |
| std::unique_ptr<SyntheticTapGesture> gesture = |
| std::make_unique<SyntheticTapGesture>(params); |
| { |
| PortalActivatedObserver activated_observer(portal); |
| PortalCreatedObserver adoption_observer(portal_frame); |
| render_widget_host->QueueSyntheticGesture(std::move(gesture), |
| base::DoNothing()); |
| activated_observer.WaitForActivate(); |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| // Wait for predecessor to be adopted. |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| |
| // Wait for a touch ack to be sent from the predecessor. |
| input_event_ack_waiter.Wait(); |
| } |
| |
| // Regression test for crbug.com/973647. Tests that receiving a touch ack |
| // after activation and reactivating a predecessor doesn't cause a crash. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, TouchAckAfterActivateAndReactivate) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| const int TOUCH_ACK_DELAY_IN_MILLISECONDS = 500; |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("document.body.addEventListener('touchstart', e => {" |
| " document.querySelector('portal').activate();" |
| " var stop = performance.now() + $1;" |
| " while (performance.now() < stop) {}" |
| "}, {passive: false});", |
| TOUCH_ACK_DELAY_IN_MILLISECONDS))); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "window.addEventListener('portalactivate', e => {" |
| " var portal = e.adoptPredecessor();" |
| " document.body.appendChild(portal);" |
| " portal.activate();" |
| "});")); |
| WaitForHitTestData(portal_frame); |
| |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| InputEventAckWaiter input_event_ack_waiter( |
| render_widget_host, blink::WebInputEvent::Type::kTouchStart); |
| |
| SyntheticTapGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.position = gfx::PointF(20, 20); |
| |
| std::unique_ptr<SyntheticTapGesture> gesture = |
| std::make_unique<SyntheticTapGesture>(params); |
| |
| absl::optional<PortalActivatedObserver> predecessor_activated; |
| { |
| PortalCreatedObserver adoption_observer(portal_frame); |
| adoption_observer.set_created_callback(base::BindLambdaForTesting( |
| [&](Portal* portal) { predecessor_activated.emplace(portal); })); |
| |
| PortalActivatedObserver activated_observer(portal); |
| render_widget_host->QueueSyntheticGesture(std::move(gesture), |
| base::DoNothing()); |
| activated_observer.WaitForActivate(); |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| |
| predecessor_activated->WaitForActivate(); |
| // Sanity check to see if the predecessor was reactivated. |
| EXPECT_EQ(web_contents_impl, shell()->web_contents()); |
| |
| // Wait for a touch ack to be sent from the predecessor. |
| input_event_ack_waiter.Wait(); |
| } |
| |
| // TODO(crbug.com/985078): Fix on Mac. |
| // TODO(crbug.com/1191782): Test is flaky. |
| #if !BUILDFLAG(IS_MAC) |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| DISABLED_TouchStateClearedBeforeActivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("document.body.addEventListener('touchstart', e => {" |
| " document.querySelector('portal').activate();" |
| "}, {passive: false});"))); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "window.addEventListener('portalactivate', e => {" |
| " var portal = e.adoptPredecessor();" |
| " document.body.appendChild(portal);" |
| " portal.activate();" |
| "});")); |
| WaitForHitTestData(portal_frame); |
| |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| |
| SyntheticTapGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.position = gfx::PointF(20, 20); |
| |
| // Activate the portal, and then wait for the predecessor to be reactivated. |
| absl::optional<PortalActivatedObserver> adopted_activated; |
| { |
| PortalCreatedObserver adoption_observer(portal_frame); |
| adoption_observer.set_created_callback(base::BindLambdaForTesting( |
| [&](Portal* portal) { adopted_activated.emplace(portal); })); |
| |
| PortalActivatedObserver activated_observer(portal); |
| std::unique_ptr<SyntheticTapGesture> gesture = |
| std::make_unique<SyntheticTapGesture>(params); |
| InputEventAckWaiter input_event_ack_waiter( |
| render_widget_host, blink::WebInputEvent::Type::kTouchCancel); |
| render_widget_host->QueueSyntheticGestureCompleteImmediately( |
| std::move(gesture)); |
| // Wait for synthetic cancel event to be sent. |
| input_event_ack_waiter.Wait(); |
| activated_observer.WaitForActivate(); |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| adopted_activated->WaitForActivate(); |
| // Sanity check to see if the predecessor was reactivated. |
| EXPECT_EQ(web_contents_impl, shell()->web_contents()); |
| |
| InputEventAckWaiter input_event_ack_waiter( |
| render_widget_host, blink::WebInputEvent::Type::kTouchStart); |
| std::unique_ptr<SyntheticTapGesture> gesture = |
| std::make_unique<SyntheticTapGesture>(params); |
| render_widget_host->QueueSyntheticGesture(std::move(gesture), |
| base::DoNothing()); |
| // Waits for touch to be acked. If touch state wasn't cleared before initial |
| // activation, a DCHECK will be hit before the ack is sent. |
| input_event_ack_waiter.Wait(); |
| } |
| #endif |
| |
| // TODO(crbug.com/985078): Fix on Mac. |
| #if !BUILDFLAG(IS_MAC) |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, GestureCleanedUpBeforeActivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| JsReplace("document.body.addEventListener('touchstart', e => {" |
| " document.querySelector('portal').activate();" |
| "}, {once: true});"))); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "window.addEventListener('portalactivate', e => {" |
| " var portal = e.adoptPredecessor();" |
| " document.body.appendChild(portal);" |
| " portal.activate(); " |
| "});")); |
| WaitForHitTestData(portal_frame); |
| |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| |
| SyntheticTapGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.position = gfx::PointF(20, 20); |
| params.duration_ms = 1; |
| |
| // Simulate a tap and activate the portal. |
| absl::optional<PortalActivatedObserver> adopted_activated; |
| { |
| PortalCreatedObserver adoption_observer(portal_frame); |
| adoption_observer.set_created_callback(base::BindLambdaForTesting( |
| [&](Portal* portal) { adopted_activated.emplace(portal); })); |
| |
| PortalActivatedObserver activated_observer(portal); |
| std::unique_ptr<SyntheticTapGesture> gesture = |
| std::make_unique<SyntheticTapGesture>(params); |
| render_widget_host->QueueSyntheticGestureCompleteImmediately( |
| std::move(gesture)); |
| activated_observer.WaitForActivate(); |
| EXPECT_EQ(portal_contents, shell()->web_contents()); |
| |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| |
| // Wait for predecessor to be reactivated. |
| adopted_activated->WaitForActivate(); |
| EXPECT_EQ(web_contents_impl, shell()->web_contents()); |
| |
| // Simulate another tap. |
| InputEventAckWaiter input_event_ack_waiter( |
| render_widget_host, blink::WebInputEvent::Type::kGestureTap); |
| auto gesture = std::make_unique<SyntheticTapGesture>(params); |
| render_widget_host->QueueSyntheticGesture(std::move(gesture), |
| base::DoNothing()); |
| // Wait for the tap gesture ack. If the initial gesture wasn't cleaned up, the |
| // new gesture created will cause an error in the gesture validator. |
| input_event_ack_waiter.Wait(); |
| } |
| #endif |
| |
| // Touch input transfer is only implemented in the content layer for Aura. |
| // TODO(crbug.com/1233183): Flaky on all aura platforms. |
| #if defined(USE_AURA) |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| DISABLED_TouchInputTransferAcrossActivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), |
| embedded_test_server()->GetURL("portal.test", "/portals/scroll.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| |
| GURL portal_url(embedded_test_server()->GetURL( |
| "portal.test", "/portals/scroll-portal.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| WaitForHitTestData(portal_frame); |
| |
| // Create and dispatch a synthetic scroll to trigger activation. |
| SyntheticSmoothScrollGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.anchor = |
| gfx::PointF(50, main_frame->GetView()->GetViewBounds().height() - 100); |
| params.distances.push_back(-gfx::Vector2d(0, 100)); |
| |
| std::unique_ptr<SyntheticSmoothScrollGesture> gesture = |
| std::make_unique<SyntheticSmoothScrollGesture>(params); |
| base::RunLoop run_loop; |
| render_widget_host->QueueSyntheticGesture( |
| std::move(gesture), |
| base::BindLambdaForTesting([&](SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::Result::GESTURE_FINISHED, result); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| |
| // Check if the activated page scrolled. |
| EXPECT_NE(0, EvalJs(portal_frame, "window.scrollY")); |
| } |
| #endif |
| |
| // TODO(crbug.com/1263222): Test fails flakily. |
| // Touch input transfer is only implemented in the content layer for Aura. |
| #if defined(USE_AURA) |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| DISABLED_TouchInputTransferAcrossReactivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "portal.test", |
| "/portals/touch-input-transfer-across-reactivation.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| RenderWidgetHostImpl* render_widget_host = main_frame->GetRenderWidgetHost(); |
| |
| GURL portal_url(embedded_test_server()->GetURL( |
| "portal.test", "/portals/reactivate-predecessor.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WaitForHitTestData(main_frame); |
| |
| PortalActivatedObserver activated_observer(portal); |
| |
| // Create and dispatch a synthetic scroll to trigger activation. |
| SyntheticSmoothScrollGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.anchor = |
| gfx::PointF(50, main_frame->GetView()->GetViewBounds().height() - 100); |
| params.distances.push_back(-gfx::Vector2d(0, 250)); |
| params.speed_in_pixels_s = 200; |
| |
| std::unique_ptr<SyntheticSmoothScrollGesture> gesture = |
| std::make_unique<SyntheticSmoothScrollGesture>(params); |
| base::RunLoop run_loop; |
| render_widget_host->QueueSyntheticGesture( |
| std::move(gesture), |
| base::BindLambdaForTesting([&](SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::Result::GESTURE_FINISHED, result); |
| run_loop.Quit(); |
| })); |
| // Portal should activate when the gesture begins. |
| activated_observer.WaitForActivate(); |
| // Wait till the scroll gesture finishes. |
| run_loop.Run(); |
| // The predecessor should have been reactivated (we should be back to the |
| // starting page). |
| EXPECT_EQ(web_contents_impl, shell()->web_contents()); |
| // The starting page should have scrolled. |
| // NOTE: This assumes that the scroll gesture is long enough that touch events |
| // are still sent after the predecessor is reactivated. |
| int scroll_y_after_portal_activate = |
| EvalJs(main_frame, "scrollYAfterPortalActivate").ExtractInt(); |
| EXPECT_LT(scroll_y_after_portal_activate, |
| EvalJs(main_frame, "window.scrollY")); |
| } |
| #endif |
| |
| // Tests that the outer FrameTreeNode is deleted after activation. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, FrameDeletedAfterActivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| FrameTreeNode* outer_frame_tree_node = FrameTreeNode::GloballyFindByID( |
| portal_contents->GetOuterDelegateFrameTreeNodeId()); |
| EXPECT_TRUE(outer_frame_tree_node); |
| |
| EXPECT_TRUE(ExecJs(portal_contents->GetPrimaryMainFrame(), |
| "window.onportalactivate = e => " |
| "document.body.appendChild(e.adoptPredecessor());")); |
| |
| { |
| FrameDeletedObserver observer(outer_frame_tree_node->current_frame_host()); |
| PortalCreatedObserver portal_created_observer( |
| portal_contents->GetPrimaryMainFrame()); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| observer.Wait(); |
| |
| // Observes the creation of a new portal due to the adoption of the |
| // predecessor during the activate event. |
| // TODO(lfg): We only wait for the adoption callback to avoid a race |
| // receiving a sync IPC in a nested message loop while the browser is |
| // sending out another sync IPC to the GPU process. |
| // https://crbug.com/976367. |
| portal_created_observer.WaitUntilPortalCreated(); |
| } |
| } |
| |
| // Tests that activating a portal at the same time as it is being removed |
| // doesn't crash the browser. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, RemovePortalWhenUnloading) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Create a container for the portal. |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var div = document.createElement('div');" |
| "document.body.appendChild(div);")); |
| |
| // Create portal. |
| PortalCreatedObserver portal_created_observer(main_frame); |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var portal = document.createElement('portal');" |
| "div.appendChild(portal);")); |
| |
| // Add a same-origin iframe in the same div as the portal that activates the |
| // portal on its unload handler. |
| EXPECT_TRUE( |
| ExecJs(main_frame, |
| "var iframe = document.createElement('iframe');" |
| "iframe.src = 'about:blank';" |
| "div.appendChild(iframe);" |
| "iframe.contentWindow.onunload = () => portal.activate();")); |
| |
| // Remove the div from the document. This destroys the portal's WebContents |
| // and should destroy the Portal object as well, so that the activate message |
| // is not processed. |
| EXPECT_TRUE(ExecJs(main_frame, "div.remove();")); |
| } |
| |
| class PortalOrphanedNavigationBrowserTest |
| : public PortalBrowserTest, |
| public ::testing::WithParamInterface<std::tuple<bool, bool>> { |
| public: |
| PortalOrphanedNavigationBrowserTest() |
| : cross_site_(std::get<0>(GetParam())), |
| commit_after_adoption_(std::get<1>(GetParam())) {} |
| |
| // Provides meaningful param names instead of /0, /1, ... |
| static std::string DescribeParams( |
| const ::testing::TestParamInfo<ParamType>& info) { |
| auto [cross_site, commit_after_adoption] = info.param; |
| return base::StringPrintf("%sSite_Commit%sAdoption", |
| cross_site ? "Cross" : "Same", |
| commit_after_adoption ? "After" : "Before"); |
| } |
| |
| protected: |
| bool cross_site() const { return cross_site_; } |
| bool commit_after_adoption() const { return commit_after_adoption_; } |
| |
| private: |
| // Whether the predecessor navigates cross site while orphaned. |
| const bool cross_site_; |
| // Whether the predecessor's navigation commits before or after adoption. |
| const bool commit_after_adoption_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| PortalOrphanedNavigationBrowserTest, |
| ::testing::Combine(::testing::Bool(), |
| ::testing::Bool()), |
| PortalOrphanedNavigationBrowserTest::DescribeParams); |
| |
| // Tests that a portal can navigate while orphaned. |
| IN_PROC_BROWSER_TEST_P(PortalOrphanedNavigationBrowserTest, |
| OrphanedNavigation) { |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| |
| GURL predecessor_nav_url(embedded_test_server()->GetURL( |
| cross_site() ? "b.com" : "portal.test", "/title2.html")); |
| |
| if (commit_after_adoption()) { |
| // Block the activate callback so that there is ample time to start the |
| // navigation while orphaned. |
| // TODO(mcnee): Ideally, we would have a test interceptor to precisely |
| // control when to proceed with adoption. |
| const int adoption_delay = TestTimeouts::tiny_timeout().InMilliseconds(); |
| EXPECT_TRUE( |
| ExecJs(portal->GetPortalContents()->GetPrimaryMainFrame(), |
| JsReplace("window.onportalactivate = e => {" |
| " let end = performance.now() + $1;" |
| " while (performance.now() < end);" |
| " document.body.appendChild(e.adoptPredecessor());" |
| "};", |
| adoption_delay))); |
| } else { |
| // Block the activate callback so that the predecessor portal stays |
| // orphaned. |
| EXPECT_TRUE(ExecJs(portal->GetPortalContents()->GetPrimaryMainFrame(), |
| "window.onportalactivate = e => { while(true) {} };")); |
| } |
| |
| // Activate the portal and navigate the predecessor. |
| PortalActivatedObserver activated_observer(portal); |
| TestNavigationManager navigation_manager(web_contents_impl, |
| predecessor_nav_url); |
| ExecuteScriptAsync(web_contents_impl->GetPrimaryMainFrame(), |
| JsReplace("document.querySelector('portal').activate();" |
| "window.location.href = $1;", |
| predecessor_nav_url)); |
| activated_observer.WaitForActivate(); |
| if (commit_after_adoption()) { |
| ASSERT_TRUE(navigation_manager.WaitForRequestStart()); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWasAdopted, |
| activated_observer.WaitForActivateResult()); |
| } |
| navigation_manager.WaitForNavigationFinished(); |
| EXPECT_TRUE(navigation_manager.was_successful()); |
| } |
| |
| // Tests that the browser doesn't crash if the renderer tries to create the |
| // PortalHost after the parent renderer dropped the portal. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| AccessPortalHostAfterPortalDestruction) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| |
| // Simulate the portal being dropped, but not the destruction of the |
| // WebContents. |
| web_contents_impl->GetPrimaryMainFrame()->DestroyPortal(portal); |
| |
| // Get the portal renderer to access the WebContents. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(portal_frame->GetProcess()); |
| ExecuteScriptAsync(portal_frame, "window.portalHost.postMessage('message');"); |
| EXPECT_EQ(bad_message::RPH_MOJO_PROCESS_ERROR, kill_waiter.Wait()); |
| } |
| |
| // Tests that activation early in navigation fails. Even though the navigation |
| // hasn't yet committed, allowing activation could allow a portal to prevent |
| // the user from navigating away. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, ActivateEarlyInNavigation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title2.html"); |
| CreatePortalToUrl(web_contents_impl, url); |
| |
| // Install a beforeunload handler. |
| main_frame->SuddenTerminationDisablerChanged( |
| true, blink::mojom::SuddenTerminationDisablerType::kBeforeUnloadHandler); |
| |
| // Have the outer page try to navigate away but stop it early in the request, |
| // where it is still possible to stop. |
| GURL destination = |
| embedded_test_server()->GetURL("portal.test", "/title3.html"); |
| TestNavigationObserver navigation_observer(web_contents_impl); |
| NavigationHandleObserver handle_observer(web_contents_impl, destination); |
| TestNavigationManager navigation_manager(web_contents_impl, destination); |
| NavigationController::LoadURLParams params(destination); |
| params.transition_type = ui::PageTransitionFromInt( |
| ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| web_contents_impl->GetController().LoadURLWithParams(params); |
| ASSERT_TRUE(navigation_manager.WaitForRequestStart()); |
| |
| // Then activate the portal, because navigation has begun and beforeunload |
| // has been dispatched, the activation should fail. |
| EvalJsResult result = EvalJs(main_frame, |
| "document.querySelector('portal').activate()" |
| ".then(() => 'success', e => e.message)"); |
| EXPECT_THAT(result.ExtractString(), |
| ::testing::HasSubstr("Cannot activate portal while document is in" |
| " beforeunload or has started unloading")); |
| |
| // The navigation should commit properly thereafter. |
| navigation_manager.WaitForNavigationFinished(); |
| navigation_observer.Wait(); |
| EXPECT_EQ(web_contents_impl, shell()->web_contents()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(destination, navigation_observer.last_navigation_url()); |
| } |
| |
| // Tests that activation late in navigation is rejected (since it's too late to |
| // stop the navigation). |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, ActivateLateInNavigation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title2.html"); |
| CreatePortalToUrl(web_contents_impl, url); |
| |
| // Install a beforeunload handler. |
| main_frame->SuddenTerminationDisablerChanged( |
| true, blink::mojom::SuddenTerminationDisablerType::kBeforeUnloadHandler); |
| |
| // Have the outer page try to navigate away and reach the point where it's |
| // about to process the response (after which it will commit). It is too late |
| // to abort the navigation. |
| GURL destination = |
| embedded_test_server()->GetURL("portal.test", "/title3.html"); |
| TestNavigationObserver navigation_observer(web_contents_impl); |
| TestNavigationManager navigation_manager(web_contents_impl, destination); |
| NavigationController::LoadURLParams params(destination); |
| params.transition_type = ui::PageTransitionFromInt( |
| ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| web_contents_impl->GetController().LoadURLWithParams(params); |
| ASSERT_TRUE(navigation_manager.WaitForResponse()); |
| |
| // Then activate the portal. Since this is late in navigation, we expect the |
| // activation to fail. Since commit hasn't actually happened yet, though, |
| // there is time for the renderer to process the promise rejection. |
| EvalJsResult result = EvalJs(main_frame, |
| "document.querySelector('portal').activate()" |
| ".then(() => 'success', e => e.message)"); |
| EXPECT_THAT(result.ExtractString(), |
| ::testing::HasSubstr("Cannot activate portal while document is in" |
| " beforeunload or has started unloading")); |
| |
| // The navigation should commit properly thereafter. |
| navigation_manager.ResumeNavigation(); |
| navigation_observer.Wait(); |
| EXPECT_EQ(web_contents_impl, shell()->web_contents()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(destination, navigation_observer.last_navigation_url()); |
| } |
| |
| namespace { |
| |
| class LocalMainFrameInterceptorBadPortalActivateResult |
| : public blink::mojom::LocalMainFrameInterceptorForTesting { |
| public: |
| explicit LocalMainFrameInterceptorBadPortalActivateResult( |
| RenderFrameHostImpl* frame_host) |
| : frame_host_(frame_host) {} |
| |
| blink::mojom::LocalMainFrame* GetForwardingInterface() override { |
| if (!local_main_frame_) |
| frame_host_->GetRemoteAssociatedInterfaces()->GetInterface( |
| &local_main_frame_); |
| return local_main_frame_.get(); |
| } |
| |
| void OnPortalActivated( |
| const blink::PortalToken& portal_token, |
| mojo::PendingAssociatedRemote<blink::mojom::Portal> portal, |
| mojo::PendingAssociatedReceiver<blink::mojom::PortalClient> portal_client, |
| blink::TransferableMessage data, |
| uint64_t trace_id, |
| OnPortalActivatedCallback callback) override { |
| GetForwardingInterface()->OnPortalActivated( |
| portal_token, std::move(portal), std::move(portal_client), |
| std::move(data), trace_id, |
| base::BindOnce( |
| [](OnPortalActivatedCallback callback, |
| blink::mojom::PortalActivateResult) { |
| // Replace the true result with one that the renderer is not |
| // allowed to send. |
| std::move(callback).Run(blink::mojom::PortalActivateResult:: |
| kRejectedDueToPredecessorNavigation); |
| }, |
| std::move(callback))); |
| } |
| |
| private: |
| raw_ptr<RenderFrameHostImpl> frame_host_; |
| mojo::AssociatedRemote<blink::mojom::LocalMainFrame> local_main_frame_; |
| }; |
| |
| class RenderFrameHostImplForLocalMainFrameInterceptor |
| : public RenderFrameHostImpl { |
| private: |
| using RenderFrameHostImpl::RenderFrameHostImpl; |
| |
| blink::mojom::LocalMainFrame* GetAssociatedLocalMainFrame() final { |
| return &interceptor_; |
| } |
| |
| LocalMainFrameInterceptorBadPortalActivateResult interceptor_{this}; |
| |
| friend class RenderFrameHostFactoryForLocalMainFrameInterceptor; |
| }; |
| |
| class RenderFrameHostFactoryForLocalMainFrameInterceptor |
| : public TestRenderFrameHostFactory { |
| protected: |
| std::unique_ptr<RenderFrameHostImpl> CreateRenderFrameHost( |
| SiteInstance* site_instance, |
| scoped_refptr<RenderViewHostImpl> render_view_host, |
| RenderFrameHostDelegate* delegate, |
| FrameTree* frame_tree, |
| FrameTreeNode* frame_tree_node, |
| int32_t routing_id, |
| mojo::PendingAssociatedRemote<mojom::Frame> frame_remote, |
| const blink::LocalFrameToken& frame_token, |
| bool renderer_initiated_creation, |
| RenderFrameHostImpl::LifecycleStateImpl lifecycle_state, |
| scoped_refptr<BrowsingContextState> browsing_context_state) override { |
| return base::WrapUnique(new RenderFrameHostImplForLocalMainFrameInterceptor( |
| site_instance, std::move(render_view_host), delegate, frame_tree, |
| frame_tree_node, routing_id, std::move(frame_remote), frame_token, |
| renderer_initiated_creation, lifecycle_state, |
| std::move(browsing_context_state), |
| frame_tree_node->frame_owner_element_type())); |
| } |
| }; |
| |
| } // namespace |
| |
| // Tests that the browser filters the renderer's replies to the portal |
| // activation event and terminates misbehaving renderers. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, MisbehavingRendererActivated) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Arrange for a special kind of RenderFrameHost to be created which permits |
| // its NavigationControl messages to be intercepted. |
| RenderFrameHostFactoryForLocalMainFrameInterceptor scoped_rfh_factory; |
| GURL url = embedded_test_server()->GetURL("a.com", "/title2.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| |
| // Then activate the portal. Due to the apparent misbehavior from the |
| // renderer, the caller should be informed that it was aborted due to a bug, |
| // and the portal's renderer (having been framed for the crime) should be |
| // killed. |
| PortalActivatedObserver activated_observer(portal); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| portal->GetPortalContents()->GetPrimaryMainFrame()->GetProcess()); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kAbortedDueToBug, |
| activated_observer.WaitForActivateResult()); |
| EXPECT_EQ(bad_message::RPH_MOJO_PROCESS_ERROR, kill_waiter.Wait()); |
| } |
| |
| // Test that user facing session history is retained after a portal activation. |
| // Before activation, we have the host WebContents's navigation entries, which |
| // is the session history presented to the user, plus the portal WebContents's |
| // navigation entry, which is not presented as part of that session history. |
| // Upon activation, the host WebContents's navigation entries are merged into |
| // the activated portal's WebContents. The resulting session history in the |
| // activated WebContents is presented to the user. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, PortalHistoryWithActivation) { |
| // We have an additional navigation entry in the portal host's contents, so |
| // that we test that we're retaining more than just the last committed entry |
| // of the portal host. |
| GURL previous_main_frame_url( |
| embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), previous_main_frame_url)); |
| |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title2.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| NavigationControllerImpl& main_controller = |
| web_contents_impl->GetController(); |
| NavigationControllerImpl& portal_controller = |
| portal_contents->GetController(); |
| |
| EXPECT_EQ(2, main_controller.GetEntryCount()); |
| ASSERT_TRUE(main_controller.GetLastCommittedEntry()); |
| EXPECT_EQ(main_url, main_controller.GetLastCommittedEntry()->GetURL()); |
| EXPECT_EQ(1, portal_controller.GetEntryCount()); |
| ASSERT_TRUE(portal_controller.GetLastCommittedEntry()); |
| EXPECT_EQ(portal_url, portal_controller.GetLastCommittedEntry()->GetURL()); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| activated_observer.WaitForActivate(); |
| |
| NavigationControllerImpl& activated_controller = portal_controller; |
| |
| ASSERT_EQ(3, activated_controller.GetEntryCount()); |
| ASSERT_EQ(2, activated_controller.GetLastCommittedEntryIndex()); |
| EXPECT_EQ(previous_main_frame_url, |
| activated_controller.GetEntryAtIndex(0)->GetURL()); |
| EXPECT_EQ(main_url, activated_controller.GetEntryAtIndex(1)->GetURL()); |
| EXPECT_EQ(portal_url, activated_controller.GetEntryAtIndex(2)->GetURL()); |
| } |
| |
| // Test that we may go back/forward across a portal activation as though it |
| // were a regular navigation. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| PortalHistoryActivateAndGoBackForward) { |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| activated_observer.WaitForActivate(); |
| |
| web_contents_impl = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| NavigationControllerImpl& controller = web_contents_impl->GetController(); |
| |
| ASSERT_TRUE(controller.CanGoBack()); |
| TestNavigationObserver go_back_observer(web_contents_impl); |
| controller.GoBack(); |
| go_back_observer.Wait(); |
| // These back/forward navigations do not involve a contents swap, since the |
| // original contents is gone as it was not adopted. |
| ASSERT_EQ(web_contents_impl, shell()->web_contents()); |
| |
| ASSERT_EQ(2, controller.GetEntryCount()); |
| ASSERT_EQ(0, controller.GetLastCommittedEntryIndex()); |
| EXPECT_EQ(main_url, controller.GetEntryAtIndex(0)->GetURL()); |
| EXPECT_EQ(portal_url, controller.GetEntryAtIndex(1)->GetURL()); |
| |
| ASSERT_TRUE(controller.CanGoForward()); |
| TestNavigationObserver go_forward_observer(web_contents_impl); |
| controller.GoForward(); |
| go_forward_observer.Wait(); |
| ASSERT_EQ(web_contents_impl, shell()->web_contents()); |
| |
| ASSERT_EQ(2, controller.GetEntryCount()); |
| ASSERT_EQ(1, controller.GetLastCommittedEntryIndex()); |
| EXPECT_EQ(main_url, controller.GetEntryAtIndex(0)->GetURL()); |
| EXPECT_EQ(portal_url, controller.GetEntryAtIndex(1)->GetURL()); |
| } |
| |
| // Activation does not cancel new pending navigations in portals. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| ActivationWithPortalPendingNavigation) { |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| NavigationControllerImpl& portal_controller = |
| portal_contents->GetController(); |
| |
| // Have the portal navigate so that we have a pending navigation. |
| GURL pending_url(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| TestNavigationManager pending_navigation(portal_contents, pending_url); |
| EXPECT_TRUE(ExecJs( |
| main_frame, |
| JsReplace("document.querySelector('portal').src = $1;", pending_url))); |
| EXPECT_TRUE(pending_navigation.WaitForRequestStart()); |
| |
| // Navigating via frame proxy does not create a pending NavigationEntry. We'll |
| // check for an ongoing NavigationRequest instead. |
| FrameTreeNode* portal_node = |
| portal_contents->GetPrimaryMainFrame()->frame_tree_node(); |
| NavigationRequest* navigation_request = portal_node->navigation_request(); |
| ASSERT_TRUE(navigation_request); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| activated_observer.WaitForActivate(); |
| |
| NavigationControllerImpl& activated_controller = portal_controller; |
| |
| pending_navigation.WaitForNavigationFinished(); |
| ASSERT_EQ(2, activated_controller.GetEntryCount()); |
| ASSERT_EQ(1, activated_controller.GetLastCommittedEntryIndex()); |
| EXPECT_EQ(main_url, activated_controller.GetEntryAtIndex(0)->GetURL()); |
| EXPECT_EQ(pending_url, activated_controller.GetEntryAtIndex(1)->GetURL()); |
| } |
| |
| namespace { |
| |
| // Returns which RenderFrameHost is focused within a given portal's frame tree. |
| RenderFrameHostImpl* GetFocusedFrameWithinPortalFrameTree( |
| WebContentsImpl* portal_contents) { |
| FrameTreeNode* focused_node = |
| portal_contents->GetPrimaryFrameTree().GetFocusedFrame(); |
| if (!focused_node) |
| return nullptr; |
| return focused_node->current_frame_host(); |
| } |
| |
| } // namespace |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, DidFocusIPCFromFrameInsidePortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_main_frame = |
| portal_contents->GetPrimaryMainFrame(); |
| |
| TestNavigationObserver iframe_navigation_observer(portal_contents); |
| EXPECT_TRUE(ExecJs(portal_main_frame, |
| JsReplace("var iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| url))); |
| iframe_navigation_observer.Wait(); |
| EXPECT_EQ(url, iframe_navigation_observer.last_navigation_url()); |
| |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), web_contents_impl); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), main_frame); |
| EXPECT_EQ(portal_contents->GetFocusedFrame(), nullptr); |
| EXPECT_EQ(GetFocusedFrameWithinPortalFrameTree(portal_contents), nullptr); |
| |
| // Simulate renderer sending LocalFrameHost::DidFocusFrame IPC. |
| RenderFrameHostImpl* iframe = |
| portal_main_frame->child_at(0)->current_frame_host(); |
| iframe->DidFocusFrame(); |
| |
| // WebContents focus should not have changed, but portal's frame tree's |
| // focused frame should have updated. |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), web_contents_impl); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), main_frame); |
| EXPECT_EQ(portal_contents->GetFocusedFrame(), nullptr); |
| EXPECT_EQ(GetFocusedFrameWithinPortalFrameTree(portal_contents), iframe); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| DidFocusIPCFromCrossProcessFrameInsidePortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| // Ensures b.com is isolated from a.com (even on Android). |
| IsolateOriginsForTesting(embedded_test_server(), portal_contents, {"b.com"}); |
| RenderFrameHostImpl* portal_main_frame = |
| portal_contents->GetPrimaryMainFrame(); |
| |
| TestNavigationObserver iframe_navigation_observer(portal_contents); |
| GURL b_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| EXPECT_TRUE(ExecJs(portal_main_frame, |
| JsReplace("var iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| b_url))); |
| iframe_navigation_observer.Wait(); |
| EXPECT_EQ(b_url, iframe_navigation_observer.last_navigation_url()); |
| |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), web_contents_impl); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), main_frame); |
| EXPECT_EQ(portal_contents->GetFocusedFrame(), nullptr); |
| EXPECT_EQ(GetFocusedFrameWithinPortalFrameTree(portal_contents), nullptr); |
| |
| FrameTreeNode* iframe_ftn = portal_main_frame->child_at(0); |
| RenderFrameHostImpl* rfhi = iframe_ftn->current_frame_host(); |
| RenderFrameProxyHost* rfph = |
| rfhi->browsing_context_state()->GetRenderFrameProxyHost( |
| portal_main_frame->GetSiteInstance()->group()); |
| |
| // Simulate renderer sending RemoteFrameHost::DidFocusFrame IPC. |
| rfph->DidFocusFrame(); |
| |
| // WebContents focus should not have changed, but portal's frame tree's |
| // focused frame should have updated. |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), web_contents_impl); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), main_frame); |
| EXPECT_EQ(portal_contents->GetFocusedFrame(), nullptr); |
| EXPECT_EQ(GetFocusedFrameWithinPortalFrameTree(portal_contents), rfhi); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, DidFocusIPCFromOrphanedPortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_main_frame = |
| portal_contents->GetPrimaryMainFrame(); |
| |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), web_contents_impl); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), main_frame); |
| EXPECT_EQ(portal_contents->GetFocusedFrame(), nullptr); |
| EXPECT_EQ(GetFocusedFrameWithinPortalFrameTree(portal_contents), nullptr); |
| EXPECT_TRUE(main_frame->GetRenderWidgetHost()->is_focused()); |
| |
| // Activate portal, keep in orphaned state for a while, and then adopt and |
| // insert predecessor. |
| // TODO(mcnee): Ideally, we would have a test interceptor to precisely |
| // control when to proceed with adoption. |
| const int time_in_orphaned_state = |
| TestTimeouts::tiny_timeout().InMilliseconds(); |
| EXPECT_TRUE( |
| ExecJs(portal_main_frame, |
| JsReplace("window.addEventListener('portalactivate', e => { " |
| " var stop = performance.now() + $1;" |
| " while (performance.now() < stop) {}" |
| " var portal = e.adoptPredecessor(); " |
| " document.body.appendChild(portal);" |
| "});", |
| time_in_orphaned_state))); |
| { |
| PortalActivatedObserver activated_observer(portal); |
| PortalCreatedObserver adoption_observer(portal_main_frame); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| activated_observer.WaitForActivate(); |
| |
| // |web_contents_impl| should be owned by an orphaned portal. |
| EXPECT_TRUE(web_contents_impl->IsPortal()); |
| EXPECT_EQ(web_contents_impl->GetOuterWebContents(), nullptr); |
| |
| // |web_contents_impl| is orphaned and therefore still points to itself |
| // as the focused WebContents node in its tree. It shouldn't have a view |
| // and it shouldn't have page focus. |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), web_contents_impl); |
| EXPECT_EQ(web_contents_impl->GetRenderWidgetHostView(), nullptr); |
| EXPECT_FALSE(main_frame->GetRenderWidgetHost()->is_focused()); |
| |
| // Simulate DidFocusFrame IPC being sent from the renderer while orphaned. |
| main_frame->DidFocusFrame(); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), main_frame); |
| EXPECT_FALSE(main_frame->GetRenderWidgetHost()->is_focused()); |
| |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWasAdopted, |
| activated_observer.WaitForActivateResult()); |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| |
| // Adoption is complete, so |web_contents_impl_| is no longer orphaned and is |
| // an inner WebContents. |
| EXPECT_EQ(web_contents_impl->GetFocusedWebContents(), portal_contents); |
| EXPECT_EQ(web_contents_impl->GetFocusedFrame(), nullptr); |
| EXPECT_EQ(GetFocusedFrameWithinPortalFrameTree(web_contents_impl), |
| main_frame); |
| EXPECT_FALSE(main_frame->GetRenderWidgetHost()->is_focused()); |
| } |
| |
| // Test that a renderer process is killed if it sends an AdvanceFocus IPC to |
| // advance focus into a portal. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, AdvanceFocusIntoPortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_main_frame = |
| portal_contents->GetPrimaryMainFrame(); |
| |
| RenderFrameProxyHost* outer_delegate_proxy = |
| portal_main_frame->frame_tree_node() |
| ->render_manager() |
| ->GetProxyToOuterDelegate(); |
| RenderProcessHostBadIpcMessageWaiter rph_kill_waiter( |
| main_frame->GetProcess()); |
| outer_delegate_proxy->AdvanceFocus(blink::mojom::FocusType::kNone, |
| main_frame->GetFrameToken()); |
| absl::optional<bad_message::BadMessageReason> result = rph_kill_waiter.Wait(); |
| EXPECT_TRUE(result.has_value()); |
| EXPECT_EQ(result.value(), bad_message::RFPH_ADVANCE_FOCUS_INTO_PORTAL); |
| } |
| |
| namespace { |
| void WaitForAccessibilityTree(WebContents* web_contents) { |
| AccessibilityNotificationWaiter waiter(web_contents, ui::kAXModeComplete, |
| ax::mojom::Event::kNone); |
| ASSERT_TRUE(waiter.WaitForNotification()); |
| } |
| } // namespace |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| RootAccessibilityManagerShouldUpdateAfterActivation) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| WaitForAccessibilityTree(web_contents_impl); |
| |
| // Create portal. |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| WaitForAccessibilityTree(portal_contents); |
| if (!main_frame->browser_accessibility_manager() || |
| !portal_frame->browser_accessibility_manager()->GetRootManager()) |
| WaitForAccessibilityTree(web_contents_impl); |
| |
| EXPECT_NE(nullptr, portal_frame->browser_accessibility_manager()); |
| EXPECT_EQ(main_frame->browser_accessibility_manager(), |
| portal_frame->browser_accessibility_manager()->GetRootManager()); |
| // Activate portal and adopt predecessor. |
| EXPECT_TRUE(ExecJs(portal_frame, |
| "window.addEventListener('portalactivate', e => { " |
| " var portal = e.adoptPredecessor(); " |
| " document.body.appendChild(portal); " |
| "});")); |
| { |
| PortalActivatedObserver activated_observer(portal); |
| PortalCreatedObserver adoption_observer(portal_frame); |
| ExecuteScriptAsync(main_frame, |
| "let portal = document.querySelector('portal');" |
| "portal.activate().then(() => { " |
| " document.body.removeChild(portal); " |
| "});"); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWasAdopted, |
| activated_observer.WaitForActivateResult()); |
| adoption_observer.WaitUntilPortalCreated(); |
| } |
| |
| EXPECT_EQ(portal_frame->browser_accessibility_manager()->GetRootManager(), |
| portal_frame->browser_accessibility_manager()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, OrphanedPortalAccessibilityReset) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Create portal. |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| |
| // Activate portal, keep in orphaned state for a while, and then adopt and |
| // insert predecessor. |
| // TODO(mcnee): Ideally, we would have a test interceptor to precisely |
| // control when to proceed with adoption. |
| const int TIME_IN_ORPHANED_STATE_MILLISECONDS = 2000; |
| EXPECT_TRUE( |
| ExecJs(portal_frame, |
| JsReplace("window.addEventListener('portalactivate', e => { " |
| " var stop = performance.now() + $1;" |
| " while (performance.now() < stop) {}" |
| " var portal = e.adoptPredecessor(); " |
| " document.body.appendChild(portal);" |
| "});", |
| TIME_IN_ORPHANED_STATE_MILLISECONDS))); |
| { |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, R"( |
| let portal = document.querySelector('portal'); |
| portal.activate(); |
| )"); |
| activated_observer.WaitForActivate(); |
| // Forces an AXTree update to be sent while portal is orphaned. |
| AccessibilityNotificationWaiter waiter(web_contents_impl, |
| ui::kAXModeComplete, |
| ax::mojom::Event::kLayoutComplete); |
| ASSERT_TRUE(waiter.WaitForNotification()); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWasAdopted, |
| activated_observer.WaitForActivateResult()); |
| ASSERT_TRUE(waiter.WaitForNotification()); |
| } |
| EXPECT_EQ(0, main_frame->accessibility_fatal_error_count_for_testing()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| OrphanedPortalActivateAccessibilityReset) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| // Create portal. |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| |
| // Activate portal, keep in predecessor in orphaned state for a while, |
| // then adopt and activate predecessor. |
| // TODO(mcnee): Ideally, we would have a test interceptor to precisely |
| // control when to proceed with adoption. |
| const int TIME_IN_ORPHANED_STATE_MILLISECONDS = 2000; |
| EXPECT_TRUE( |
| ExecJs(portal_frame, |
| JsReplace("window.addEventListener('portalactivate', e => { " |
| " var stop = performance.now() + $1;" |
| " while (performance.now() < stop) {}" |
| " e.adoptPredecessor().activate(); " |
| "});", |
| TIME_IN_ORPHANED_STATE_MILLISECONDS))); |
| { |
| PortalActivatedObserver activated_observer(portal); |
| PortalCreatedObserver adoption_observer(main_frame); |
| ExecuteScriptAsync(main_frame, R"( |
| window.addEventListener('portalactivate', e => { |
| document.body.appendChild(e.adoptPredecessor()); |
| }); |
| |
| let portal = document.querySelector('portal'); |
| portal.activate().then(() => { |
| document.body.removeChild(portal); |
| }); |
| )"); |
| activated_observer.WaitForActivate(); |
| // Forces an AXTree update to be sent while portal is orphaned. |
| AccessibilityNotificationWaiter waiter(web_contents_impl, |
| ui::kAXModeComplete, |
| ax::mojom::Event::kLayoutComplete); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWasAdopted, |
| activated_observer.WaitForActivateResult()); |
| adoption_observer.WaitUntilPortalCreated(); |
| ASSERT_TRUE(waiter.WaitForNotification()); |
| } |
| EXPECT_EQ(0, main_frame->accessibility_fatal_error_count_for_testing()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| CrossSitePortalNavCommitsAfterActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| |
| TestNavigationObserver nav_observer(portal->GetPortalContents()); |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| // Start a cross site navigation in the portal contents and immediately |
| // activate the portal. The navigation starts in a child frame but commits |
| // after the frame becomes the root frame. |
| ExecuteScriptAsync(main_frame, |
| JsReplace("let portal = document.querySelector('portal');" |
| "portal.src = $1;" |
| "portal.activate();", |
| b_url)); |
| nav_observer.Wait(); |
| } |
| |
| // This is similar to CrossSitePortalNavCommitsAfterActivation, but we navigate |
| // an adopted predecessor that hasn't been attached and the navigation commits |
| // after the predecessor is reactivated. |
| IN_PROC_BROWSER_TEST_F( |
| PortalBrowserTest, |
| CrossSitePortalNavInUnattachedPredecessorCommitsAfterActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_frame = portal_contents->GetPrimaryMainFrame(); |
| |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE( |
| ExecJs(portal_frame, |
| JsReplace("window.addEventListener('portalactivate', (e) => {" |
| " let predecessor = e.adoptPredecessor();" |
| " predecessor.src = $1;" |
| " predecessor.activate();" |
| "});", |
| b_url))); |
| |
| TestNavigationObserver nav_observer(web_contents_impl); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| nav_observer.Wait(); |
| } |
| |
| // Ensure portal activations respect navigation precedence. If there is an |
| // ongoing browser initiated navigation, then a portal activation without user |
| // activation cannot proceed. |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, NavigationPrecedence) { |
| GURL main_url1(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| GURL main_url2(embedded_test_server()->GetURL("portal.test", "/title2.html")); |
| GURL main_url3(embedded_test_server()->GetURL("portal.test", "/title3.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url1)); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url2)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| CreatePortalToUrl(web_contents_impl, portal_url); |
| |
| TestNavigationManager pending_navigation(web_contents_impl, main_url3); |
| shell()->LoadURL(main_url3); |
| EXPECT_TRUE(pending_navigation.WaitForRequestStart()); |
| |
| EXPECT_EQ("reject", EvalJs(main_frame, |
| "document.querySelector('portal').activate().then(" |
| " () => 'resolve', () => 'reject');", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| pending_navigation.WaitForNavigationFinished(); |
| EXPECT_TRUE(pending_navigation.was_successful()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, RejectActivationOfErrorPages) { |
| net::EmbeddedTestServer bad_https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| bad_https_server.AddDefaultHandlers(GetTestDataFilePath()); |
| bad_https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED); |
| ASSERT_TRUE(bad_https_server.Start()); |
| |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL bad_portal_url(bad_https_server.GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, bad_portal_url, |
| /* number_of_navigations */ 1, |
| /* expected_to_succeed */ false); |
| |
| PortalActivatedObserver activated_observer(portal); |
| EXPECT_EQ("reject", EvalJs(main_frame, |
| "document.querySelector('portal').activate().then(" |
| " () => 'resolve', () => 'reject');")); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kRejectedDueToErrorInPortal, |
| activated_observer.result()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, |
| RejectActivationOfPostCommitErrorPages) { |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| |
| std::string error_html = "Error page"; |
| TestNavigationObserver error_observer(portal_contents); |
| portal_contents->GetController().LoadPostCommitErrorPage( |
| portal_contents->GetPrimaryMainFrame(), portal_url, error_html, |
| net::ERR_BLOCKED_BY_CLIENT); |
| error_observer.Wait(); |
| EXPECT_FALSE(error_observer.last_navigation_succeeded()); |
| EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code()); |
| |
| PortalActivatedObserver activated_observer(portal); |
| EXPECT_EQ("reject", EvalJs(main_frame, |
| "document.querySelector('portal').activate().then(" |
| " () => 'resolve', () => 'reject');")); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kRejectedDueToErrorInPortal, |
| activated_observer.result()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, RejectActivationOfCrashedPages) { |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| CrashTab(portal_contents); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kRejectedDueToErrorInPortal, |
| activated_observer.WaitForActivateResult()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, ActivatePreviouslyCrashedPortal) { |
| GURL main_url(embedded_test_server()->GetURL("portal.test", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL portal_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, portal_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| CrashTab(portal_contents); |
| |
| TestNavigationObserver navigation_observer(portal_contents); |
| EXPECT_TRUE(ExecJs( |
| main_frame, |
| JsReplace("document.querySelector('portal').src = $1;", portal_url))); |
| navigation_observer.Wait(); |
| |
| PortalActivatedObserver activated_observer(portal); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| EXPECT_EQ(blink::mojom::PortalActivateResult::kPredecessorWillUnload, |
| activated_observer.WaitForActivateResult()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, CallCreateProxyAndAttachPortalTwice) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| PortalInterceptorForTesting* portal_interceptor = |
| PortalInterceptorForTesting::From(portal); |
| // Hijacks navigate request and calls CreateProxyAndAttachPortal instead. |
| portal_interceptor->SetNavigateCallback(base::BindRepeating( |
| [](Portal* portal, const GURL& url, blink::mojom::ReferrerPtr referrer, |
| blink::mojom::Portal::NavigateCallback callback) { |
| // Create stub RemoteFrameInterfaces. |
| auto remote_frame_interfaces = |
| blink::mojom::RemoteFrameInterfacesFromRenderer::New(); |
| remote_frame_interfaces->frame_host_receiver = |
| mojo::AssociatedRemote<blink::mojom::RemoteFrameHost>() |
| .BindNewEndpointAndPassDedicatedReceiver(); |
| mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame; |
| std::ignore = frame.BindNewEndpointAndPassDedicatedReceiver(); |
| remote_frame_interfaces->frame = frame.Unbind(); |
| |
| portal->CreateProxyAndAttachPortal(std::move(remote_frame_interfaces)); |
| std::move(callback).Run(); |
| }, |
| portal)); |
| |
| RenderProcessHostBadIpcMessageWaiter rph_kill_waiter( |
| main_frame->GetProcess()); |
| GURL dummy_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| ExecuteScriptAsync( |
| main_frame, |
| JsReplace("document.querySelector('portal').src = $1", dummy_url)); |
| EXPECT_EQ(bad_message::RPH_MOJO_PROCESS_ERROR, rph_kill_waiter.Wait()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, CrossSiteActivationReusingRVH) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| LeaveInPendingDeletionState(portal_contents->GetPrimaryMainFrame()); |
| |
| // Navigate portal to b.com. |
| TestNavigationObserver b_nav_observer(portal_contents); |
| EXPECT_TRUE(ExecJs(portal_contents->GetPrimaryMainFrame(), |
| JsReplace("location.href = $1;", b_url))); |
| b_nav_observer.Wait(); |
| |
| // Navigate portal to a.com once activated. |
| EXPECT_TRUE( |
| ExecJs(portal_contents->GetPrimaryMainFrame(), |
| JsReplace("window.addEventListener('portalactivate', (e) => {" |
| " location.href = $1;" |
| "});", |
| a_url))); |
| |
| TestNavigationObserver nav_observer(portal_contents); |
| ExecuteScriptAsync(web_contents_impl->GetPrimaryMainFrame(), |
| "document.querySelector('portal').activate();"); |
| nav_observer.Wait(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| } |
| |
| class PortalOOPIFBrowserTest : public PortalBrowserTest { |
| protected: |
| PortalOOPIFBrowserTest() {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| IsolateAllSitesForTesting(command_line); |
| } |
| }; |
| |
| // Tests that creating and destroying OOPIFs inside the portal works as |
| // intended. |
| IN_PROC_BROWSER_TEST_F(PortalOOPIFBrowserTest, OOPIFInsidePortal) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, a_url); |
| WebContentsImpl* portal_contents = portal->GetPortalContents(); |
| RenderFrameHostImpl* portal_main_frame = |
| portal_contents->GetPrimaryMainFrame(); |
| |
| // Add an out-of-process iframe to the portal. |
| GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| TestNavigationObserver iframe_navigation_observer(portal_contents); |
| EXPECT_TRUE(ExecJs(portal_main_frame, |
| JsReplace("var iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| b_url))); |
| iframe_navigation_observer.Wait(); |
| EXPECT_EQ(b_url, iframe_navigation_observer.last_navigation_url()); |
| RenderFrameHostImpl* portal_iframe = |
| portal_main_frame->child_at(0)->current_frame_host(); |
| EXPECT_NE(portal_main_frame->GetSiteInstance(), |
| portal_iframe->GetSiteInstance()); |
| |
| // Remove the OOPIF from the portal. |
| RenderFrameDeletedObserver deleted_observer(portal_iframe); |
| EXPECT_TRUE( |
| ExecJs(portal_main_frame, "document.querySelector('iframe').remove();")); |
| deleted_observer.WaitUntilDeleted(); |
| } |
| |
| namespace { |
| |
| class DownloadObserver : public DownloadManager::Observer { |
| public: |
| DownloadObserver() |
| : manager_(ShellContentBrowserClient::Get() |
| ->browser_context() |
| ->GetDownloadManager()) { |
| manager_->AddObserver(this); |
| } |
| |
| ~DownloadObserver() override { |
| if (manager_) |
| manager_->RemoveObserver(this); |
| } |
| |
| ::testing::AssertionResult DownloadObserved() { |
| if (download_url_.is_empty()) |
| return ::testing::AssertionFailure() << "no download observed"; |
| return ::testing::AssertionSuccess() |
| << "download observed: " << download_url_; |
| } |
| |
| ::testing::AssertionResult AwaitDownload() { |
| if (download_url_.is_empty() && !dropped_download_) { |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| quit_closure_.Reset(); |
| } |
| return DownloadObserved(); |
| } |
| |
| // DownloadManager::Observer |
| |
| void ManagerGoingDown(DownloadManager* manager) override { |
| DCHECK_EQ(manager_, manager); |
| manager_->RemoveObserver(this); |
| manager_ = nullptr; |
| } |
| |
| void OnDownloadCreated(DownloadManager* manager, |
| download::DownloadItem* item) override { |
| DCHECK_EQ(manager_, manager); |
| if (download_url_.is_empty()) { |
| download_url_ = item->GetURL(); |
| if (!quit_closure_.is_null()) |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| void OnDownloadDropped(DownloadManager* manager) override { |
| DCHECK_EQ(manager_, manager); |
| dropped_download_ = true; |
| if (!quit_closure_.is_null()) |
| std::move(quit_closure_).Run(); |
| } |
| |
| private: |
| raw_ptr<DownloadManager> manager_; |
| bool dropped_download_ = false; |
| GURL download_url_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| } // namespace |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, DownloadsBlockedInMainFrame) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents_impl, |
| embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| CreatePortalToUrl(web_contents_impl, embedded_test_server()->GetURL( |
| "portal.test", "/title2.html")); |
| |
| GURL download_url = embedded_test_server()->GetURL( |
| "portal.test", "/set-header?Content-Disposition: attachment"); |
| |
| DownloadObserver download_observer; |
| EXPECT_TRUE(ExecJs( |
| web_contents_impl, |
| JsReplace("document.querySelector('portal').src = $1", download_url))); |
| EXPECT_FALSE(download_observer.AwaitDownload()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, DownloadsBlockedInSubframe) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents_impl, |
| embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| CreatePortalToUrl(web_contents_impl, embedded_test_server()->GetURL( |
| "portal.test", "/title2.html")); |
| |
| GURL download_url = embedded_test_server()->GetURL( |
| "portal.test", "/set-header?Content-Disposition: attachment"); |
| GURL iframe_url = embedded_test_server()->GetURL( |
| "portal.test", "/iframe?" + base::EscapeQueryParamValue( |
| download_url.spec(), /*use_plus=*/false)); |
| |
| DownloadObserver download_observer; |
| EXPECT_TRUE(ExecJs( |
| web_contents_impl, |
| JsReplace("document.querySelector('portal').src = $1", iframe_url))); |
| EXPECT_FALSE(download_observer.AwaitDownload()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, DownloadsBlockedViaDownloadLink) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents_impl, |
| embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| Portal* portal = CreatePortalToUrl( |
| web_contents_impl, |
| embedded_test_server()->GetURL("portal.test", "/title2.html")); |
| |
| DownloadObserver download_observer; |
| EXPECT_TRUE(ExecJs(portal->GetPortalContents(), |
| "let a = document.createElement('a');\n" |
| "a.download = 'download.html';\n" |
| "a.href = '/title3.html';\n" |
| "a.click();\n")); |
| EXPECT_FALSE(download_observer.AwaitDownload()); |
| } |
| |
| // The following tests check code paths that won't be hit on Android as we |
| // do not create DevTools windows on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, CallActivateOnTwoPortals) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| shell()->ShowDevTools(); |
| |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal_a = CreatePortalToUrl(web_contents_impl, url_a); |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| Portal* portal_b = CreatePortalToUrl(web_contents_impl, url_b); |
| |
| PortalInterceptorForTesting* portal_interceptor = |
| PortalInterceptorForTesting::From(portal_a); |
| // Hijacks navigate request and calls Activate on both portals. |
| portal_interceptor->SetNavigateCallback(base::BindRepeating( |
| [](Portal* portal_a, Portal* portal_b, const GURL&, |
| blink::mojom::ReferrerPtr, |
| blink::mojom::Portal::NavigateCallback callback) { |
| blink::TransferableMessage message1; |
| message1.sender_agent_cluster_id = base::UnguessableToken::Create(); |
| blink::TransferableMessage message2; |
| message2.sender_agent_cluster_id = base::UnguessableToken::Create(); |
| portal_a->Activate(std::move(message1), base::TimeTicks::Now(), 0, |
| base::DoNothing()); |
| portal_b->Activate(std::move(message2), base::TimeTicks::Now(), 0, |
| base::DoNothing()); |
| std::move(callback).Run(); |
| }, |
| portal_a, portal_b)); |
| |
| RenderProcessHostBadIpcMessageWaiter rph_kill_waiter( |
| main_frame->GetProcess()); |
| GURL dummy_url = embedded_test_server()->GetURL("c.com", "/title1.html"); |
| ExecuteScriptAsync( |
| main_frame, |
| JsReplace("document.querySelector('portal').src = $1", dummy_url)); |
| EXPECT_EQ(bad_message::RPH_MOJO_PROCESS_ERROR, rph_kill_waiter.Wait()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalBrowserTest, CallActivateTwice) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents_impl->GetPrimaryMainFrame(); |
| shell()->ShowDevTools(); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| Portal* portal = CreatePortalToUrl(web_contents_impl, url); |
| PortalInterceptorForTesting* portal_interceptor = |
| PortalInterceptorForTesting::From(portal); |
| // Hijacks navigate request and calls Activate twice instead. |
| portal_interceptor->SetNavigateCallback(base::BindRepeating( |
| [](Portal* portal, const GURL&, blink::mojom::ReferrerPtr, |
| blink::mojom::Portal::NavigateCallback callback) { |
| blink::TransferableMessage message1; |
| message1.sender_agent_cluster_id = base::UnguessableToken::Create(); |
| blink::TransferableMessage message2; |
| message2.sender_agent_cluster_id = base::UnguessableToken::Create(); |
| portal->Activate(std::move(message1), base::TimeTicks::Now(), 0, |
| base::DoNothing()); |
| portal->Activate(std::move(message2), base::TimeTicks::Now(), 0, |
| base::DoNothing()); |
| std::move(callback).Run(); |
| }, |
| portal)); |
| |
| RenderProcessHostBadIpcMessageWaiter rph_kill_waiter( |
| main_frame->GetProcess()); |
| GURL dummy_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| ExecuteScriptAsync( |
| main_frame, |
| JsReplace("document.querySelector('portal').src = $1", dummy_url)); |
| EXPECT_EQ(bad_message::RPH_MOJO_PROCESS_ERROR, rph_kill_waiter.Wait()); |
| } |
| #endif |
| |
| // Tests that various ways of enabling features via the command line produce a |
| // valid configuration. That is, a configuration where we don't have the |
| // renderer thinking that portals are enabled when the browser thinks portals |
| // are disabled. |
| class PortalsValidConfigurationBrowserTest |
| : public ContentBrowserTest, |
| public ::testing::WithParamInterface< |
| std::pair<const char*, const char*>> { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ContentBrowserTest::SetUpCommandLine(command_line); |
| |
| auto [switch_name, switch_value] = GetParam(); |
| if (switch_name == switches::kEnableFeatures) { |
| scoped_feature_list_.InitFromCommandLine(switch_value, ""); |
| } else { |
| command_line->AppendSwitchASCII(switch_name, switch_value); |
| } |
| } |
| |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| namespace { |
| const std::pair<const char*, const char*> kEnablingFlags[] = { |
| {switches::kEnableFeatures, blink::features::kPortals.name}, |
| {switches::kEnableBlinkTestFeatures, ""}, |
| {switches::kEnableExperimentalWebPlatformFeatures, ""}, |
| {switches::kEnableBlinkFeatures, "Portals"}}; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| PortalsValidConfigurationBrowserTest, |
| ::testing::ValuesIn(kEnablingFlags)); |
| |
| IN_PROC_BROWSER_TEST_P(PortalsValidConfigurationBrowserTest, |
| ConfigurationIsValid) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("portal.test", "/title1.html"))); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* possible_portal_host = |
| web_contents_impl->GetPrimaryMainFrame(); |
| |
| bool html_portal_element_exposed = |
| EvalJs(possible_portal_host, "'HTMLPortalElement' in self").ExtractBool(); |
| |
| if (html_portal_element_exposed) |
| EXPECT_TRUE(Portal::IsEnabled()); |
| } |
| |
| namespace { |
| |
| static constexpr struct { |
| base::StringPiece token_id; |
| base::StringPiece token; |
| } kOriginTrialTokens[] = { |
| // Generated by: |
| // tools/origin_trials/generate_token.py --version 3 --expire-days 3650 \ |
| // https://portal.test Portals |
| // Token details: |
| // Version: 3 |
| // Origin: https://portal.test:443 |
| // Is Subdomain: None |
| // Is Third Party: None |
| // Usage Restriction: None |
| // Feature: Portals |
| // Expiry: 1907172789 (2030-06-08 18:13:09 UTC) |
| // Signature (Base64): |
| // 3nrCPtI01xhkOinmRegbwhnA5VrNBJUnxLv2yPxSKdtUMyoo9iUZszqtkaTFyV8Al/VJigcAOzLLsKOZ2N6DBQ== |
| {"portals", |
| "A956wj7SNNcYZDop5kXoG8IZwOVazQSVJ8S79sj8UinbVDMqKPYlGbM6rZGkxclfAJf1SYoHA" |
| "Dsyy7CjmdjegwUAAABReyJvcmlnaW4iOiAiaHR0cHM6Ly9wb3J0YWwudGVzdDo0NDMiLCAiZm" |
| "VhdHVyZSI6ICJQb3J0YWxzIiwgImV4cGlyeSI6IDE5MDcxNzI3ODl9"}, |
| }; |
| |
| } // namespace |
| |
| // Tests that origin trials correctly toggle the feature, and that default |
| // states are as intended for the same-origin origin trial |
| // (https://crbug.com/1040212). |
| // |
| // That these controls provide suitable safeguards and functionality is tested |
| // elsewhere. |
| class PortalOriginTrialBrowserTest : public ContentBrowserTest { |
| protected: |
| PortalOriginTrialBrowserTest() = default; |
| |
| bool PlatformSupportsPortalsOriginTrial() { return false; } |
| |
| void SetUp() override { |
| ContentBrowserTest::SetUp(); |
| EXPECT_EQ(base::FeatureList::IsEnabled(blink::features::kPortals), |
| PlatformSupportsPortalsOriginTrial()); |
| EXPECT_FALSE( |
| base::FeatureList::IsEnabled(blink::features::kPortalsCrossOrigin)); |
| } |
| |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| url_loader_interceptor_.emplace( |
| base::BindRepeating(&PortalOriginTrialBrowserTest::InterceptRequest)); |
| } |
| |
| void TearDownOnMainThread() override { url_loader_interceptor_.reset(); } |
| |
| // URLLoaderInterceptor callback |
| static bool InterceptRequest(URLLoaderInterceptor::RequestParams* params) { |
| // Find the appropriate origin trial token. |
| base::StringPiece origin_trial_token; |
| std::string origin_trial_query_param; |
| if (net::GetValueForKeyInQuery(params->url_request.url, "origintrial", |
| &origin_trial_query_param)) { |
| for (const auto& pair : kOriginTrialTokens) |
| if (pair.token_id == origin_trial_query_param) |
| origin_trial_token = pair.token; |
| } |
| |
| // Construct and send the response. |
| std::string headers = |
| "HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n"; |
| if (!origin_trial_token.empty()) |
| base::StrAppend(&headers, {"Origin-Trial: ", origin_trial_token, "\n"}); |
| headers += '\n'; |
| std::string body = "<!DOCTYPE html><body>Hello world!</body>"; |
| URLLoaderInterceptor::WriteResponse(headers, body, params->client.get()); |
| return true; |
| } |
| |
| private: |
| absl::optional<URLLoaderInterceptor> url_loader_interceptor_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(PortalOriginTrialBrowserTest, WithoutTrialToken) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| ASSERT_TRUE(NavigateToURL(web_contents_impl, GURL("https://portal.test/"))); |
| EXPECT_EQ(false, EvalJs(web_contents_impl, "'HTMLPortalElement' in self")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PortalOriginTrialBrowserTest, WithTrialToken) { |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| ASSERT_TRUE(NavigateToURL(web_contents_impl, |
| GURL("https://portal.test/?origintrial=portals"))); |
| bool html_portal_element_exposed = |
| EvalJs(web_contents_impl, "'HTMLPortalElement' in self").ExtractBool(); |
| EXPECT_EQ(PlatformSupportsPortalsOriginTrial(), html_portal_element_exposed); |
| if (html_portal_element_exposed) |
| EXPECT_TRUE(Portal::IsEnabled()); |
| } |
| |
| class PortalPixelBrowserTest : public PortalBrowserTest { |
| public: |
| void SetUp() override { |
| EnablePixelOutput(); |
| PortalBrowserTest::SetUp(); |
| } |
| }; |
| |
| // Ensures content is correctly rastered with respect to the page scale factor. |
| // Note: a portaled page is unique in that it has both kinds of page scale |
| // factor simultaneously; an external page scale factor that comes from the |
| // page scale on the embedder page as well as the natural page scale factor on |
| // the portal page. Though the portal cannot be pinch-zoomed until activated, |
| // it may use a viewport <meta> tag to set an initial scale factor. This test |
| // loads a portal that has a zoomed out page, then pinch zooms in on the |
| // embedder page. Both page scales should be accounted for so the pattern in |
| // the portal should appear the correct size (4x4 checkerboard tiles) as well |
| // as be re-rastered for the embedder's zoom so it should appear crisp. |
| // |
| // Flaky on Android: https://crbug.com/1120213 |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_PageScaleRaster DISABLED_PageScaleRaster |
| #else |
| #define MAYBE_PageScaleRaster PageScaleRaster |
| #endif |
| IN_PROC_BROWSER_TEST_F(PortalPixelBrowserTest, MAYBE_PageScaleRaster) { |
| ShellContentBrowserClient::Get()->set_override_web_preferences_callback( |
| base::BindRepeating([](blink::web_pref::WebPreferences* prefs) { |
| // Enable processing of the viewport <meta> tag in the same way the |
| // Android browser would. |
| prefs->viewport_enabled = true; |
| prefs->viewport_meta_enabled = true; |
| prefs->shrinks_viewport_contents_to_fit = true; |
| prefs->viewport_style = blink::mojom::ViewportStyle::kMobile; |
| |
| // Hide scrollbars to make pixel testing more robust. |
| prefs->hide_scrollbars = true; |
| })); |
| |
| EXPECT_TRUE(NavigateToURL( |
| shell(), |
| embedded_test_server()->GetURL("portal.test", "/portals/raster.html"))); |
| |
| auto* main_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| std::vector<WebContents*> inner_web_contents = |
| main_contents->GetInnerWebContents(); |
| ASSERT_EQ(1u, inner_web_contents.size()); |
| auto* portal_contents = static_cast<WebContentsImpl*>(inner_web_contents[0]); |
| |
| RenderFrameSubmissionObserver portal_frame_observer( |
| portal_contents->GetPrimaryFrameTree().root()); |
| |
| // Perform a pinch-zoom action into the top-left of the page. |
| { |
| content::RenderWidgetHostImpl* widget_host = |
| content::RenderWidgetHostImpl::From( |
| main_contents->GetRenderViewHost()->GetWidget()); |
| |
| content::SyntheticPinchGestureParams params; |
| params.gesture_source_type = |
| content::mojom::GestureSourceType::kTouchpadInput; |
| params.scale_factor = 4.f; |
| params.anchor = gfx::PointF(); |
| params.relative_pointer_speed_in_pixels_s = 40000; |
| auto pinch_gesture = |
| std::make_unique<content::SyntheticTouchpadPinchGesture>(params); |
| |
| base::RunLoop run_loop; |
| widget_host->QueueSyntheticGesture( |
| std::move(pinch_gesture), |
| base::BindOnce( |
| [](base::OnceClosure quit_closure, |
| content::SyntheticGesture::Result result) { |
| EXPECT_EQ(content::SyntheticGesture::GESTURE_FINISHED, result); |
| std::move(quit_closure).Run(); |
| }, |
| run_loop.QuitClosure())); |
| run_loop.Run(); |
| } |
| |
| const float kScaleTolerance = 0.1f; |
| |
| // The portal should have its external page scale factor set from the main |
| // frame's pinch zoom. However, it should have a page scale factor set as |
| // well, coming from the initial-scale value of its viewport <meta> tag. |
| { |
| portal_frame_observer.WaitForExternalPageScaleFactor(4.f, kScaleTolerance); |
| EXPECT_EQ( |
| 0.5, portal_frame_observer.LastRenderFrameMetadata().page_scale_factor); |
| } |
| |
| // This test passes if the result matches the previously rendered |
| // expectation. A small amount of jitter is allowed due to differences in |
| // graphics drivers or raster code, but the resulting image should appear |
| // crisp. |
| { |
| // Compare only the top-left 200x200 rect - the checkerboard DIV is 100x100 |
| // with initial-scale of 0.5. The embedder page zooms in to 4x so the |
| // content rect should only be 200x200 output pixels. |
| const gfx::Size kCompareSize(200, 200); |
| base::FilePath reference = |
| content::GetTestFilePath("portals", "raster-expected.png"); |
| EXPECT_TRUE(CompareWebContentsOutputToReference(main_contents, reference, |
| kCompareSize)); |
| } |
| |
| // Now activate the portal. Since this replaces the embedder as the main |
| // WebContents, the external page scale factor coming from the embedder |
| // should be cleared. |
| { |
| RenderFrameHostImpl* main_frame = main_contents->GetPrimaryMainFrame(); |
| ExecuteScriptAsync(main_frame, |
| "document.querySelector('portal').activate();"); |
| portal_frame_observer.WaitForExternalPageScaleFactor(1.f, kScaleTolerance); |
| } |
| } |
| |
| class PortalFencedFrameBrowserTest : public PortalBrowserTest { |
| public: |
| PortalFencedFrameBrowserTest() = default; |
| ~PortalFencedFrameBrowserTest() override = default; |
| |
| RenderFrameHost* primary_main_frame_host() { |
| return shell()->web_contents()->GetPrimaryMainFrame(); |
| } |
| |
| protected: |
| content::test::FencedFrameTestHelper fenced_frame_helper_; |
| }; |
| |
| // Create a fenced frame in the primary main page that creates a portal which |
| // should fail. Ideally this would be a WPT test but that requires a special |
| // virtual test suite which would just be for enabling fenced frames and |
| // portals together. |
| IN_PROC_BROWSER_TEST_F(PortalFencedFrameBrowserTest, CreatePortalBlocked) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| const GURL fenced_frame_url = |
| embedded_test_server()->GetURL("/fenced_frames/title1.html"); |
| RenderFrameHost* fenced_frame_rfh = fenced_frame_helper_.CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url); |
| ASSERT_NE(nullptr, fenced_frame_rfh); |
| |
| WebContentsConsoleObserver console_observer(shell()->web_contents()); |
| console_observer.SetPattern( |
| "*Cannot use <portal> in a nested browsing context.*"); |
| |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, |
| R"(let portal = document.createElement('portal'); |
| portal.src = new URL('about:blank', location.href); |
| document.body.appendChild(portal); |
| )")); |
| console_observer.Wait(); |
| } |
| |
| } // namespace content |