blob: 4f357f62366a416caa4cac66a601f895b7817926 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <concepts>
#include "base/check_op.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame.mojom-shared.h"
#include "content/common/frame.mojom.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.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/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/frame/remote_frame.mojom.h"
namespace content {
namespace {
template <typename T>
concept IsWebContentsOrRenderFrameHost =
(std::same_as<T, WebContents*> ||
std::same_as<T, RenderFrameHost*>)&&requires(T p) { base::to_address(p); };
enum class TestAction {
kNavigateToSameSitePage,
kNavigateToSameSitePageWithJs,
kNavigateToCrossSitePage,
kNavigateToCrossSitePageWithJs,
kNavigateToFragment,
kNavigateToFragmentWithJs,
kPushHistory,
kReplaceHistory,
kDestroy,
kDestroyWithJs,
};
std::string GenerateTestName(const testing::TestParamInfo<TestAction>& info) {
std::string test_name;
switch (info.param) {
case TestAction::kNavigateToSameSitePage:
test_name = "NavigateToSameSitePage";
break;
case TestAction::kNavigateToSameSitePageWithJs:
test_name = "NavigateToSameSitePageWithJs";
break;
case TestAction::kNavigateToCrossSitePage:
test_name = "NavigateToCrossSitePage";
break;
case TestAction::kNavigateToCrossSitePageWithJs:
test_name = "NavigateToCrossSitePageWithJs";
break;
case TestAction::kNavigateToFragment:
test_name = "NavigateToFragment";
break;
case TestAction::kNavigateToFragmentWithJs:
test_name = "NavigateToFragmentWithJs";
break;
case TestAction::kPushHistory:
test_name = "PushHistory";
break;
case TestAction::kReplaceHistory:
test_name = "ReplaceHistory";
break;
case TestAction::kDestroy:
test_name = "Destroy";
break;
case TestAction::kDestroyWithJs:
test_name = "DestroyWithJs";
break;
}
return base::StringPrintf("%d_%s", info.index, test_name);
}
class PartitionedPopinsControllerBrowserTest
: public ContentBrowserTest,
public testing::WithParamInterface<TestAction> {
public:
PartitionedPopinsControllerBrowserTest() {
feature_list_.InitAndEnableFeature(blink::features::kPartitionedPopins);
}
protected:
WebContents* GetMainWebContents() { return shell()->web_contents(); }
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_https_test_server().SetSSLConfig(
net::EmbeddedTestServer::CERT_TEST_NAMES);
embedded_https_test_server().ServeFilesFromSourceDirectory(
"content/test/data");
ASSERT_TRUE(embedded_https_test_server().Start());
SetUpIframesAndNewWindow(CreateBrowser()->web_contents());
}
void FlushRunLoop() {
base::RunLoop run_loop;
GetUIThreadTaskRunner({})->PostTask(FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
template <content::IsWebContentsOrRenderFrameHost Ptr>
void DoAction(Ptr render_frame_host_or_web_contents) {
switch (GetParam()) {
case TestAction::kNavigateToSameSitePage:
Navigate(render_frame_host_or_web_contents, GetUrl("/title1.html"));
break;
case TestAction::kNavigateToSameSitePageWithJs:
NavigateWithJs(render_frame_host_or_web_contents,
GetUrl("/title1.html"));
break;
case TestAction::kNavigateToCrossSitePage:
Navigate(render_frame_host_or_web_contents,
GetUrl("/title1.html", "b.test"));
break;
case TestAction::kNavigateToCrossSitePageWithJs:
NavigateWithJs(render_frame_host_or_web_contents,
GetUrl("/title1.html", "b.test"));
break;
case TestAction::kNavigateToFragment:
NavigateToFragment(render_frame_host_or_web_contents, "#ref");
break;
case TestAction::kNavigateToFragmentWithJs:
NavigateToFragmentWithJs(render_frame_host_or_web_contents, "#ref");
break;
case TestAction::kPushHistory:
PushHistory(render_frame_host_or_web_contents, "/title1.html");
break;
case TestAction::kReplaceHistory:
ReplaceHistory(render_frame_host_or_web_contents, "/title1.html");
break;
case TestAction::kDestroy:
Destroy(render_frame_host_or_web_contents);
break;
case TestAction::kDestroyWithJs:
DestroyWithJs(render_frame_host_or_web_contents);
break;
}
}
WebContents* OpenPopin(const ToRenderFrameHost& execution_target) {
const GURL url_popin = embedded_https_test_server().GetURL(
"a.test", "/partitioned_popins/wildcard_policy.html");
WebContentsAddedObserver new_tab_observer;
TestNavigationObserver nav_observer(nullptr);
nav_observer.StartWatchingNewWebContents();
CHECK(ExecJs(execution_target,
"window.open('" + url_popin.spec() + "', '_blank', 'popin')"));
WebContents* popin_web_contents = new_tab_observer.GetWebContents();
CHECK(popin_web_contents);
nav_observer.Wait();
// Check that the popin is open and it's partitioned.
CHECK_EQ(EvalJs(popin_web_contents, "window.popinContextType()"),
"partitioned");
// Check that the popin considers itself to be embedded by the origin of
// the opener.
CHECK_EQ(static_cast<WebContentsImpl*>(popin_web_contents)
->GetPartitionedPopinEmbedderOriginForTesting(),
execution_target.render_frame_host()
->GetMainFrame()
->GetLastCommittedOrigin()
.GetURL());
return popin_web_contents;
}
WebContentsDestroyedWatcher OpenPopinAndWatchDestruction(
const ToRenderFrameHost& execution_target) {
return WebContentsDestroyedWatcher(OpenPopin(execution_target));
}
void SetUpIframesAndNewWindow(WebContents* new_window_web_contents) {
DCHECK(new_window_web_contents);
const GURL url = embedded_https_test_server().GetURL(
"a.test", "/partitioned_popins/iframes_allow_popins.html");
ASSERT_TRUE(NavigateToURL(GetMainWebContents(), url));
ASSERT_TRUE(
NavigateToURLFromRenderer(ChildFrameAt(GetMainWebContents(), 0), url));
const GURL links_url =
embedded_https_test_server().GetURL("a.test", "/links.html");
ASSERT_TRUE(NavigateToURL(new_window_web_contents, links_url));
new_window_web_contents_ = new_window_web_contents->GetWeakPtr();
}
WebContents* GetNewWindowWebContents() {
DCHECK(new_window_web_contents_);
return new_window_web_contents_.get();
}
GURL GetUrl(std::string_view relative_url,
std::string_view hostname = "a.test") {
return embedded_https_test_server().GetURL(hostname, relative_url);
}
void Navigate(WebContents* web_contents, const GURL& url) {
EXPECT_TRUE(NavigateToURL(web_contents, url));
}
void Navigate(RenderFrameHost* render_frame_host, const GURL& url) {
content::TestNavigationObserver observer(
WebContents::FromRenderFrameHost(render_frame_host));
auto params = blink::mojom::OpenURLParams::New();
params->url = url;
params->disposition = WindowOpenDisposition::CURRENT_TAB;
static_cast<content::mojom::FrameHost*>(
static_cast<content::RenderFrameHostImpl*>(render_frame_host))
->OpenURL(std::move(params));
observer.Wait();
}
void NavigateWithJs(const ToRenderFrameHost& adapter, const GURL& url) {
EXPECT_TRUE(NavigateToURLFromRenderer(adapter, GetUrl("/title1.html")));
}
void NavigateToFragment(WebContents* web_contents,
std::string_view fragment) {
CHECK(base::StartsWith(fragment, "#"));
Navigate(web_contents,
web_contents->GetLastCommittedURL().Resolve(fragment));
}
void NavigateToFragment(RenderFrameHost* render_frame_host,
std::string_view fragment) {
CHECK(base::StartsWith(fragment, "#"));
Navigate(render_frame_host,
render_frame_host->GetLastCommittedURL().Resolve(fragment));
}
void NavigateToFragmentWithJs(const ToRenderFrameHost& adapter,
std::string_view fragment) {
CHECK(base::StartsWith(fragment, "#"));
NavigateWithJs(
adapter,
adapter.render_frame_host()->GetLastCommittedURL().Resolve(fragment));
}
void Destroy(WebContents* web_contents) { web_contents->Close(); }
void Destroy(RenderFrameHost* render_frame_host) {
static_cast<content::RenderFrameHostImpl*>(render_frame_host)
->DeleteRenderFrame(mojom::FrameDeleteIntention::kNotMainFrame);
}
void DestroyWithJs(WebContents* web_contents) {
blink::web_pref::WebPreferences prefs =
web_contents->GetOrCreateWebPreferences();
prefs.allow_scripts_to_close_windows = true;
web_contents->SetWebPreferences(prefs);
EXPECT_TRUE(ExecJs(web_contents, "window.close()"));
}
void DestroyWithJs(RenderFrameHost* render_frame_host) {
CHECK_NE(render_frame_host->GetFrameName(), "");
CHECK(render_frame_host->GetParentOrOuterDocumentOrEmbedder());
EXPECT_TRUE(content::ExecJs(
render_frame_host->GetParentOrOuterDocumentOrEmbedder(),
JsReplace("document.querySelector('iframe[name=$1]').remove();",
render_frame_host->GetFrameName())));
}
void PushHistory(const ToRenderFrameHost& execution_target,
std::string_view url) {
EXPECT_TRUE(content::ExecJs(
execution_target, JsReplace("history.pushState(null, '', $1)", url)));
}
void ReplaceHistory(const ToRenderFrameHost& execution_target,
std::string_view url) {
EXPECT_TRUE(
content::ExecJs(execution_target,
JsReplace("history.replaceState(null, '', $1)", url)));
}
private:
base::test::ScopedFeatureList feature_list_;
base::WeakPtr<WebContents> new_window_web_contents_;
};
IN_PROC_BROWSER_TEST_P(PartitionedPopinsControllerBrowserTest,
TopFrameOpenerDoesAction) {
WebContentsDestroyedWatcher popin_destroyed_watcher =
OpenPopinAndWatchDestruction(GetMainWebContents());
// Do action on unrelated contents/frames.
DoAction(GetNewWindowWebContents());
DoAction(ChildFrameAt(GetMainWebContents(), 0));
FlushRunLoop();
EXPECT_FALSE(popin_destroyed_watcher.IsDestroyed());
DoAction(GetMainWebContents());
popin_destroyed_watcher.Wait();
EXPECT_TRUE(popin_destroyed_watcher.IsDestroyed());
}
IN_PROC_BROWSER_TEST_P(PartitionedPopinsControllerBrowserTest,
IframeOpenerDoesAction) {
RenderFrameHost* iframe = ChildFrameAt(GetMainWebContents(), 0);
ASSERT_TRUE(iframe);
WebContentsDestroyedWatcher popin_destroyed_watcher =
OpenPopinAndWatchDestruction(iframe);
// Do action on unrelated contents/frames.
DoAction(GetNewWindowWebContents());
DoAction(ChildFrameAt(GetMainWebContents(), 1));
DoAction(ChildFrameAt(iframe, 0));
FlushRunLoop();
EXPECT_FALSE(popin_destroyed_watcher.IsDestroyed());
DoAction(iframe);
popin_destroyed_watcher.Wait();
EXPECT_TRUE(popin_destroyed_watcher.IsDestroyed());
}
IN_PROC_BROWSER_TEST_P(PartitionedPopinsControllerBrowserTest,
IframeOpenerTopFrameDoesAction) {
RenderFrameHost* iframe = ChildFrameAt(GetMainWebContents(), 0);
ASSERT_TRUE(iframe);
WebContentsDestroyedWatcher popin_destroyed_watcher =
OpenPopinAndWatchDestruction(iframe);
// Do action on unrelated contents/frames.
DoAction(GetNewWindowWebContents());
DoAction(ChildFrameAt(GetMainWebContents(), 1));
DoAction(ChildFrameAt(iframe, 0));
FlushRunLoop();
EXPECT_FALSE(popin_destroyed_watcher.IsDestroyed());
DoAction(GetMainWebContents());
popin_destroyed_watcher.Wait();
EXPECT_TRUE(popin_destroyed_watcher.IsDestroyed());
}
IN_PROC_BROWSER_TEST_P(PartitionedPopinsControllerBrowserTest,
IframeOpenerIframeParentDoesAction) {
RenderFrameHost* iframe = ChildFrameAt(GetMainWebContents(), 0);
ASSERT_TRUE(iframe);
RenderFrameHost* child_iframe = ChildFrameAt(iframe, 0);
ASSERT_TRUE(child_iframe);
WebContentsDestroyedWatcher popin_destroyed_watcher =
OpenPopinAndWatchDestruction(child_iframe);
// Do action on unrelated contents/frames.
DoAction(GetNewWindowWebContents());
DoAction(ChildFrameAt(GetMainWebContents(), 1));
DoAction(ChildFrameAt(iframe, 1));
FlushRunLoop();
EXPECT_FALSE(popin_destroyed_watcher.IsDestroyed());
DoAction(iframe);
popin_destroyed_watcher.Wait();
EXPECT_TRUE(popin_destroyed_watcher.IsDestroyed());
}
INSTANTIATE_TEST_SUITE_P(
,
PartitionedPopinsControllerBrowserTest,
::testing::Values(TestAction::kNavigateToSameSitePage,
TestAction::kNavigateToSameSitePageWithJs,
TestAction::kNavigateToCrossSitePage,
TestAction::kNavigateToCrossSitePageWithJs,
TestAction::kNavigateToFragment,
TestAction::kNavigateToFragmentWithJs,
TestAction::kPushHistory,
TestAction::kReplaceHistory,
TestAction::kDestroy,
TestAction::kDestroyWithJs),
&GenerateTestName);
} // namespace
} // namespace content