blob: a3f389268746ca2eb994c07998644ecd84f157b8 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/bind.h"
#include "base/callback.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/frame_host/render_frame_host_manager.h"
#include "content/browser/frame_host/render_frame_proxy_host.h"
#include "content/browser/portal/portal.h"
#include "content/browser/renderer_host/render_widget_host_input_event_router.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/public/browser/web_contents_delegate.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/hit_test_region_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "mojo/public/cpp/bindings/strong_associated_binding.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/blink/public/common/features.h"
#include "third_party/blink/public/mojom/portal/portal.mojom-test-utils.h"
#include "third_party/blink/public/mojom/portal/portal.mojom.h"
#include "url/url_constants.h"
using testing::_;
namespace content {
// The PortalInterceptorForTesting can be used in tests to inspect Portal IPCs.
class PortalInterceptorForTesting final
: public blink::mojom::PortalInterceptorForTesting {
public:
static PortalInterceptorForTesting* Create(
RenderFrameHostImpl* render_frame_host_impl,
blink::mojom::PortalAssociatedRequest request,
blink::mojom::PortalClientAssociatedPtr client);
static PortalInterceptorForTesting* From(content::Portal* portal);
void Activate(blink::TransferableMessage data,
ActivateCallback callback) override {
portal_activated_ = true;
if (run_loop_) {
run_loop_->Quit();
run_loop_ = nullptr;
}
// |this| can be destroyed after Activate() is called.
portal_->Activate(std::move(data), std::move(callback));
}
void WaitForActivate() {
if (portal_activated_)
return;
base::RunLoop run_loop;
run_loop_ = &run_loop;
run_loop.Run();
}
// Test getters.
content::Portal* GetPortal() { return portal_.get(); }
WebContents* GetPortalContents() { return portal_->GetPortalContents(); }
private:
PortalInterceptorForTesting(RenderFrameHostImpl* render_frame_host_impl)
: portal_(content::Portal::CreateForTesting(render_frame_host_impl)) {}
blink::mojom::Portal* GetForwardingInterface() override {
return portal_.get();
}
std::unique_ptr<content::Portal> portal_;
bool portal_activated_ = false;
base::RunLoop* run_loop_ = nullptr;
};
// static
PortalInterceptorForTesting* PortalInterceptorForTesting::Create(
RenderFrameHostImpl* render_frame_host_impl,
blink::mojom::PortalAssociatedRequest request,
blink::mojom::PortalClientAssociatedPtr client) {
auto test_portal_ptr =
base::WrapUnique(new PortalInterceptorForTesting(render_frame_host_impl));
PortalInterceptorForTesting* test_portal = test_portal_ptr.get();
test_portal->GetPortal()->SetBindingForTesting(
mojo::MakeStrongAssociatedBinding(std::move(test_portal_ptr),
std::move(request)));
test_portal->GetPortal()->SetClientForTesting(std::move(client));
return test_portal;
}
// static
PortalInterceptorForTesting* PortalInterceptorForTesting::From(
content::Portal* portal) {
blink::mojom::Portal* impl = portal->GetBindingForTesting()->impl();
auto* interceptor = static_cast<PortalInterceptorForTesting*>(impl);
CHECK_NE(static_cast<blink::mojom::Portal*>(portal), impl);
CHECK_EQ(interceptor->GetPortal(), portal);
return interceptor;
}
// The PortalCreatedObserver observes portal creations on
// |render_frame_host_impl|. This observer can be used to monitor for multiple
// Portal creations on the same RenderFrameHost, by repeatedly calling
// WaitUntilPortalCreated().
class PortalCreatedObserver : public mojom::FrameHostInterceptorForTesting {
public:
explicit PortalCreatedObserver(RenderFrameHostImpl* render_frame_host_impl)
: render_frame_host_impl_(render_frame_host_impl) {
render_frame_host_impl_->frame_host_binding_for_testing()
.SwapImplForTesting(this);
}
~PortalCreatedObserver() override {}
FrameHost* GetForwardingInterface() override {
return render_frame_host_impl_;
}
void CreatePortal(blink::mojom::PortalAssociatedRequest request,
blink::mojom::PortalClientAssociatedPtrInfo client,
CreatePortalCallback callback) override {
PortalInterceptorForTesting* portal_interceptor =
PortalInterceptorForTesting::Create(
render_frame_host_impl_, std::move(request),
blink::mojom::PortalClientAssociatedPtr(std::move(client)));
portal_ = portal_interceptor->GetPortal();
RenderFrameProxyHost* proxy_host = portal_->CreateProxyAndAttachPortal();
std::move(callback).Run(proxy_host->GetRoutingID(), portal_->portal_token(),
portal_->GetDevToolsFrameToken());
if (run_loop_)
run_loop_->Quit();
}
Portal* WaitUntilPortalCreated() {
Portal* portal = portal_;
if (portal) {
portal_ = nullptr;
return portal;
}
base::RunLoop run_loop;
run_loop_ = &run_loop;
run_loop.Run();
run_loop_ = nullptr;
portal = portal_;
portal_ = nullptr;
return portal;
}
private:
RenderFrameHostImpl* render_frame_host_impl_;
base::RunLoop* run_loop_ = nullptr;
Portal* portal_ = nullptr;
};
class PortalBrowserTest : public ContentBrowserTest {
protected:
PortalBrowserTest() {}
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(blink::features::kPortals);
ContentBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ContentBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
}
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* main_frame = web_contents_impl->GetMainFrame();
PortalCreatedObserver portal_created_observer(main_frame);
EXPECT_TRUE(
ExecJs(main_frame,
"document.body.appendChild(document.createElement('portal'));"));
Portal* portal = portal_created_observer.WaitUntilPortalCreated();
EXPECT_NE(nullptr, portal);
}
// 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->GetMainFrame();
PortalCreatedObserver portal_created_observer(main_frame);
// 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"));
EXPECT_TRUE(
ExecJs(main_frame,
base::StringPrintf("var portal = document.createElement('portal');"
"portal.src = '%s';"
"document.body.appendChild(portal);",
a_url.spec().c_str())));
PortalInterceptorForTesting* portal_interceptor =
PortalInterceptorForTesting::From(
portal_created_observer.WaitUntilPortalCreated());
WebContents* portal_contents = portal_interceptor->GetPortalContents();
EXPECT_NE(nullptr, portal_contents);
EXPECT_NE(portal_contents->GetLastCommittedURL(), a_url);
// The portal should not have navigated yet, we can observe the Portal's
// first navigation.
TestNavigationObserver navigation_observer(portal_contents);
navigation_observer.Wait();
EXPECT_EQ(navigation_observer.last_navigation_url(), a_url);
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,
base::StringPrintf("document.querySelector('portal').src = '%s';",
b_url.spec().c_str())));
navigation_observer.Wait();
EXPECT_EQ(navigation_observer.last_navigation_url(), b_url);
EXPECT_EQ(portal_contents->GetLastCommittedURL(), b_url);
}
// Tests that a portal can navigating by attribute.
{
TestNavigationObserver navigation_observer(portal_contents);
GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
EXPECT_TRUE(ExecJs(
main_frame,
base::StringPrintf(
"document.querySelector('portal').setAttribute('src', '%s');",
c_url.spec().c_str())));
navigation_observer.Wait();
EXPECT_EQ(navigation_observer.last_navigation_url(), c_url);
EXPECT_EQ(portal_contents->GetLastCommittedURL(), c_url);
}
}
// Tests that a portal can be activated in content_shell.
IN_PROC_BROWSER_TEST_F(PortalBrowserTest, ActivatePortalInShell) {
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->GetMainFrame();
Portal* portal = nullptr;
{
PortalCreatedObserver portal_created_observer(main_frame);
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(ExecJs(
main_frame, JsReplace("var portal = document.createElement('portal');"
"portal.src = $1;"
"document.body.appendChild(portal);",
a_url)));
portal = portal_created_observer.WaitUntilPortalCreated();
}
PortalInterceptorForTesting* portal_interceptor =
PortalInterceptorForTesting::From(portal);
// 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());
ExecuteScriptAsync(main_frame,
"document.querySelector('portal').activate();");
portal_interceptor->WaitForActivate();
// After activation, the shell's WebContents should be the previous portal's
// WebContents.
EXPECT_EQ(portal_contents, shell()->web_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());
RenderFrameHostImpl* main_frame = web_contents_impl->GetMainFrame();
Portal* portal = nullptr;
PortalCreatedObserver portal_created_observer(main_frame);
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(ExecJs(main_frame,
JsReplace("var portal = document.createElement('portal');"
"portal.src = $1;"
"document.body.appendChild(portal);",
a_url)));
portal = portal_created_observer.WaitUntilPortalCreated();
WebContentsImpl* portal_contents = portal->GetPortalContents();
RenderFrameProxyHost* proxy_host = portal_contents->GetFrameTree()
->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->GetMainFrame();
Portal* portal = nullptr;
PortalCreatedObserver portal_created_observer(main_frame);
GURL a_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(ExecJs(main_frame,
JsReplace("var portal = document.createElement('portal');"
"portal.src = $1;"
"document.body.appendChild(portal);",
a_url)));
// Wait for portal to be created.
portal = portal_created_observer.WaitUntilPortalCreated();
WebContentsImpl* portal_contents = portal->GetPortalContents();
FrameTreeNode* portal_main_frame_node =
portal_contents->GetFrameTree()->root();
// The portal should not have navigated yet, wait for the first navigation.
TestNavigationObserver navigation_observer(portal_contents);
navigation_observer.Wait();
// 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(portal);"));
fdo1.Wait();
fdo2.Wait();
}
// Tests that input events targeting the portal are only received by the parent
// renderer.
IN_PROC_BROWSER_TEST_F(PortalBrowserTest, 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->GetMainFrame();
// Create portal and wait for navigation.
Portal* portal = nullptr;
PortalCreatedObserver portal_created_observer(main_frame);
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(ExecJs(main_frame,
JsReplace("var portal = document.createElement('portal');"
"portal.src = $1;"
"document.body.appendChild(portal);",
a_url)));
portal = portal_created_observer.WaitUntilPortalCreated();
WebContentsImpl* portal_contents = portal->GetPortalContents();
RenderFrameHostImpl* portal_frame = portal_contents->GetMainFrame();
EXPECT_TRUE(static_cast<RenderWidgetHostViewBase*>(portal_frame->GetView())
->IsRenderWidgetHostViewChildFrame());
RenderWidgetHostViewChildFrame* portal_view =
static_cast<RenderWidgetHostViewChildFrame*>(portal_frame->GetView());
TestNavigationObserver navigation_observer(portal_contents);
navigation_observer.Wait();
WaitForHitTestDataOrChildSurfaceReady(portal_frame);
// Create listeners for both widgets.
RenderWidgetHostMouseEventMonitor main_frame_monitor(
main_frame->GetRenderWidgetHost());
RenderWidgetHostMouseEventMonitor portal_frame_monitor(
portal_frame->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_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));
main_frame_monitor.ResetEventReceived();
portal_frame_monitor.ResetEventReceived();
InputEventAckWaiter waiter(main_frame->GetRenderWidgetHost(),
blink::WebInputEvent::kMouseDown);
SimulateRoutedMouseEvent(web_contents_impl, blink::WebInputEvent::kMouseDown,
blink::WebPointerProperties::Button::kLeft,
root_location);
waiter.Wait();
// Check that the click event was only received by the main frame.
EXPECT_TRUE(main_frame_monitor.EventWasReceived());
EXPECT_FALSE(portal_frame_monitor.EventWasReceived());
EXPECT_EQ(true, EvalJs(main_frame, "clicked"));
EXPECT_EQ(false, EvalJs(portal_frame, "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->GetMainFrame();
// Create portal and wait for navigation.
PortalCreatedObserver portal_created_observer(main_frame);
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(ExecJs(main_frame,
JsReplace("var portal = document.createElement('portal');"
"portal.src = $1;"
"document.body.appendChild(portal);",
a_url)));
Portal* portal = portal_created_observer.WaitUntilPortalCreated();
WebContentsImpl* portal_contents = portal->GetPortalContents();
RenderFrameHostImpl* portal_frame = portal_contents->GetMainFrame();
ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(portal_frame->GetView())
->IsRenderWidgetHostViewChildFrame());
RenderWidgetHostViewChildFrame* portal_view =
static_cast<RenderWidgetHostViewChildFrame*>(portal_frame->GetView());
TestNavigationObserver navigation_observer(portal_contents);
navigation_observer.Wait();
WaitForHitTestDataOrChildSurfaceReady(portal_frame);
viz::mojom::InputTargetClient* target_client =
main_frame->GetRenderWidgetHost()->input_target_client();
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();
}
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());
RenderFrameHostImpl* main_frame = web_contents_impl->GetMainFrame();
// Create portal and wait for navigation.
PortalCreatedObserver portal_created_observer(main_frame);
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(ExecJs(main_frame,
JsReplace("var portal = document.createElement('portal');"
"portal.src = $1;"
"document.body.appendChild(portal);",
a_url)));
Portal* portal = portal_created_observer.WaitUntilPortalCreated();
WebContentsImpl* portal_contents = portal->GetPortalContents();
RenderFrameHostImpl* portal_main_frame = portal_contents->GetMainFrame();
TestNavigationObserver portal_navigation_observer(portal_contents);
portal_navigation_observer.Wait();
// 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 content