blob: 9b372cd7b11be579942173fb655ec16e2e8f0242 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdint.h>
#include <array>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/statistics_recorder.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/test/test_timeouts.h"
#include "base/test/values_test_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "cc/test/pixel_test_utils.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/browser_url_handler_impl.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigation_state_keep_alive.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/spare_render_process_host_manager_impl.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/common/frame_messages.mojom.h"
#include "content/common/navigation_client.mojom-forward.h"
#include "content/common/navigation_client.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_message_filter.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/browser_url_handler.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/download_manager_delegate.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/network_service_util.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition_config.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/result_codes.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/download_test_observer.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/no_renderer_crashes_assertion.h"
#include "content/public/test/slow_http_response.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_navigation_throttle.h"
#include "content/public/test/test_navigation_throttle_inserter.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_download_manager_delegate.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
#include "content/test/fake_network_url_loader_factory.h"
#include "content/test/render_document_feature.h"
#include "content/test/task_runner_deferring_throttle.h"
#include "content/test/test_render_frame_host_factory.h"
#include "ipc/ipc_security_test_util.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "net/base/features.h"
#include "net/base/load_flags.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/url_request/url_request_failed_job.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/loader/url_loader_throttle.h"
#include "third_party/blink/public/mojom/frame/remote_frame.mojom-test-utils.h"
#include "third_party/blink/public/mojom/frame/sudden_termination_disabler_type.mojom-shared.h"
#include "third_party/blink/public/mojom/navigation/navigation_params.mojom-shared.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"
#include "url/url_util.h"
namespace content {
namespace {
class InterceptAndCancelDidCommitProvisionalLoad
: public DidCommitNavigationInterceptor {
public:
explicit InterceptAndCancelDidCommitProvisionalLoad(WebContents* web_contents)
: DidCommitNavigationInterceptor(web_contents) {}
~InterceptAndCancelDidCommitProvisionalLoad() override {}
void Wait(size_t number_of_messages) {
while (intercepted_messages_.size() < number_of_messages) {
loop_ = std::make_unique<base::RunLoop>();
loop_->Run();
}
}
const std::vector<raw_ptr<NavigationRequest, VectorExperimental>>&
intercepted_navigations() const {
return intercepted_navigations_;
}
const std::vector<mojom::DidCommitProvisionalLoadParamsPtr>&
intercepted_messages() const {
return intercepted_messages_;
}
protected:
bool WillProcessDidCommitNavigation(
RenderFrameHost* render_frame_host,
NavigationRequest* navigation_request,
mojom::DidCommitProvisionalLoadParamsPtr* params,
mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
override {
intercepted_navigations_.push_back(navigation_request);
intercepted_messages_.push_back(std::move(*params));
if (loop_)
loop_->Quit();
// Do not send the message to the RenderFrameHostImpl.
return false;
}
// Note: Do not dereference the intercepted_navigations_, they are used as
// indices in the RenderFrameHostImpl and not for themselves.
std::vector<raw_ptr<NavigationRequest, VectorExperimental>>
intercepted_navigations_;
std::vector<mojom::DidCommitProvisionalLoadParamsPtr> intercepted_messages_;
std::unique_ptr<base::RunLoop> loop_;
};
class RenderFrameHostImplForHistoryBackInterceptor
: public RenderFrameHostImpl {
public:
using RenderFrameHostImpl::RenderFrameHostImpl;
void GoToEntryAtOffset(int32_t offset,
bool has_user_gesture,
std::optional<blink::scheduler::TaskAttributionId>
soft_navigation_heuristics_task_id) override {
if (quit_handler_)
std::move(quit_handler_).Run();
}
void set_quit_handler(base::OnceClosure handler) {
quit_handler_ = std::move(handler);
}
private:
friend class RenderFrameHostFactoryForHistoryBackInterceptor;
base::OnceClosure quit_handler_;
};
class RenderFrameHostFactoryForHistoryBackInterceptor
: 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,
const blink::DocumentToken& document_token,
base::UnguessableToken devtools_frame_token,
bool renderer_initiated_creation,
RenderFrameHostImpl::LifecycleStateImpl lifecycle_state,
scoped_refptr<BrowsingContextState> browsing_context_state) override {
return base::WrapUnique(new RenderFrameHostImplForHistoryBackInterceptor(
site_instance, std::move(render_view_host), delegate, frame_tree,
frame_tree_node, routing_id, std::move(frame_remote), frame_token,
document_token, devtools_frame_token, renderer_initiated_creation,
lifecycle_state, std::move(browsing_context_state),
frame_tree_node->frame_owner_element_type(), frame_tree_node->parent(),
frame_tree_node->fenced_frame_status()));
}
};
// Simulate embedders of content/ keeping track of the current visible URL using
// NavigateStateChanged() and GetVisibleURL() API.
class EmbedderVisibleUrlTracker : public WebContentsDelegate {
public:
const GURL& url() { return url_; }
// WebContentsDelegate's implementation:
void NavigationStateChanged(WebContents* source,
InvalidateTypes changed_flags) override {
if (!(changed_flags & INVALIDATE_TYPE_URL))
return;
url_ = source->GetVisibleURL();
if (on_url_invalidated_)
std::move(on_url_invalidated_).Run();
}
void WaitUntilUrlInvalidated() {
base::RunLoop loop;
on_url_invalidated_ = loop.QuitClosure();
loop.Run();
}
private:
GURL url_;
base::OnceClosure on_url_invalidated_;
};
// Helper class. Immediately run a callback when a navigation starts.
class DidStartNavigationCallback final : public WebContentsObserver {
public:
explicit DidStartNavigationCallback(
WebContents* web_contents,
base::OnceCallback<void(NavigationHandle*)> callback)
: WebContentsObserver(web_contents), callback_(std::move(callback)) {}
~DidStartNavigationCallback() override = default;
private:
void DidStartNavigation(NavigationHandle* navigation_handle) override {
if (callback_)
std::move(callback_).Run(navigation_handle);
}
base::OnceCallback<void(NavigationHandle*)> callback_;
};
// Helper class. Immediately run a callback when a navigation finishes.
class DidFinishNavigationCallback final : public WebContentsObserver {
public:
explicit DidFinishNavigationCallback(
WebContents* web_contents,
base::OnceCallback<void(NavigationHandle*)> callback)
: WebContentsObserver(web_contents), callback_(std::move(callback)) {}
~DidFinishNavigationCallback() override = default;
private:
void DidFinishNavigation(NavigationHandle* navigation_handle) override {
if (callback_)
std::move(callback_).Run(navigation_handle);
}
base::OnceCallback<void(NavigationHandle*)> callback_;
};
const char* non_cacheable_html_response =
"HTTP/1.1 200 OK\n"
"cache-control: no-cache, no-store, must-revalidate\n"
"content-type: text/html; charset=UTF-8\n"
"\n"
"HTML content.";
// Insert a navigation throttle blocking every navigation in its
// WillProcessResponse handler.
std::unique_ptr<content::TestNavigationThrottleInserter>
BlockNavigationWillProcessResponse(WebContentsImpl* web_content) {
return std::make_unique<content::TestNavigationThrottleInserter>(
web_content,
base::BindLambdaForTesting(
[&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> {
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
throttle->SetResponse(TestNavigationThrottle::WILL_PROCESS_RESPONSE,
TestNavigationThrottle::SYNCHRONOUS,
NavigationThrottle::BLOCK_RESPONSE);
return throttle;
}));
}
void WaitForHistogramRecordedInChildProcess(std::string name) {
base::HistogramTester histogram_tester;
while (true) {
if (!histogram_tester.GetAllSamples(name).empty()) {
return;
}
// Retry fetching the histogram since it's not populated yet.
content::FetchHistogramsFromChildProcesses();
base::StatisticsRecorder::ImportProvidedHistogramsSync();
base::RunLoop().RunUntilIdle();
}
}
} // namespace
// Test about navigation.
// If you don't need a custom embedded test server, please use the next class
// below (NavigationBrowserTest), it will automatically start the
// default server.
class NavigationBaseBrowserTest : public ContentBrowserTest {
public:
NavigationBaseBrowserTest() {}
void PreRunTestOnMainThread() override {
ContentBrowserTest::PreRunTestOnMainThread();
test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
const ukm::TestAutoSetUkmRecorder& test_ukm_recorder() const {
return *test_ukm_recorder_;
}
protected:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
FrameTreeNode* main_frame() {
return web_contents()->GetPrimaryFrameTree().root();
}
RenderFrameHostImpl* current_frame_host() {
return main_frame()->current_frame_host();
}
private:
std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
};
class NavigationBrowserTest : public NavigationBaseBrowserTest {
protected:
void SetUpOnMainThread() override {
NavigationBaseBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
}
};
class NavigationGoToEntryAtOffsetBrowserTest : public NavigationBrowserTest {
public:
void SetQuitHandlerForGoToEntryAtOffset(base::OnceClosure handler) {
RenderFrameHostImplForHistoryBackInterceptor* render_frame_host =
static_cast<RenderFrameHostImplForHistoryBackInterceptor*>(
current_frame_host());
render_frame_host->set_quit_handler(std::move(handler));
}
private:
RenderFrameHostFactoryForHistoryBackInterceptor render_frame_host_factory_;
};
class NetworkIsolationNavigationBrowserTest : public ContentBrowserTest {
public:
NetworkIsolationNavigationBrowserTest() = default;
protected:
void SetUpOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->Start());
ContentBrowserTest::SetUpOnMainThread();
}
};
class NavigationBrowserTestReferrerPolicy
: public NavigationBrowserTest,
public ::testing::WithParamInterface<network::mojom::ReferrerPolicy> {
protected:
network::mojom::ReferrerPolicy GetReferrerPolicy() const {
return GetParam();
}
};
INSTANTIATE_TEST_SUITE_P(
All,
NavigationBrowserTestReferrerPolicy,
::testing::Values(
network::mojom::ReferrerPolicy::kAlways,
network::mojom::ReferrerPolicy::kDefault,
network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade,
network::mojom::ReferrerPolicy::kNever,
network::mojom::ReferrerPolicy::kOrigin,
network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin,
network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
network::mojom::ReferrerPolicy::kSameOrigin,
network::mojom::ReferrerPolicy::kStrictOrigin));
// Ensure that browser initiated basic navigations work.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BrowserInitiatedNavigations) {
// Perform a navigation with no live renderer.
{
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_FALSE(observer.last_initiator_origin().has_value());
EXPECT_FALSE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(ChildProcessHost::kInvalidUniqueID,
observer.last_initiator_process_id());
}
RenderFrameHost* initial_rfh = current_frame_host();
// Perform a same site navigation.
{
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_FALSE(observer.last_initiator_origin().has_value());
EXPECT_FALSE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(ChildProcessHost::kInvalidUniqueID,
observer.last_initiator_process_id());
}
RenderFrameHost* second_rfh = current_frame_host();
if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
// If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument
// is enabled, the navigation will result in a new RFH.
EXPECT_NE(initial_rfh, second_rfh);
} else {
EXPECT_EQ(initial_rfh, second_rfh);
}
// Perform a cross-site navigation.
{
TestNavigationObserver observer(web_contents());
GURL url = embedded_test_server()->GetURL("foo.com", "/title3.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_FALSE(observer.last_initiator_origin().has_value());
EXPECT_FALSE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(ChildProcessHost::kInvalidUniqueID,
observer.last_initiator_process_id());
}
// The RenderFrameHost should have changed.
EXPECT_NE(second_rfh, current_frame_host());
// Check the UKM for navigation responses received.
EXPECT_EQ(3u, test_ukm_recorder()
.GetEntriesByName("Navigation.ReceivedResponse")
.size());
}
// Ensure that renderer initiated same-site navigations work.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedSameSiteNavigation) {
// Perform a navigation with no live renderer.
{
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_FALSE(observer.last_initiator_origin().has_value());
EXPECT_FALSE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(ChildProcessHost::kInvalidUniqueID,
observer.last_initiator_process_id());
}
RenderFrameHost* initial_rfh = current_frame_host();
auto initial_rfh_global_token = initial_rfh->GetGlobalFrameToken();
// Simulate clicking on a same-site link.
{
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteLink();"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(current_frame_host()->GetLastCommittedOrigin(),
observer.last_initiator_origin());
EXPECT_TRUE(observer.last_initiator_frame_token().has_value());
if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
// If same-site ProactivelySwapBrowsingInstance or main-frame
// RenderDocument is enabled, the navigation will result in a new RFH, so
// we need to compare with |initial_rfh|.
EXPECT_NE(current_frame_host(), initial_rfh);
EXPECT_EQ(initial_rfh_global_token.frame_token,
observer.last_initiator_frame_token().value());
EXPECT_EQ(initial_rfh_global_token.child_id,
observer.last_initiator_process_id());
} else {
EXPECT_EQ(current_frame_host(), initial_rfh);
EXPECT_EQ(current_frame_host()->GetFrameToken(),
observer.last_initiator_frame_token().value());
EXPECT_EQ(current_frame_host()->GetProcess()->GetDeprecatedID(),
observer.last_initiator_process_id());
}
}
RenderFrameHost* second_rfh = current_frame_host();
if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
// If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument
// is enabled, the navigation will result in a new RFH.
EXPECT_NE(initial_rfh, second_rfh);
} else {
EXPECT_EQ(initial_rfh, second_rfh);
}
}
// Ensure that renderer initiated cross-site navigations work.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedCrossSiteNavigation) {
// Perform a navigation with no live renderer.
{
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
}
RenderFrameHost* initial_rfh = current_frame_host();
url::Origin initial_origin = initial_rfh->GetLastCommittedOrigin();
auto initial_rfh_global_token = initial_rfh->GetGlobalFrameToken();
// Simulate clicking on a cross-site link.
{
TestNavigationObserver observer(web_contents());
const char kReplacePortNumber[] = "setPortNumber(%d);";
uint16_t port_number = embedded_test_server()->port();
GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html");
EXPECT_EQ(true, EvalJs(shell(), base::StringPrintf(kReplacePortNumber,
port_number)));
EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteLink();"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(initial_origin, observer.last_initiator_origin().value());
EXPECT_TRUE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(initial_rfh_global_token.frame_token,
observer.last_initiator_frame_token().value());
EXPECT_EQ(initial_rfh_global_token.child_id,
observer.last_initiator_process_id());
}
// The RenderFrameHost should have changed unless full site isolation and
// proactive BrowsingInstance swaps are both disabled.
if (!AreAllSitesIsolatedForTesting() &&
!CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
EXPECT_EQ(initial_rfh, current_frame_host());
} else {
EXPECT_NE(initial_rfh, current_frame_host());
}
}
// Ensure navigation failures are handled.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FailedNavigation) {
// Perform a navigation with no live renderer.
{
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
// Check the UKM for navigation responses received.
EXPECT_EQ(1u, test_ukm_recorder()
.GetEntriesByName("Navigation.ReceivedResponse")
.size());
}
// Now navigate to an unreachable url.
{
TestNavigationObserver observer(web_contents());
GURL error_url(embedded_test_server()->GetURL("/close-socket"));
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&net::URLRequestFailedJob::AddUrlHandler));
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_EQ(error_url, observer.last_navigation_url());
NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
// No response on an unreachable URL, so the ReceivedResponse event should
// not have increased.
EXPECT_EQ(1u, test_ukm_recorder()
.GetEntriesByName("Navigation.ReceivedResponse")
.size());
}
}
// Ensure that browser initiated navigations to view-source URLs works.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
ViewSourceNavigation_BrowserInitiated) {
TestNavigationObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/title1.html"));
GURL view_source_url(content::kViewSourceScheme + std::string(":") +
url.spec());
EXPECT_TRUE(NavigateToURL(shell(), view_source_url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
}
// Ensure that content initiated navigations to view-sources URLs are blocked.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
ViewSourceNavigation_RendererInitiated) {
TestNavigationObserver observer(web_contents());
GURL kUrl(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), kUrl));
EXPECT_EQ(kUrl, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"Not allowed to load local resource: view-source:about:blank");
EXPECT_EQ(true, EvalJs(web_contents(), "clickViewSourceLink();"));
ASSERT_TRUE(console_observer.Wait());
// Original page shouldn't navigate away.
EXPECT_EQ(kUrl, web_contents()->GetLastCommittedURL());
EXPECT_FALSE(shell()
->web_contents()
->GetController()
.GetLastCommittedEntry()
->IsViewSourceMode());
}
// Ensure that content initiated navigations to googlechrome: URLs are blocked.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
GoogleChromeNavigation_RendererInitiated) {
TestNavigationObserver observer(web_contents());
GURL kUrl(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), kUrl));
EXPECT_EQ(kUrl, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"Not allowed to load local resource: googlechrome://");
EXPECT_EQ(true, EvalJs(web_contents(), "clickGoogleChromeLink();"));
ASSERT_TRUE(console_observer.Wait());
// Original page shouldn't navigate away.
EXPECT_EQ(kUrl, web_contents()->GetLastCommittedURL());
}
// Ensure that closing a page by running its beforeunload handler doesn't hang
// if there's an ongoing navigation.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, UnloadDuringNavigation) {
WebContentsDestroyedWatcher close_observer(web_contents());
GURL url("chrome://resources/css/tabs.css");
NavigationHandleObserver handle_observer(web_contents(), url);
shell()->LoadURL(url);
web_contents()->DispatchBeforeUnload(false /* auto_cancel */);
close_observer.Wait();
EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code());
}
// Ensure that the referrer of a navigation is properly sanitized.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SanitizeReferrer) {
const GURL kInsecureUrl(embedded_test_server()->GetURL("/title1.html"));
const Referrer kSecureReferrer(
GURL("https://secure-url.com"),
network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade);
// Navigate to an insecure url with a secure referrer with a policy of no
// referrer on downgrades. The referrer url should be rewritten right away.
NavigationController::LoadURLParams load_params(kInsecureUrl);
load_params.referrer = kSecureReferrer;
TestNavigationManager manager(web_contents(), kInsecureUrl);
web_contents()->GetController().LoadURLWithParams(load_params);
EXPECT_TRUE(manager.WaitForRequestStart());
// The referrer should have been sanitized.
ASSERT_TRUE(main_frame()->navigation_request());
EXPECT_EQ(GURL(), main_frame()->navigation_request()->GetReferrer().url);
// The navigation should commit without being blocked.
EXPECT_TRUE(manager.WaitForResponse());
ASSERT_TRUE(manager.WaitForNavigationFinished());
EXPECT_EQ(kInsecureUrl, web_contents()->GetLastCommittedURL());
}
// Ensure the correctness of a navigation request's referrer. This is a
// regression test for https://crbug.com/1004083.
IN_PROC_BROWSER_TEST_P(NavigationBrowserTestReferrerPolicy, ReferrerPolicy) {
const GURL kDestination(embedded_test_server()->GetURL("/title1.html"));
const GURL kReferrerURL(embedded_test_server()->GetURL("/referrer-page"));
const url::Origin kReferrerOrigin = url::Origin::Create(kReferrerURL);
// It is possible that the referrer URL does not match what the policy
// demands (e.g., non-empty URL and kNever policy), so we'll test that the
// correct referrer is generated, and that the navigation succeeds.
const Referrer referrer(kReferrerURL, GetReferrerPolicy());
// Navigate to a resource whose destination URL is same-origin with the
// navigation's referrer. The final referrer should be generated correctly.
NavigationController::LoadURLParams load_params(kDestination);
load_params.referrer = referrer;
TestNavigationManager manager(web_contents(), kDestination);
web_contents()->GetController().LoadURLWithParams(load_params);
EXPECT_TRUE(manager.WaitForRequestStart());
// The referrer should have been sanitized.
ASSERT_TRUE(main_frame()->navigation_request());
switch (GetReferrerPolicy()) {
case network::mojom::ReferrerPolicy::kAlways:
case network::mojom::ReferrerPolicy::kDefault:
case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade:
case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin:
case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin:
case network::mojom::ReferrerPolicy::kSameOrigin:
EXPECT_EQ(kReferrerURL,
main_frame()->navigation_request()->GetReferrer().url);
break;
case network::mojom::ReferrerPolicy::kNever:
EXPECT_EQ(GURL(), main_frame()->navigation_request()->GetReferrer().url);
break;
case network::mojom::ReferrerPolicy::kOrigin:
case network::mojom::ReferrerPolicy::kStrictOrigin:
EXPECT_EQ(kReferrerOrigin.GetURL(),
main_frame()->navigation_request()->GetReferrer().url);
break;
}
// The navigation should commit without being blocked.
EXPECT_TRUE(manager.WaitForResponse());
ASSERT_TRUE(manager.WaitForNavigationFinished());
EXPECT_EQ(kDestination, web_contents()->GetLastCommittedURL());
}
// Test to verify that an exploited renderer process trying to upload a file
// it hasn't been explicitly granted permissions to is correctly terminated.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, PostUploadIllegalFilePath) {
GURL form_url(
embedded_test_server()->GetURL("/form_that_posts_to_echoall.html"));
EXPECT_TRUE(NavigateToURL(shell(), form_url));
// Prepare a file for the upload form.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
base::FilePath file_path;
std::string file_content("test-file-content");
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
ASSERT_TRUE(base::WriteFile(file_path, file_content));
base::RunLoop run_loop;
// Fill out the form to refer to the test file.
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file_path, run_loop.QuitClosure()));
web_contents()->SetDelegate(delegate.get());
EXPECT_TRUE(
ExecJs(web_contents(), "document.getElementById('file').click();"));
run_loop.Run();
// Ensure that the process is allowed to access to the chosen file and
// does not have access to the other file name.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
current_frame_host()->GetProcess()->GetDeprecatedID(), file_path));
// Revoke the access to the file and submit the form. The renderer process
// should be terminated.
RenderProcessHostBadIpcMessageWaiter process_kill_waiter(
current_frame_host()->GetProcess());
ChildProcessSecurityPolicyImpl* security_policy =
ChildProcessSecurityPolicyImpl::GetInstance();
security_policy->RevokeAllPermissionsForFile(
current_frame_host()->GetProcess()->GetDeprecatedID(), file_path);
// Use EvalJs and respond back to the browser process before doing the actual
// submission. This will ensure that the process termination is guaranteed to
// arrive after the response from the executed JavaScript.
EXPECT_EQ(
true,
EvalJs(
shell(),
"setTimeout(() => document.getElementById('file-form').submit(), 0);"
"true;"));
EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, process_kill_waiter.Wait());
}
// Test case to verify that redirects to data: URLs are properly disallowed,
// even when invoked through a reload.
// See https://crbug.com/723796.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
VerifyBlockedErrorPageURL_Reload) {
NavigationControllerImpl& controller = web_contents()->GetController();
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
// Navigate to an URL, which redirects to a data: URL, since it is an
// unsafe redirect and will result in a blocked navigation and error page.
GURL redirect_to_blank_url(
embedded_test_server()->GetURL("/server-redirect?data:text/html,Hello!"));
EXPECT_FALSE(NavigateToURL(shell(), redirect_to_blank_url));
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());
TestNavigationObserver reload_observer(web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload()"));
reload_observer.Wait();
// The expectation is that the blocked URL is present in the NavigationEntry,
// and shows up in both GetURL and GetVirtualURL.
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->GetURL().SchemeIs(url::kDataScheme));
EXPECT_EQ(redirect_to_blank_url,
controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(redirect_to_blank_url,
controller.GetLastCommittedEntry()->GetVirtualURL());
}
// TODO(crbug.com/40924471): Test is flaky on Android, Linux.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX)
#define MAYBE_BackFollowedByReload DISABLED_BackFollowedByReload
#else
#define MAYBE_BackFollowedByReload BackFollowedByReload
#endif
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, MAYBE_BackFollowedByReload) {
// First, make two history entries.
GURL url1(embedded_test_server()->GetURL("/title1.html"));
GURL url2(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(NavigateToURL(shell(), url2));
// Then execute a back navigation in Javascript followed by a reload.
TestNavigationObserver navigation_observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "history.back(); location.reload();"));
navigation_observer.Wait();
// The reload should have cancelled the back navigation, and the last
// committed URL should still be the second URL.
EXPECT_EQ(url2, web_contents()->GetLastCommittedURL());
}
// Test that a navigation response can be entirely fetched, even after the
// NavigationURLLoader has been deleted.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
FetchResponseAfterNavigationURLLoaderDeleted) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/main_document");
ASSERT_TRUE(embedded_test_server()->Start());
// Load a new document.
GURL url(embedded_test_server()->GetURL("/main_document"));
TestNavigationManager navigation_manager(web_contents(), url);
shell()->LoadURL(url);
// The navigation starts.
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
// A NavigationRequest exists at this point.
EXPECT_TRUE(main_frame()->navigation_request());
// The response's headers are received.
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n"
"...");
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
// The renderer commits the navigation and the browser deletes its
// NavigationRequest.
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_FALSE(main_frame()->navigation_request());
// The NavigationURLLoader has been deleted by now. Check that the renderer
// can still receive more bytes.
DOMMessageQueue dom_message_queue(web_contents());
response.Send(
"<script>window.domAutomationController.send('done');</script>");
std::string done;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&done));
EXPECT_EQ("\"done\"", done);
}
IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest,
BrowserNavigationNetworkIsolationKey) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
url::Origin origin = url::Origin::Create(url);
URLLoaderMonitor monitor({url});
EXPECT_TRUE(NavigateToURL(shell(), url));
monitor.WaitForUrls();
std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url);
ASSERT_TRUE(request->trusted_params);
EXPECT_TRUE(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin))
.IsEqualForTesting(request->trusted_params->isolation_info));
}
IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest,
RenderNavigationIsolationInfo) {
GURL url(embedded_test_server()->GetURL("/title2.html"));
url::Origin origin = url::Origin::Create(url);
EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
URLLoaderMonitor monitor({url});
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url));
monitor.WaitForUrls();
std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url);
ASSERT_TRUE(request->trusted_params);
EXPECT_TRUE(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin))
.IsEqualForTesting(request->trusted_params->isolation_info));
}
IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest,
SubframeIsolationInfo) {
GURL url(embedded_test_server()->GetURL("/page_with_iframe.html"));
GURL iframe_document = embedded_test_server()->GetURL("/title1.html");
url::Origin origin = url::Origin::Create(url);
url::Origin iframe_origin = url::Origin::Create(iframe_document);
URLLoaderMonitor monitor({iframe_document});
EXPECT_TRUE(NavigateToURL(shell(), url));
monitor.WaitForUrls();
std::optional<network::ResourceRequest> main_frame_request =
monitor.GetRequestInfo(url);
ASSERT_TRUE(main_frame_request.has_value());
ASSERT_TRUE(main_frame_request->trusted_params);
EXPECT_TRUE(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin))
.IsEqualForTesting(
main_frame_request->trusted_params->isolation_info));
std::optional<network::ResourceRequest> iframe_request =
monitor.GetRequestInfo(iframe_document);
ASSERT_TRUE(iframe_request->trusted_params);
EXPECT_TRUE(
net::IsolationInfo::Create(net::IsolationInfo::RequestType::kSubFrame,
origin, iframe_origin,
net::SiteForCookies::FromOrigin(origin))
.IsEqualForTesting(iframe_request->trusted_params->isolation_info));
}
// Tests that the initiator is not set for a browser initiated top frame
// navigation.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BrowserNavigationInitiator) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
URLLoaderMonitor monitor;
// Perform the actual navigation.
EXPECT_TRUE(NavigateToURL(shell(), url));
std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url);
ASSERT_TRUE(request.has_value());
ASSERT_FALSE(request->request_initiator.has_value());
}
// Test that the initiator is set to the starting page when a renderer initiated
// navigation goes from the starting page to another page.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererNavigationInitiator) {
GURL starting_page(embedded_test_server()->GetURL("a.com", "/title1.html"));
url::Origin starting_page_origin;
starting_page_origin = starting_page_origin.Create(starting_page);
EXPECT_TRUE(NavigateToURL(shell(), starting_page));
GURL url(embedded_test_server()->GetURL("/title2.html"));
URLLoaderMonitor monitor;
// Perform the actual navigation.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url));
std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url);
ASSERT_TRUE(request.has_value());
EXPECT_EQ(starting_page_origin, request->request_initiator);
}
// Test that the initiator is set to the starting page when a sub frame is
// navigated by Javascript from some starting page to another page.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SubFrameJsNavigationInitiator) {
GURL starting_page(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), starting_page));
// The main_frame() and subframe should each have a live RenderFrame.
EXPECT_TRUE(main_frame()
->current_frame_host()
->render_view_host()
->IsRenderViewLive());
EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(
main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive());
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
URLLoaderMonitor monitor({url});
std::string script = "location.href='" + url.spec() + "'";
// Perform the actual navigation.
EXPECT_TRUE(ExecJs(main_frame()->child_at(0)->current_frame_host(), script));
EXPECT_TRUE(main_frame()
->current_frame_host()
->render_view_host()
->IsRenderViewLive());
EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(
main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive());
url::Origin starting_page_origin;
starting_page_origin = starting_page_origin.Create(starting_page);
monitor.WaitForUrls();
std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url);
EXPECT_EQ(starting_page_origin, request->request_initiator);
}
// Test that the initiator is set to the starting page when a sub frame,
// selected by Id, is navigated by Javascript from some starting page to another
// page.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SubframeNavigationByTopFrameInitiator) {
// Go to a page on a.com with an iframe that is on b.com
GURL starting_page(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), starting_page));
// The main_frame and subframe should each have a live RenderFrame.
EXPECT_TRUE(main_frame()
->current_frame_host()
->render_view_host()
->IsRenderViewLive());
EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(
main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive());
GURL url(embedded_test_server()->GetURL("c.com", "/title1.html"));
URLLoaderMonitor monitor;
// Perform the actual navigation.
NavigateIframeToURL(web_contents(), "child-0", url);
EXPECT_TRUE(main_frame()
->current_frame_host()
->render_view_host()
->IsRenderViewLive());
EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(
main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive());
url::Origin starting_page_origin;
starting_page_origin = starting_page_origin.Create(starting_page);
std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url);
ASSERT_TRUE(request.has_value());
EXPECT_EQ(starting_page_origin, request->request_initiator);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedCrossSiteNewWindowInitator) {
GURL url(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
auto initiator_global_token = current_frame_host()->GetGlobalFrameToken();
// Simulate clicking on a cross-site link.
{
const char kReplacePortNumber[] = "setPortNumber(%d);";
uint16_t port_number = embedded_test_server()->port();
url = embedded_test_server()->GetURL("foo.com", "/title2.html");
EXPECT_TRUE(
ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number)));
TestNavigationObserver observer(url);
observer.StartWatchingNewWebContents();
EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteNewWindowLink();"));
observer.Wait();
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_TRUE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(initiator_global_token.frame_token,
observer.last_initiator_frame_token().value());
EXPECT_EQ(initiator_global_token.child_id,
observer.last_initiator_process_id());
}
}
// Ensure that renderer initiated navigations which have the opener suppressed
// work.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedNewWindowNoOpenerNavigation) {
GURL url(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHost* initial_rfh = current_frame_host();
url::Origin initial_origin = initial_rfh->GetLastCommittedOrigin();
auto initiator_global_token = initial_rfh->GetGlobalFrameToken();
// Simulate clicking on a cross-site link which has rel="noopener".
{
const char kReplacePortNumber[] = "setPortNumber(%d);";
uint16_t port_number = embedded_test_server()->port();
url = embedded_test_server()->GetURL("foo.com", "/title2.html");
EXPECT_TRUE(
ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number)));
TestNavigationObserver observer(url);
observer.StartWatchingNewWebContents();
EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteNewWindowNoOpenerLink();"));
observer.Wait();
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(initial_origin, observer.last_initiator_origin().value());
EXPECT_TRUE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(initiator_global_token.frame_token,
observer.last_initiator_frame_token().value());
EXPECT_EQ(initiator_global_token.child_id,
observer.last_initiator_process_id());
}
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedWithSubframeInitator) {
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a())"));
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL subframe_url =
embedded_test_server()->GetURL("a.com", "/simple_links.html");
EXPECT_TRUE(
NavigateToURLFromRenderer(main_frame()->child_at(0), subframe_url));
RenderFrameHostImpl* subframe_rfh =
current_frame_host()->child_at(0)->current_frame_host();
auto initiator_global_token = subframe_rfh->GetGlobalFrameToken();
// Simulate clicking on a cross-site link.
{
const char kReplacePortNumber[] = "setPortNumber(%d);";
uint16_t port_number = embedded_test_server()->port();
url = embedded_test_server()->GetURL("foo.com", "/title2.html");
EXPECT_TRUE(ExecJs(subframe_rfh,
base::StringPrintf(kReplacePortNumber, port_number)));
TestNavigationObserver observer(url);
observer.StartWatchingNewWebContents();
EXPECT_EQ(true, EvalJs(subframe_rfh, "clickCrossSiteNewWindowLink();"));
observer.Wait();
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_TRUE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(initiator_global_token.frame_token,
observer.last_initiator_frame_token().value());
EXPECT_EQ(initiator_global_token.child_id,
observer.last_initiator_process_id());
}
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
InitiatorFrameStateConsistentAtDidStartNavigation) {
GURL form_page_url(embedded_test_server()->GetURL(
"a.com", "/form_that_posts_to_echoall.html"));
EXPECT_TRUE(NavigateToURL(shell(), form_page_url));
// Give the form an action that will navigate to a slow page.
GURL form_action_url(embedded_test_server()->GetURL("b.com", "/slow?100"));
EXPECT_TRUE(
ExecJs(shell(), JsReplace("document.getElementById('form').action = $1",
form_action_url)));
// Open a new window that can be targeted by the form submission.
WebContents* form_contents = web_contents();
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(shell(), "window.open('about:blank', 'target_frame');"));
WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
EXPECT_TRUE(
ExecJs(form_contents,
"document.getElementById('form').target = 'target_frame';"));
TestNavigationManager popup_manager(popup_contents, form_action_url);
TestNavigationManager form_manager(
form_contents, embedded_test_server()->GetURL("a.com", "/title2.html"));
// Submit the form and navigate the form's page.
EXPECT_TRUE(ExecJs(form_contents, "window.location.href = 'title2.html'"));
EXPECT_TRUE(
ExecJs(form_contents, "document.getElementById('form').submit();"));
// The form page's navigation should start prior to the form navigation.
EXPECT_TRUE(form_manager.WaitForRequestStart());
EXPECT_FALSE(popup_manager.GetNavigationHandle());
// When the navigation starts for the popup, ensure that the original page has
// not finished navigating. If this was not the case, we could not make any
// statements on the validity of initiator state during a navigation.
// Navigation handles are only available prior to DidFinishNavigation().
EXPECT_TRUE(popup_manager.WaitForRequestStart());
EXPECT_TRUE(form_manager.GetNavigationHandle());
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedMiddleClickInitator) {
GURL url(embedded_test_server()->GetURL("/simple_links.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
auto initiator_global_token = current_frame_host()->GetGlobalFrameToken();
// Simulate middle-clicking on a cross-site link.
{
const char kReplacePortNumber[] = "setPortNumber(%d);";
uint16_t port_number = embedded_test_server()->port();
url = embedded_test_server()->GetURL("foo.com", "/title2.html");
EXPECT_TRUE(
ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number)));
TestNavigationObserver observer(url);
observer.StartWatchingNewWebContents();
EXPECT_EQ(true, EvalJs(shell(), R"(
target = document.getElementById('cross_site_link');
var evt = new MouseEvent("click", {"button": 1 /* middle_button */});
target.dispatchEvent(evt);)"));
observer.Wait();
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_TRUE(observer.last_initiator_frame_token().has_value());
EXPECT_EQ(initiator_global_token.frame_token,
observer.last_initiator_frame_token().value());
EXPECT_EQ(initiator_global_token.child_id,
observer.last_initiator_process_id());
}
}
// Data URLs can have a reference fragment like any other URLs. This test makes
// sure it is taken into account.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, DataURLWithReferenceFragment) {
GURL url("data:text/html,body#foo");
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ("body", EvalJs(shell(), "document.body.textContent;"));
EXPECT_EQ("#foo", EvalJs(shell(), "location.hash;"));
}
// Regression test for https://crbug.com/796561.
// 1) Start on a document with history.length == 1.
// 2) Create an iframe and call history.pushState at the same time.
// 3) history.back() must work.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
IframeAndPushStateSimultaneously) {
GURL main_url = embedded_test_server()->GetURL("/simple_page.html");
GURL iframe_url = embedded_test_server()->GetURL("/hello.html");
// 1) Start on a new document such that history.length == 1.
{
EXPECT_TRUE(NavigateToURL(shell(), main_url));
EXPECT_EQ(1, EvalJs(shell(), "history.length"));
}
// 2) Create an iframe and call history.pushState at the same time.
{
TestNavigationManager iframe_navigation(web_contents(), iframe_url);
ExecuteScriptAsync(shell(),
"let iframe = document.createElement('iframe');"
"iframe.src = '/hello.html';"
"document.body.appendChild(iframe);");
EXPECT_TRUE(iframe_navigation.WaitForRequestStart());
// The iframe navigation is paused. In the meantime, a pushState navigation
// begins and ends.
TestNavigationManager push_state_navigation(web_contents(), main_url);
ExecuteScriptAsync(shell(), "window.history.pushState({}, null);");
ASSERT_TRUE(push_state_navigation.WaitForNavigationFinished());
// The iframe navigation is resumed.
ASSERT_TRUE(iframe_navigation.WaitForNavigationFinished());
}
// 3) history.back() must work.
{
TestNavigationObserver navigation_observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "history.back();"));
navigation_observer.Wait();
}
}
// Regression test for https://crbug.com/260144
// Back/Forward navigation in an iframe must not stop ongoing XHR.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
IframeNavigationsDoNotStopXHR) {
// A response for the XHR request. It will be delayed until the end of all the
// navigations.
net::test_server::ControllableHttpResponse xhr_response(
embedded_test_server(), "/xhr");
EXPECT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
DOMMessageQueue dom_message_queue(web_contents());
std::string message;
// 1) Send an XHR.
ExecuteScriptAsync(
shell(),
"let xhr = new XMLHttpRequest();"
"xhr.open('GET', './xhr', true);"
"xhr.onabort = () => window.domAutomationController.send('xhr.onabort');"
"xhr.onerror = () => window.domAutomationController.send('xhr.onerror');"
"xhr.onload = () => window.domAutomationController.send('xhr.onload');"
"xhr.send();");
// 2) Create an iframe and wait for the initial load.
{
ExecuteScriptAsync(
shell(),
"var iframe = document.createElement('iframe');"
"iframe.src = './title1.html';"
"iframe.onload = function() {"
" window.domAutomationController.send('iframe.onload');"
"};"
"document.body.appendChild(iframe);");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe.onload\"", message);
}
// 3) Navigate the iframe elsewhere.
{
ExecuteScriptAsync(shell(),
"var iframe = document.querySelector('iframe');"
"iframe.src = './title2.html';");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe.onload\"", message);
}
// 4) history.back() in the iframe.
{
ExecuteScriptAsync(shell(),
"var iframe = document.querySelector('iframe');"
"iframe.contentWindow.history.back()");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe.onload\"", message);
}
// 5) history.forward() in the iframe.
{
ExecuteScriptAsync(shell(),
"var iframe = document.querySelector('iframe');"
"iframe.contentWindow.history.forward()");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe.onload\"", message);
}
// 6) Wait for the XHR.
{
xhr_response.WaitForRequest();
xhr_response.Send(
"HTTP/1.1 200 OK\r\n"
"Connection: close\r\n"
"Content-Length: 2\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"\r\n"
"OK");
xhr_response.Done();
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"xhr.onload\"", message);
}
EXPECT_FALSE(dom_message_queue.PopMessage(&message));
}
// Regression test for https://crbug.com/856396.
// Note that original issue for the bug is not applicable anymore, because there
// is no provisional document loader which has not committed yet. We keep the
// modified version of this test to check removing iframe from the load event
// handler.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
ReplacingDocumentLoaderFiresLoadEvent) {
net::test_server::ControllableHttpResponse main_document_response(
embedded_test_server(), "/main_document");
net::test_server::ControllableHttpResponse iframe_response(
embedded_test_server(), "/iframe");
ASSERT_TRUE(embedded_test_server()->Start());
// 1) Load the main document.
shell()->LoadURL(embedded_test_server()->GetURL("/main_document"));
main_document_response.WaitForRequest();
main_document_response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n"
"<script>"
" var detach_iframe = function() {"
" var iframe = document.querySelector('iframe');"
" iframe.parentNode.removeChild(iframe);"
" }"
"</script>"
"<body onload='detach_iframe()'>"
" <iframe src='/iframe'></iframe>"
"</body>");
main_document_response.Done();
// 2) The iframe starts to load, but the server only have time to send the
// response's headers, not the response's body. This should commit the
// iframe's load.
iframe_response.WaitForRequest();
iframe_response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
// 3) In the meantime the iframe navigates elsewhere. It causes the previous
// DocumentLoader to be replaced by the new one. Removing it may
// trigger the 'load' event and delete the iframe.
EXPECT_TRUE(
ExecJs(shell(), "document.querySelector('iframe').src = '/title1.html'"));
// 4) Finish the original request.
iframe_response.Done();
// Wait for the iframe to be deleted and check the renderer process is still
// alive.
int iframe_count = 1;
while (iframe_count != 0) {
iframe_count =
EvalJs(
shell(),
"var iframe_count = document.getElementsByTagName('iframe').length;"
"iframe_count;")
.ExtractInt();
}
}
class NavigationDownloadBrowserTest : public NavigationBaseBrowserTest {
protected:
void SetUpOnMainThread() override {
NavigationBaseBrowserTest::SetUpOnMainThread();
// Set up a test download directory, in order to prevent prompting for
// handling downloads.
ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir());
ShellDownloadManagerDelegate* delegate =
static_cast<ShellDownloadManagerDelegate*>(
web_contents()->GetBrowserContext()->GetDownloadManagerDelegate());
delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath());
}
private:
base::ScopedTempDir downloads_directory_;
};
// Regression test for https://crbug.com/855033
// 1) A page contains many scripts and DOM elements. It forces the parser to
// yield CPU to other tasks. That way the response body's data are not fully
// read when URLLoaderClient::OnComplete(..) is received.
// 2) A script makes the document navigates elsewhere while it is still loading.
// It cancels the parser of the current document. Due to a bug, the document
// loader was not marked to be 'loaded' at this step.
// 3) The request for the new navigation starts and it turns out it is a
// download. The navigation is dropped.
// 4) There are no more possibilities for DidStopLoading() to be sent.
IN_PROC_BROWSER_TEST_F(NavigationDownloadBrowserTest,
StopLoadingAfterDroppedNavigation) {
net::test_server::ControllableHttpResponse main_response(
embedded_test_server(), "/main");
ASSERT_TRUE(embedded_test_server()->Start());
GURL main_url(embedded_test_server()->GetURL("/main"));
GURL download_url(embedded_test_server()->GetURL("/download-test1.lib"));
shell()->LoadURL(main_url);
main_response.WaitForRequest();
std::string headers =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n";
// Craft special HTML to make the blink::DocumentParser yield CPU to other
// tasks. The goal is to ensure the response body datapipe is not fully read
// when URLLoaderClient::OnComplete() is called.
// This relies on the HTMLParserScheduler::ShouldYield() heuristics.
std::string mix_of_script_and_div = "<script></script><div></div>";
for (size_t i = 0; i < 10; ++i) {
mix_of_script_and_div += mix_of_script_and_div; // Exponential growth.
}
std::string navigate_to_download =
"<script>location.href='" + download_url.spec() + "'</script>";
main_response.Send(headers + navigate_to_download + mix_of_script_and_div);
main_response.Done();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
}
// Renderer initiated back/forward navigation in beforeunload should not prevent
// the user to navigate away from a website.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, HistoryBackInBeforeUnload) {
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
EXPECT_TRUE(ExecJs(web_contents(),
"onbeforeunload = function() {"
" history.pushState({}, null, '/');"
" history.back();"
"};",
EXECUTE_SCRIPT_NO_USER_GESTURE));
EXPECT_TRUE(NavigateToURL(shell(), url_2));
}
// Same as 'HistoryBackInBeforeUnload', but wraps history.back() inside
// window.setTimeout(). Thus it is executed "outside" of its beforeunload
// handler and thus avoid basic navigation circumventions.
// Regression test for: https://crbug.com/879965.
IN_PROC_BROWSER_TEST_F(NavigationGoToEntryAtOffsetBrowserTest,
HistoryBackInBeforeUnloadAfterSetTimeout) {
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
EXPECT_TRUE(ExecJs(web_contents(),
"onbeforeunload = function() {"
" history.pushState({}, null, '/');"
" setTimeout(()=>history.back());"
"};",
EXECUTE_SCRIPT_NO_USER_GESTURE));
TestNavigationManager navigation(web_contents(), url_2);
base::RunLoop run_loop;
SetQuitHandlerForGoToEntryAtOffset(run_loop.QuitClosure());
shell()->LoadURL(url_2);
run_loop.Run();
ASSERT_TRUE(navigation.WaitForNavigationFinished());
EXPECT_TRUE(navigation.was_successful());
}
// Renderer initiated back/forward navigation can't cancel an ongoing browser
// initiated navigation if it is not user initiated.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
HistoryBackCancelPendingNavigationNoUserGesture) {
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
// 1) A pending browser initiated navigation (omnibox, ...) starts.
TestNavigationManager navigation(web_contents(), url_2);
shell()->LoadURL(url_2);
EXPECT_TRUE(navigation.WaitForRequestStart());
// 2) history.back() is sent but is not user initiated.
EXPECT_TRUE(ExecJs(web_contents(),
"history.pushState({}, null, '/');"
"history.back();",
EXECUTE_SCRIPT_NO_USER_GESTURE));
// 3) The first pending navigation is not canceled and can continue.
ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Resume navigation.
EXPECT_TRUE(navigation.was_successful());
}
// Renderer initiated back/forward navigation can cancel an ongoing browser
// initiated navigation if it is user initiated.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
HistoryBackCancelPendingNavigationUserGesture) {
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
// 1) A pending browser initiated navigation (omnibox, ...) starts.
TestNavigationManager navigation(web_contents(), url_2);
shell()->LoadURL(url_2);
EXPECT_TRUE(navigation.WaitForRequestStart());
// 2) history.back() is sent and is user initiated.
EXPECT_TRUE(ExecJs(web_contents(),
"history.pushState({}, null, '/');"
"history.back();"));
// 3) Check the first pending navigation has been canceled.
ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Resume navigation.
EXPECT_FALSE(navigation.was_successful());
}
// Ensure the renderer process doesn't send too many IPC to the browser process
// when history.pushState() and history.back() are called in a loop.
// Failing to do so causes the browser to become unresponsive.
// See https://crbug.com/882238
// TODO(crbug.com/379844650): Disabled on Linux sanitizer bots due to flakiness.
#if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER)
#define MAYBE_IPCFlood_GoToEntryAtOffset DISABLED_IPCFlood_GoToEntryAtOffset
#else
#define MAYBE_IPCFlood_GoToEntryAtOffset IPCFlood_GoToEntryAtOffset
#endif
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, MAYBE_IPCFlood_GoToEntryAtOffset) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"Throttling navigation to prevent the browser from hanging. See "
"https://crbug.com/1038223. Command line switch "
"--disable-ipc-flooding-protection can be used to bypass the "
"protection");
EXPECT_TRUE(ExecJs(shell(), R"(
for(let i = 0; i<1000; ++i) {
history.pushState({},"page 2", "bar.html");
history.back();
}
)"));
ASSERT_TRUE(console_observer.Wait());
}
// Ensure the renderer process doesn't send too many IPC to the browser process
// when doing a same-document navigation is requested in a loop.
// Failing to do so causes the browser to become unresponsive.
// TODO(arthursonzogni): Make the same test, but when the navigation is
// requested from a remote frame.
// See https://crbug.com/882238
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, IPCFlood_Navigation) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"Throttling navigation to prevent the browser from hanging. See "
"https://crbug.com/1038223. Command line switch "
"--disable-ipc-flooding-protection can be used to bypass the "
"protection");
EXPECT_TRUE(ExecJs(shell(), R"(
for(let i = 0; i<1000; ++i) {
location.href = "#" + i;
++i;
}
)"));
ASSERT_TRUE(console_observer.Wait());
}
// TODO(http://crbug.com/632514): This test currently expects opener downloads
// go through, but when the linked bug is resolved the download should be
// disallowed.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OpenerNavigation_DownloadPolicy) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir download_dir;
ASSERT_TRUE(download_dir.CreateUniqueTempDir());
ShellDownloadManagerDelegate* delegate =
static_cast<ShellDownloadManagerDelegate*>(
web_contents()->GetBrowserContext()->GetDownloadManagerDelegate());
delegate->SetDownloadBehaviorForTesting(download_dir.GetPath());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
// Open a popup.
EXPECT_EQ(true, EvalJs(web_contents(), "!!window.open();"));
EXPECT_EQ(2u, Shell::windows().size());
// Using the popup, navigate its opener to a download.
base::HistogramTester histograms;
WebContents* popup = Shell::windows()[1]->web_contents();
EXPECT_NE(popup, web_contents());
DownloadTestObserverInProgress observer(
web_contents()->GetBrowserContext()->GetDownloadManager(),
1 /* wait_count */);
EXPECT_TRUE(ExecJs(
popup,
"window.opener.location ='data:html/text;base64,'+btoa('payload');",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.WaitForFinished();
}
// A variation of the OpenerNavigation_DownloadPolicy test above, but uses a
// cross-origin URL for the popup window.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
CrossOriginOpenerNavigation_DownloadPolicy) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir download_dir;
ASSERT_TRUE(download_dir.CreateUniqueTempDir());
ShellDownloadManagerDelegate* delegate =
static_cast<ShellDownloadManagerDelegate*>(
web_contents()->GetBrowserContext()->GetDownloadManagerDelegate());
delegate->SetDownloadBehaviorForTesting(download_dir.GetPath());
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a popup.
ShellAddedObserver shell_observer;
EXPECT_TRUE(EvalJs(web_contents(), JsReplace("!!window.open($1);",
embedded_test_server()->GetURL(
"bar.com", "/title1.html")))
.ExtractBool());
Shell* new_shell = shell_observer.GetShell();
EXPECT_EQ(2u, Shell::windows().size());
// Wait for the navigation in the popup to complete, so the origin of the
// document will be correct.
WebContents* popup = new_shell->web_contents();
EXPECT_NE(popup, web_contents());
EXPECT_TRUE(WaitForLoadStop(popup));
// Using the popup, navigate its opener to a download.
base::HistogramTester histograms;
const GURL data_url("data:html/text;base64,cGF5bG9hZA==");
TestNavigationManager manager(web_contents(), data_url);
EXPECT_TRUE(ExecJs(popup, base::StringPrintf("window.opener.location ='%s'",
data_url.spec().c_str())));
ASSERT_TRUE(manager.WaitForNavigationFinished());
EXPECT_FALSE(manager.was_successful());
}
// Regression test for https://crbug.com/872284.
// A NavigationThrottle cancels a download in WillProcessResponse.
// The navigation request must be canceled and it must also cancel the network
// request. Failing to do so resulted in the network socket being leaked.
IN_PROC_BROWSER_TEST_F(NavigationDownloadBrowserTest,
CancelDownloadOnResponseStarted) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Block every iframe in WillProcessResponse.
content::TestNavigationThrottleInserter throttle_inserter(
web_contents(),
base::BindLambdaForTesting(
[&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> {
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
throttle->SetResponse(TestNavigationThrottle::WILL_PROCESS_RESPONSE,
TestNavigationThrottle::SYNCHRONOUS,
NavigationThrottle::CANCEL_AND_IGNORE);
return throttle;
}));
// Insert enough iframes so that if sockets are not properly released: there
// will not be enough of them to complete all navigations. As of today, only 6
// sockets can be used simultaneously. So using 7 iframes is enough. This test
// uses 33 as a margin.
EXPECT_TRUE(ExecJs(shell(), R"(
for(let i = 0; i<33; ++i) {
let iframe = document.createElement('iframe');
iframe.src = './download-test1.lib'
document.body.appendChild(iframe);
}
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
}
// Add header on redirect.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, AddRequestHeaderOnRedirect) {
net::test_server::ControllableHttpResponse response_1(embedded_test_server(),
"", true);
net::test_server::ControllableHttpResponse response_2(embedded_test_server(),
"", true);
ASSERT_TRUE(embedded_test_server()->Start());
content::TestNavigationThrottleInserter throttle_inserter(
web_contents(),
base::BindLambdaForTesting(
[](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> {
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
NavigationRequest* request = NavigationRequest::From(handle);
throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST,
base::BindLambdaForTesting([request]() {
request->SetRequestHeader("header_name",
"header_value");
}));
return throttle;
}));
// 1) There is no "header_name" header in the initial request.
shell()->LoadURL(embedded_test_server()->GetURL("/doc"));
response_1.WaitForRequest();
EXPECT_FALSE(
base::Contains(response_1.http_request()->headers, "header_name"));
response_1.Send(
"HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n");
response_1.Done();
// 2) The header is added to the second request after the redirect.
response_2.WaitForRequest();
EXPECT_EQ("header_value",
response_2.http_request()->headers.at("header_name"));
// Redirect should not record a ReceivedResponse event.
EXPECT_EQ(0u, test_ukm_recorder()
.GetEntriesByName("Navigation.ReceivedResponse")
.size());
}
// Add header on request start, modify it on redirect.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
AddRequestHeaderModifyOnRedirect) {
net::test_server::ControllableHttpResponse response_1(embedded_test_server(),
"", true);
net::test_server::ControllableHttpResponse response_2(embedded_test_server(),
"", true);
ASSERT_TRUE(embedded_test_server()->Start());
content::TestNavigationThrottleInserter throttle_inserter(
web_contents(),
base::BindLambdaForTesting(
[](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> {
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
NavigationRequest* request = NavigationRequest::From(handle);
throttle->SetCallback(TestNavigationThrottle::WILL_START_REQUEST,
base::BindLambdaForTesting([request]() {
request->SetRequestHeader("header_name",
"header_value");
}));
throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST,
base::BindLambdaForTesting([request]() {
request->SetRequestHeader("header_name",
"other_value");
}));
return throttle;
}));
// 1) The header is added to the initial request.
shell()->LoadURL(embedded_test_server()->GetURL("/doc"));
response_1.WaitForRequest();
EXPECT_EQ("header_value",
response_1.http_request()->headers.at("header_name"));
response_1.Send(
"HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n");
response_1.Done();
// 2) The header is modified in the second request after the redirect.
response_2.WaitForRequest();
EXPECT_EQ("other_value",
response_2.http_request()->headers.at("header_name"));
}
// Add header on request start, remove it on redirect.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
AddRequestHeaderRemoveOnRedirect) {
net::test_server::ControllableHttpResponse response_1(embedded_test_server(),
"", true);
net::test_server::ControllableHttpResponse response_2(embedded_test_server(),
"", true);
ASSERT_TRUE(embedded_test_server()->Start());
content::TestNavigationThrottleInserter throttle_inserter(
web_contents(),
base::BindLambdaForTesting(
[](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> {
NavigationRequest* request = NavigationRequest::From(handle);
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
throttle->SetCallback(TestNavigationThrottle::WILL_START_REQUEST,
base::BindLambdaForTesting([request]() {
request->SetRequestHeader("header_name",
"header_value");
}));
throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST,
base::BindLambdaForTesting([request]() {
request->RemoveRequestHeader("header_name");
}));
return throttle;
}));
// 1) The header is added to the initial request.
shell()->LoadURL(embedded_test_server()->GetURL("/doc"));
response_1.WaitForRequest();
EXPECT_EQ("header_value",
response_1.http_request()->headers.at("header_name"));
response_1.Send(
"HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n");
response_1.Done();
// 2) The header is removed from the second request after the redirect.
response_2.WaitForRequest();
EXPECT_FALSE(
base::Contains(response_2.http_request()->headers, "header_name"));
}
// Name of header used by CorsInjectingUrlLoader.
const std::string kCorsHeaderName = "test-header";
// URLLoaderThrottle that stores the last value of |kCorsHeaderName|.
class CorsInjectingUrlLoader : public blink::URLLoaderThrottle {
public:
explicit CorsInjectingUrlLoader(std::string* last_cors_header_value)
: last_cors_header_value_(last_cors_header_value) {}
// blink::URLLoaderThrottle:
void WillStartRequest(network::ResourceRequest* request,
bool* defer) override {
if (std::optional<std::string> header =
request->cors_exempt_headers.GetHeader(kCorsHeaderName);
header) {
last_cors_header_value_->swap(*header);
} else {
last_cors_header_value_->clear();
}
}
private:
// See |NavigationCorsExemptBrowserTest::last_cors_header_value_| for details.
raw_ptr<std::string> last_cors_header_value_;
};
// ContentBrowserClient responsible for creating CorsInjectingUrlLoader.
class CorsContentBrowserClient : public ContentBrowserTestContentBrowserClient {
public:
explicit CorsContentBrowserClient(std::string* last_cors_header_value)
: last_cors_header_value_(last_cors_header_value) {}
// ContentBrowserClient overrides:
std::vector<std::unique_ptr<blink::URLLoaderThrottle>>
CreateURLLoaderThrottles(
const network::ResourceRequest& request,
BrowserContext* browser_context,
const base::RepeatingCallback<WebContents*()>& wc_getter,
NavigationUIData* navigation_ui_data,
FrameTreeNodeId frame_tree_node_id,
std::optional<int64_t> navigation_id) override {
std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles;
throttles.push_back(
std::make_unique<CorsInjectingUrlLoader>(last_cors_header_value_));
return throttles;
}
private:
// See |NavigationCorsExemptBrowserTest::last_cors_header_value_| for details.
raw_ptr<std::string> last_cors_header_value_;
};
class NavigationCorsExemptBrowserTest : public NavigationBaseBrowserTest {
public:
NavigationCorsExemptBrowserTest() = default;
protected:
const std::string& last_cors_header_value() const {
return last_cors_header_value_;
}
void SetUpCommandLine(base::CommandLine* command_line) override {
ShellContentBrowserClient::set_allow_any_cors_exempt_header_for_browser(
true);
NavigationBaseBrowserTest::SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
cors_content_browser_client_ =
std::make_unique<CorsContentBrowserClient>(&last_cors_header_value_);
host_resolver()->AddRule("*", "127.0.0.1");
}
void TearDownOnMainThread() override {
cors_content_browser_client_.reset();
ShellContentBrowserClient::set_allow_any_cors_exempt_header_for_browser(
false);
}
private:
// Last value of kCorsHeaderName. Set by CorsInjectingUrlLoader.
std::string last_cors_header_value_;
std::unique_ptr<CorsContentBrowserClient> cors_content_browser_client_;
};
// Verifies a header added by way of SetRequestHeader() makes it into
// |cors_exempt_headers|.
IN_PROC_BROWSER_TEST_F(NavigationCorsExemptBrowserTest,
SetCorsExemptRequestHeader) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"", true);
ASSERT_TRUE(embedded_test_server()->Start());
const std::string header_value = "value";
content::TestNavigationThrottleInserter throttle_inserter(
web_contents(),
base::BindLambdaForTesting([header_value](NavigationHandle* handle)
-> std::unique_ptr<NavigationThrottle> {
NavigationRequest* request = NavigationRequest::From(handle);
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
throttle->SetCallback(
TestNavigationThrottle::WILL_START_REQUEST,
base::BindLambdaForTesting([request, header_value]() {
request->SetCorsExemptRequestHeader(kCorsHeaderName,
header_value);
}));
return throttle;
}));
shell()->LoadURL(embedded_test_server()->GetURL("/doc"));
response.WaitForRequest();
EXPECT_EQ(header_value, response.http_request()->headers.at(kCorsHeaderName));
EXPECT_EQ(header_value, last_cors_header_value());
}
// Test NavigationRequest::CheckAboutSrcDoc()
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BlockedSrcDocBrowserInitiated) {
const char* about_srcdoc_urls[] = {"about:srcdoc", "about:srcdoc?foo",
"about:srcdoc#foo"};
// 1. Main frame navigations to about:srcdoc and its variations are blocked.
for (const char* url : about_srcdoc_urls) {
NavigationHandleObserver handle_observer(web_contents(), GURL(url));
EXPECT_FALSE(NavigateToURL(shell(), GURL(url)));
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_TRUE(handle_observer.is_error());
EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code());
}
// 2. Subframe navigations to variations of about:srcdoc are not blocked.
for (const char* url : about_srcdoc_urls) {
GURL main_url =
embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
NavigationHandleObserver handle_observer(web_contents(), GURL(url));
shell()->LoadURLForFrame(GURL(url), "child-name-0",
ui::PAGE_TRANSITION_FORWARD_BACK);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_FALSE(handle_observer.is_error());
EXPECT_EQ(net::OK, handle_observer.net_error_code());
}
}
// Test NavigationRequest::CheckAboutSrcDoc().
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BlockedSrcDocRendererInitiated) {
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
const char* about_srcdoc_urls[] = {"about:srcdoc", "about:srcdoc?foo",
"about:srcdoc#foo"};
// 1. Main frame navigations to about:srcdoc and its variations are blocked.
for (const char* url : about_srcdoc_urls) {
DidStartNavigationObserver start_observer(web_contents());
NavigationHandleObserver handle_observer(web_contents(), GURL(url));
EXPECT_TRUE(ExecJs(main_frame(), JsReplace("location.href = $1", url)));
start_observer.Wait();
WaitForLoadStop(web_contents());
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_TRUE(handle_observer.is_error());
EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code());
}
// 2. Subframe navigations to variations of about:srcdoc are blocked unless
// they are same-document or the initiator is same origin to the srcdoc's
// parent. In this test suite, the subframe is self-navigating, but attempting
// a cross-origin navigation from a non-about:srcdoc page to about:srcdoc,
// which isn't allowed.
for (const char* url : about_srcdoc_urls) {
GURL main_url =
embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
DidStartNavigationObserver start_observer(web_contents());
NavigationHandleObserver handle_observer(web_contents(), GURL(url));
FrameTreeNode* subframe = main_frame()->child_at(0);
// Executing location.href = "about:srcdoc" fails.
// Note: if the subframe had already been navigated to about:srcdoc, then
// executing location.href = 'about:srcdoc#foo' would be considered a same-
// document navigation, and would be allowed. This behavior is tested in
// web platform tests, e.g.
// grandparent_location_aboutsrcdoc.sub.window.js, added in the same CL that
// added this comment.
EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", url)));
start_observer.Wait();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_TRUE(handle_observer.is_error());
EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code());
}
// 3. Navigate the sub-frame to be same-origin to the mainframe, then do the
// about:srcdoc navigations with with the main frame as the initiator. The
// test should succeed since the initiator and the parent are the same.
{
DidStartNavigationObserver start_observer(web_contents());
EXPECT_TRUE(ExecJs(
main_frame(), "document.querySelector('iframe').src = 'title1.html';"));
start_observer.Wait();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
}
for (const char* url : about_srcdoc_urls) {
DidStartNavigationObserver start_observer(web_contents());
NavigationHandleObserver handle_observer(web_contents(), GURL(url));
EXPECT_TRUE(
ExecJs(main_frame(), JsReplace("frames[0].location.href = $1", url)));
start_observer.Wait();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_FALSE(handle_observer.is_error());
EXPECT_EQ(net::OK, handle_observer.net_error_code());
}
// 4. The subframe is now on about:srcdoc ... verify it can reload itself.
{
FrameTreeNode* subframe = main_frame()->child_at(0);
GURL url("about:srcdoc");
{
// First, navigate the subframe to about:srcdoc without a fragment, so
// that the subsequent reload will have a commit (i.e. it won't be
// reloading a same-document navigation).
DidStartNavigationObserver start_observer(web_contents());
NavigationHandleObserver handle_observer(web_contents(), GURL(url));
EXPECT_TRUE(
ExecJs(main_frame(), JsReplace("frames[0].location.href = $1", url)));
start_observer.Wait();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_FALSE(handle_observer.is_error());
EXPECT_EQ(net::OK, handle_observer.net_error_code());
}
DidStartNavigationObserver start_observer(web_contents());
NavigationHandleObserver handle_observer(web_contents(), url);
EXPECT_TRUE(ExecJs(subframe, "location.reload()"));
start_observer.Wait();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(handle_observer.has_committed());
EXPECT_FALSE(handle_observer.is_error());
EXPECT_EQ(net::OK, handle_observer.net_error_code());
}
}
// Ensure that about:srcdoc navigations get their origin and base URL from their
// parent frame (since that's where the content comes from) and not from the
// initiator of the navigation (like about:blank cases). See also the
// NavigateGrandchildToAboutBlank test. See https://crbug.com/1515381.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
GrandchildToAboutSrcdoc_BaseUrl_CrossOrigin) {
GURL parent_url = embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_one_frame.html");
EXPECT_TRUE(NavigateToURL(shell(), parent_url));
FrameTreeNode* subframe = main_frame()->child_at(0);
// Navigate the subframe to a cross-site URL with a srcdoc subframe.
GURL child_url = embedded_test_server()->GetURL(
"b.com", "/frame_tree/page_with_srcdoc_frame.html");
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", child_url)));
observer.Wait();
EXPECT_EQ(child_url, subframe->current_frame_host()->GetLastCommittedURL());
FrameTreeNode* grandchild = subframe->child_at(0);
EXPECT_EQ("hello",
EvalJs(grandchild, "document.body.innerHTML").ExtractString());
// From the main frame, attempt to navigate the grandchild frame to
// about:srcdoc. This should fail.
TestNavigationObserver srcdoc_observer(web_contents());
EXPECT_TRUE(
ExecJs(main_frame(), "frames[0][0].location.href = 'about:srcdoc';"));
srcdoc_observer.Wait();
EXPECT_EQ(
"Could not load the requested resource.<br>Error code: -300 "
"(net::ERR_INVALID_URL)",
EvalJs(grandchild, "document.body.innerHTML").ExtractString());
// Since the navigation attempt failed, the origin and base URI are inherited
// from the error page.
EXPECT_EQ(GURL(url::kAboutSrcdocURL),
grandchild->current_frame_host()->GetLastCommittedURL());
EXPECT_TRUE(
grandchild->current_frame_host()->GetLastCommittedOrigin().opaque());
EXPECT_EQ("chrome-error://chromewebdata/",
EvalJs(grandchild, "document.baseURI").ExtractString());
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
GrandchildToAboutSrcdoc_BaseUrl_SameOrigin) {
GURL mainframe_url = embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_one_frame.html");
EXPECT_TRUE(NavigateToURL(shell(), mainframe_url));
FrameTreeNode* sub_frame = main_frame()->child_at(0);
// Navigate `sub_frame` to a same-origin URL with a srcdoc subframe.
GURL subframe_url = embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_srcdoc_frame.html");
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(sub_frame, JsReplace("location.href = $1", subframe_url)));
observer.Wait();
EXPECT_EQ(subframe_url,
sub_frame->current_frame_host()->GetLastCommittedURL());
FrameTreeNode* srcdoc_frame = sub_frame->child_at(0);
EXPECT_EQ("hello",
EvalJs(srcdoc_frame, "document.body.innerHTML").ExtractString());
EXPECT_EQ(subframe_url,
EvalJs(srcdoc_frame, "document.baseURI").ExtractString());
// From the mainframe, attempt to navigate `srcdoc_frame` to about:srcdoc.
// This should succeed since the mainframe and `sub_frame` are same origin.
TestNavigationObserver srcdoc_observer(web_contents());
EXPECT_TRUE(
ExecJs(main_frame(), "frames[0][0].location.href = 'about:srcdoc';"));
srcdoc_observer.Wait();
EXPECT_EQ("hello",
EvalJs(srcdoc_frame, "document.body.innerHTML").ExtractString());
// The origin and base URI should be inherited from the initiator, since it's
// same-origin to the srcdoc's parent frame `sub_frame`.
EXPECT_EQ(GURL(url::kAboutSrcdocURL),
srcdoc_frame->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(subframe_url),
srcdoc_frame->current_frame_host()->GetLastCommittedOrigin());
// This picks up the mainframe's url since it's same-origin to `sub_frame`,
// which is `srcdoc_frame`'s parent.
EXPECT_EQ(mainframe_url,
EvalJs(srcdoc_frame, "document.baseURI").ExtractString());
}
// Ensure that about:blank navigations get their origin and base URL from the
// initiator of the navigation, and not from their parent frame (like
// about:srcdoc cases). See also the NavigateGrandchildToAboutSrcdoc test.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, GrandchildToAboutBlank_BaseUrl) {
GURL url_a = embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_one_frame.html");
EXPECT_TRUE(NavigateToURL(shell(), url_a));
FrameTreeNode* subframe = main_frame()->child_at(0);
// Navigate the subframe to a cross-site URL with a srcdoc subframe.
GURL url_b = embedded_test_server()->GetURL(
"b.com", "/frame_tree/page_with_srcdoc_frame.html");
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", url_b)));
observer.Wait();
EXPECT_EQ(url_b, subframe->current_frame_host()->GetLastCommittedURL());
FrameTreeNode* grandchild = subframe->child_at(0);
EXPECT_EQ("hello",
EvalJs(grandchild, "document.body.innerHTML").ExtractString());
// From the main frame, navigate the grandchild frame to about:blank.
TestNavigationObserver srcdoc_observer(web_contents());
EXPECT_TRUE(
ExecJs(main_frame(), "frames[0][0].location.href = 'about:blank';"));
srcdoc_observer.Wait();
// There is no content when navigating to about:blank.
EXPECT_EQ("", EvalJs(grandchild, "document.body.innerHTML").ExtractString());
// The origin and base URI should be inherited from the initiator of the
// navigation and not the parent frame, unlike navigations to about:srcdoc.
EXPECT_EQ(GURL(url::kAboutBlankURL),
grandchild->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
grandchild->current_frame_host()->GetLastCommittedOrigin());
EXPECT_EQ(url_a, EvalJs(grandchild, "document.baseURI").ExtractString());
}
// Test renderer initiated navigations to about:srcdoc are routed through the
// browser process. It means RenderFrameHostImpl::BeginNavigation() is called.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutSrcDocUsesBeginNavigation) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// If DidStartNavigation is called before DidCommitProvisionalLoad, then it
// means the navigation was driven by the browser process, otherwise by the
// renderer process. This tests it was driven by the browser process:
InterceptAndCancelDidCommitProvisionalLoad interceptor(web_contents());
DidStartNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
let iframe = document.createElement("iframe");
iframe.srcdoc = "foo"
document.body.appendChild(iframe);
)"));
observer.Wait(); // BeginNavigation is called.
interceptor.Wait(1); // DidCommitNavigation is called.
}
// Regression test for https://crbug.com/996044
// 1) Navigate an iframe to srcdoc (about:srcdoc);
// 2) Same-document navigation to about:srcdoc#1.
// 3) Same-document navigation to about:srcdoc#2.
// 4) history.back() to about:srcdoc#1.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SrcDocWithFragmentHistoryNavigation) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// 1) Navigate an iframe to srcdoc (about:srcdoc)
EXPECT_TRUE(ExecJs(shell(), R"(
new Promise(async resolve => {
let iframe = document.createElement('iframe');
iframe.srcdoc = "test";
iframe.onload = resolve;
document.body.appendChild(iframe);
});
)"));
// 2) Same-document navigation to about:srcdoc#1.
// 3) Same-document navigation to about:srcdoc#2.
EXPECT_TRUE(ExecJs(shell(), R"(
let subwindow = document.querySelector('iframe').contentWindow;
subwindow.location.hash = "1";
subwindow.location.hash = "2";
)"));
// Inspect the session history.
NavigationControllerImpl& controller = web_contents()->GetController();
ASSERT_EQ(3, controller.GetEntryCount());
ASSERT_EQ(2, controller.GetCurrentEntryIndex());
std::array<FrameNavigationEntry*, 3> entry;
for (int i = 0; i < 3; ++i) {
entry[i] = controller.GetEntryAtIndex(i)
->root_node()
->children[0]
->frame_entry.get();
}
EXPECT_EQ(entry[0]->url(), "about:srcdoc");
EXPECT_EQ(entry[1]->url(), "about:srcdoc#1");
EXPECT_EQ(entry[2]->url(), "about:srcdoc#2");
// 4) history.back() to about:srcdoc#1.
EXPECT_TRUE(ExecJs(shell(), "history.back()"));
ASSERT_EQ(3, controller.GetEntryCount());
ASSERT_EQ(1, controller.GetCurrentEntryIndex());
}
// Regression test for https://crbug.com/996044.
// 1) Navigate an iframe to srcdoc (about:srcdoc).
// 2) Cross-document navigation to about:srcdoc?1.
// 3) Cross-document navigation to about:srcdoc?2.
// 4) history.back() to about:srcdoc?1.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SrcDocWithQueryHistoryNavigation) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// 1) Navigate an iframe to srcdoc (about:srcdoc).
EXPECT_TRUE(ExecJs(shell(), R"(
new Promise(async resolve => {
let iframe = document.createElement('iframe');
iframe.srcdoc = "test";
iframe.onload = resolve;
document.body.appendChild(iframe);
});
)"));
// 2) Cross-document navigation to about:srcdoc?1.
{
TestNavigationManager commit_waiter(web_contents(), GURL("about:srcdoc?1"));
EXPECT_TRUE(ExecJs(shell(), R"(
let subwindow = document.querySelector('iframe').contentWindow;
subwindow.location.search = "1";
)"));
ASSERT_TRUE(commit_waiter.WaitForNavigationFinished());
}
// 3) Cross-document navigation to about:srcdoc?2.
{
TestNavigationManager commit_waiter(web_contents(), GURL("about:srcdoc?2"));
EXPECT_TRUE(ExecJs(shell(), R"(
let subwindow = document.querySelector('iframe').contentWindow;
subwindow.location.search = "2";
)"));
ASSERT_TRUE(commit_waiter.WaitForNavigationFinished());
}
// Inspect the session history.
NavigationControllerImpl& controller = web_contents()->GetController();
ASSERT_EQ(3, controller.GetEntryCount());
ASSERT_EQ(2, controller.GetCurrentEntryIndex());
std::array<FrameNavigationEntry*, 3> entry;
for (int i = 0; i < 3; ++i) {
entry[i] = controller.GetEntryAtIndex(i)
->root_node()
->children[0]
->frame_entry.get();
}
EXPECT_EQ(entry[0]->url(), "about:srcdoc");
EXPECT_EQ(entry[1]->url(), "about:srcdoc?1");
EXPECT_EQ(entry[2]->url(), "about:srcdoc?2");
// 4) history.back() to about:srcdoc#1.
EXPECT_TRUE(ExecJs(shell(), "history.back()"));
ASSERT_EQ(3, controller.GetEntryCount());
ASSERT_EQ(1, controller.GetCurrentEntryIndex());
}
// Make sure embedders are notified about visible URL changes in this scenario:
// 1. Navigate to A.
// 2. Navigate to B.
// 3. Add a forward entry in the history for later (same-document).
// 4. Start navigation to C.
// 5. Start history cross-document navigation, cancelling 4.
// 6. Start history same-document navigation, cancelling 5.
//
// Regression test for https://crbug.com/998284.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
BackForwardInOldDocumentCancelPendingNavigation) {
// This test expects a new request to be made when navigating back, which is
// not happening with back-forward cache enabled.
// See BackForwardCacheBrowserTest.RestoreWhilePendingCommit which covers the
// same scenario for back-forward cache.
web_contents()->GetController().GetBackForwardCache().DisableForTesting(
BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
using Response = net::test_server::ControllableHttpResponse;
Response response_A1(embedded_test_server(), "/A");
Response response_A2(embedded_test_server(), "/A");
Response response_B1(embedded_test_server(), "/B");
Response response_C1(embedded_test_server(), "/C");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a = embedded_test_server()->GetURL("a.com", "/A");
GURL url_b = embedded_test_server()->GetURL("b.com", "/B");
GURL url_c = embedded_test_server()->GetURL("c.com", "/C");
EmbedderVisibleUrlTracker embedder_url_tracker;
web_contents()->SetDelegate(&embedder_url_tracker);
// 1. Navigate to A.
shell()->LoadURL(url_a);
response_A1.WaitForRequest();
response_A1.Send(non_cacheable_html_response);
response_A1.Done();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// 2. Navigate to B.
shell()->LoadURL(url_b);
response_B1.WaitForRequest();
response_B1.Send(non_cacheable_html_response);
response_B1.Done();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// 3. Add a forward entry in the history for later (same-document).
EXPECT_TRUE(ExecJs(web_contents(), R"(
history.pushState({},'');
history.back();
)"));
// 4. Start navigation to C.
{
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
EXPECT_EQ(url_b, embedder_url_tracker.url());
}
shell()->LoadURL(url_c);
// TODO(arthursonzogni): The embedder_url_tracker should update to url_c at
// this point, but we currently rely on FrameTreeNode::DidStopLoading for
// invalidation and it does not occur when a prior navigation is already in
// progress. The browser is still waiting on the same-document
// "history.back()" to complete.
{
EXPECT_EQ(url_c, web_contents()->GetVisibleURL());
EXPECT_EQ(url_b, embedder_url_tracker.url());
}
embedder_url_tracker.WaitUntilUrlInvalidated();
{
EXPECT_EQ(url_c, web_contents()->GetVisibleURL());
EXPECT_EQ(url_c, embedder_url_tracker.url());
}
response_C1.WaitForRequest();
// 5. Start history cross-document navigation, cancelling 4.
EXPECT_TRUE(ExecJs(web_contents(), "history.back()"));
{
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
EXPECT_EQ(url_b, embedder_url_tracker.url());
}
response_A2.WaitForRequest();
{
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
EXPECT_EQ(url_b, embedder_url_tracker.url());
}
// 6. Start history same-document navigation, cancelling 5.
EXPECT_TRUE(ExecJs(web_contents(), "history.forward()"));
{
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
EXPECT_EQ(url_b, embedder_url_tracker.url());
}
EXPECT_TRUE(WaitForLoadStop(web_contents()));
{
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
EXPECT_EQ(url_b, embedder_url_tracker.url());
}
}
// Regression test for https://crbug.com/999932.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, CanceledNavigationBug999932) {
using Response = net::test_server::ControllableHttpResponse;
Response response_A1(embedded_test_server(), "/A");
Response response_A2(embedded_test_server(), "/A");
Response response_B1(embedded_test_server(), "/B");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a = embedded_test_server()->GetURL("a.com", "/A");
GURL url_b = embedded_test_server()->GetURL("b.com", "/B");
// 1. Navigate to A.
shell()->LoadURL(url_a);
response_A1.WaitForRequest();
response_A1.Send(non_cacheable_html_response);
response_A1.Done();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// 2. Start pending navigation to B.
shell()->LoadURL(url_b);
EXPECT_EQ(url_b, web_contents()->GetVisibleURL());
EXPECT_TRUE(web_contents()->GetController().GetPendingEntry());
// 3. Cancel (2) with renderer-initiated reload with a UserGesture.
EXPECT_TRUE(ExecJs(web_contents(), "location.reload()"));
EXPECT_EQ(url_a, web_contents()->GetVisibleURL());
EXPECT_FALSE(web_contents()->GetController().GetPendingEntry());
// 4. Cancel (3) using document.open();
EXPECT_TRUE(ExecJs(web_contents(), "document.open()"));
EXPECT_EQ(url_a, web_contents()->GetVisibleURL());
EXPECT_FALSE(web_contents()->GetController().GetPendingEntry());
}
// Regression test for https://crbug.com/1001283
// 1) Load main document with CSP: script-src 'none'
// 2) Open an about:srcdoc iframe. It inherits the CSP.
// 3) The iframe navigates elsewhere.
// 4) The iframe navigates back to about:srcdoc.
// Check Javascript is never allowed.
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
SrcDocCSPInheritedAfterSameSiteHistoryNavigation) {
using Response = net::test_server::ControllableHttpResponse;
Response main_document_response(embedded_test_server(), "/main_document");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a = embedded_test_server()->GetURL("a.com", "/main_document");
GURL url_b = embedded_test_server()->GetURL("a.com", "/title1.html");
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern("Refused to execute inline script *");
// 1) Load main document with CSP: script-src 'none'
// 2) Open an about:srcdoc iframe. It inherits the CSP from its parent.
shell()->LoadURL(url_a);
main_document_response.WaitForRequest();
main_document_response.Send(
"HTTP/1.1 200 OK\n"
"content-type: text/html; charset=UTF-8\n"
"Content-Security-Policy: script-src 'none'\n"
"\n"
"<iframe name='theiframe' srcdoc='"
" <script>"
" console.error(\"CSP failure\");"
" </script>"
"'>"
"</iframe>");
main_document_response.Done();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Check Javascript was blocked the first time.
ASSERT_TRUE(console_observer.Wait());
}
// 3) The iframe navigates elsewhere.
shell()->LoadURLForFrame(url_b, "theiframe",
ui::PAGE_TRANSITION_MANUAL_SUBFRAME);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern("Refused to execute inline script *");
// 4) The iframe navigates back to about:srcdoc.
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Check Javascript was blocked the second time.
ASSERT_TRUE(console_observer.Wait());
}
}
IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest,
SrcDocCSPInheritedAfterCrossSiteHistoryNavigation) {
using Response = net::test_server::ControllableHttpResponse;
Response main_document_response(embedded_test_server(), "/main_document");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a = embedded_test_server()->GetURL("a.com", "/main_document");
GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html");
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern("Refused to execute inline script *");
// 1) Load main document with CSP: script-src 'none'
// 2) Open an about:srcdoc iframe. It inherits the CSP from its parent.
shell()->LoadURL(url_a);
main_document_response.WaitForRequest();
main_document_response.Send(
"HTTP/1.1 200 OK\n"
"content-type: text/html; charset=UTF-8\n"
"Content-Security-Policy: script-src 'none'\n"
"\n"
"<iframe name='theiframe' srcdoc='"
" <script>"
" console.error(\"CSP failure\");"
" </script>"
"'>"
"</iframe>");
main_document_response.Done();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Check Javascript was blocked the first time.
ASSERT_TRUE(console_observer.Wait());
}
// 3) The iframe navigates elsewhere.
shell()->LoadURLForFrame(url_b, "theiframe",
ui::PAGE_TRANSITION_MANUAL_SUBFRAME);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern("Refused to execute inline script *");
// 4) The iframe navigates back to about:srcdoc.
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Check Javascript was blocked the second time.
ASSERT_TRUE(console_observer.Wait());
}
}
// Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual
// value of RenderFrameHost::GetPageUkmSourceId() --- unremarkable top-level
// navigation case.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
NavigationRequest_GetNextPageUkmSourceId_Basic) {
const GURL kUrl(embedded_test_server()->GetURL("/title1.html"));
TestNavigationManager manager(web_contents(), kUrl);
shell()->LoadURL(kUrl);
EXPECT_TRUE(manager.WaitForRequestStart());
ASSERT_TRUE(main_frame()->navigation_request());
ukm::SourceId nav_request_id =
main_frame()->navigation_request()->GetNextPageUkmSourceId();
EXPECT_TRUE(manager.WaitForResponse());
ASSERT_TRUE(manager.WaitForNavigationFinished());
EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), nav_request_id);
}
// Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual
// value of RenderFrameHost::GetPageUkmSourceId() --- child frame case.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
NavigationRequest_GetNextPageUkmSourceId_ChildFrame) {
const GURL kUrl(
embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"));
const GURL kDestUrl(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), kUrl));
FrameTreeNode* subframe = main_frame()->child_at(0);
ASSERT_TRUE(subframe);
TestNavigationManager manager(web_contents(), kDestUrl);
EXPECT_TRUE(
ExecJs(subframe, JsReplace("location.href = $1", kDestUrl.spec())));
EXPECT_TRUE(manager.WaitForRequestStart());
ASSERT_TRUE(subframe->navigation_request());
ukm::SourceId nav_request_id =
subframe->navigation_request()->GetNextPageUkmSourceId();
EXPECT_TRUE(manager.WaitForResponse());
ASSERT_TRUE(manager.WaitForNavigationFinished());
// Should have the same page UKM ID in navigation as page post commit, and as
// the top-level frame.
EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), nav_request_id);
EXPECT_EQ(subframe->current_frame_host()->GetPageUkmSourceId(),
nav_request_id);
}
// Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual
// value of RenderFrameHost::GetPageUkmSourceId() --- same document navigation.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
NavigationRequest_GetNextPageUkmSourceId_SameDocument) {
const GURL kUrl(embedded_test_server()->GetURL("/title1.html"));
const GURL kFragment(kUrl.Resolve("#here"));
EXPECT_TRUE(NavigateToURL(shell(), kUrl));
NavigationHandleObserver handle_observer(web_contents(), kFragment);
EXPECT_TRUE(
ExecJs(main_frame(), JsReplace("location.href = $1", kFragment.spec())));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(handle_observer.is_same_document());
EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(),
handle_observer.next_page_ukm_source_id());
}
// Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual
// value of RenderFrameHost::GetPageUkmSourceId() --- back navigation;
// this case matters because of back-forward cache.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
NavigationRequest_GetNextPageUkmSourceId_Back) {
const GURL kUrl1(embedded_test_server()->GetURL("a.com", "/title1.html"));
const GURL kUrl2(embedded_test_server()->GetURL("b.com", "/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), kUrl1));
EXPECT_TRUE(NavigateToURL(shell(), kUrl2));
NavigationHandleObserver handle_observer(web_contents(), kUrl1);
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(),
handle_observer.next_page_ukm_source_id());
}
// Tests for cookies. Provides an HTTPS server.
class NavigationCookiesBrowserTest : public NavigationBaseBrowserTest {
protected:
NavigationCookiesBrowserTest() = default;
net::EmbeddedTestServer* https_server() { return &https_server_; }
private:
void SetUpOnMainThread() override {
NavigationBaseBrowserTest::SetUpOnMainThread();
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
https_server()->AddDefaultHandlers(GetTestDataFilePath());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
NavigationBaseBrowserTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpInProcessBrowserTestFixture() override {
NavigationBaseBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
NavigationBaseBrowserTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
content::ContentMockCertVerifier mock_cert_verifier_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
// Test how cookies are inherited in about:srcdoc iframes.
//
// Regression test: https://crbug.com/1003167.
// Test is flaky on all platforms: https://crbug.com/339033006
IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest,
DISABLED_CookiesInheritedSrcDoc) {
using Response = net::test_server::ControllableHttpResponse;
Response response_1(https_server(), "/response_1");
Response response_2(https_server(), "/response_2");
Response response_3(https_server(), "/response_3");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(ExecJs(shell(), R"(
let iframe = document.createElement("iframe");
iframe.srcdoc = "foo";
document.body.appendChild(iframe);
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* main_document = current_frame_host();
RenderFrameHostImpl* sub_document_1 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url::kAboutSrcdocURL, sub_document_1->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
sub_document_1->GetLastCommittedOrigin());
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_1->GetSiteInstance());
// 0. The default state doesn't contain any cookies.
EXPECT_EQ("", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie"));
// 1. Set a cookie in the main document, it affects its child too.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';"));
EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie"));
// 2. Set a cookie in the child, it affects its parent too.
EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';"));
EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie"));
// 3. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_1, "fetch('/response_1');");
response_1.WaitForRequest();
EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie"));
// 4. Navigate the iframe elsewhere.
EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_2 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie"));
// 5. Set a cookie in the main document. It doesn't affect its child.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie"));
// 6. Set a cookie in the child. It doesn't affect its parent.
EXPECT_TRUE(ExecJs(sub_document_2,
"document.cookie = 'd=0; SameSite=none; Secure';"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie"));
// 7. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_2, "fetch('/response_2');");
response_2.WaitForRequest();
EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie"));
// 8. Navigate the iframe back to about:srcdoc.
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_3 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url_a, main_document->GetLastCommittedURL());
EXPECT_EQ(url::kAboutSrcdocURL, sub_document_3->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
sub_document_3->GetLastCommittedOrigin());
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_3->GetSiteInstance());
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie"));
// 9. Set cookie in the main document. It should be inherited by the child.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';"));
EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie"));
// 11. Set cookie in the child document. It should be reflected on its parent.
EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';"));
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
EvalJs(sub_document_3, "document.cookie"));
// 12. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_3, "fetch('/response_3');");
response_3.WaitForRequest();
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
response_3.http_request()->headers.at("Cookie"));
}
// Test how cookies are inherited in about:blank iframes.
IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest,
CookiesInheritedAboutBlank) {
// This test expects several cross-site navigation to happen.
if (!AreAllSitesIsolatedForTesting())
return;
using Response = net::test_server::ControllableHttpResponse;
Response response_1(https_server(), "/response_1");
Response response_2(https_server(), "/response_2");
Response response_3(https_server(), "/response_3");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(
ExecJs(shell(), JsReplace("let iframe = document.createElement('iframe');"
"iframe.src = $1;"
"document.body.appendChild(iframe);",
url_b)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(ExecJs(shell(), R"(
document.querySelector('iframe').src = "about:blank"
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* main_document = current_frame_host();
RenderFrameHostImpl* sub_document_1 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url::kAboutBlankURL, sub_document_1->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
sub_document_1->GetLastCommittedOrigin());
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_1->GetSiteInstance());
// 0. The default state doesn't contain any cookies.
EXPECT_EQ("", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie"));
// 1. Set a cookie in the main document, it affects its child too.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';"));
EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie"));
// 2. Set a cookie in the child, it affects its parent too.
EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';"));
EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie"));
// 3. Checks cookies are sent while requesting resources.
GURL url_response_1 = https_server()->GetURL("a.com", "/response_1");
ExecuteScriptAsync(sub_document_1, JsReplace("fetch($1)", url_response_1));
response_1.WaitForRequest();
EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie"));
// 4. Navigate the iframe elsewhere.
EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_2 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie"));
// 5. Set a cookie in the main document. It doesn't affect its child.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie"));
// 6. Set a cookie in the child. It doesn't affect its parent.
EXPECT_TRUE(ExecJs(sub_document_2,
"document.cookie = 'd=0; SameSite=none; Secure';"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie"));
// 7. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_2, "fetch('/response_2');");
response_2.WaitForRequest();
EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie"));
// 8. Navigate the iframe back to about:blank.
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_3 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url_a, main_document->GetLastCommittedURL());
EXPECT_EQ(url::kAboutBlankURL, sub_document_3->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
sub_document_3->GetLastCommittedOrigin());
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_3->GetSiteInstance());
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie"));
// 9. Set cookie in the main document. It affects the iframe.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';"));
EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie"));
// 10. Set cookie in the iframe. It affects the main frame.
EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';"));
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
EvalJs(sub_document_3, "document.cookie"));
// 11. Even if document.cookie is empty, cookies are sent.
ExecuteScriptAsync(sub_document_3, "fetch('/response_3');");
response_3.WaitForRequest();
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
response_3.http_request()->headers.at("Cookie"));
}
// Test how cookies are inherited in about:blank iframes.
//
// This is a variation of
// NavigationCookiesBrowserTest.CookiesInheritedAboutBlank. Instead of
// requesting an history navigation, a new navigation is requested from the main
// frame. The navigation is cross-site instead of being same-site.
IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest,
CookiesInheritedAboutBlank2) {
// This test expects several cross-site navigation to happen.
if (!AreAllSitesIsolatedForTesting())
return;
using Response = net::test_server::ControllableHttpResponse;
Response response_1(https_server(), "/response_1");
Response response_2(https_server(), "/response_2");
Response response_3(https_server(), "/response_3");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(
ExecJs(shell(), JsReplace("let iframe = document.createElement('iframe');"
"iframe.src = $1;"
"document.body.appendChild(iframe);",
url_b)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(ExecJs(shell(), R"(
document.querySelector('iframe').src = "about:blank"
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* main_document = current_frame_host();
RenderFrameHostImpl* sub_document_1 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url::kAboutBlankURL, sub_document_1->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
sub_document_1->GetLastCommittedOrigin());
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_1->GetSiteInstance());
// 0. The default state doesn't contain any cookies.
EXPECT_EQ("", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie"));
// 1. Set a cookie in the main document, it affects its child too.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';"));
EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie"));
// 2. Set a cookie in the child, it affects its parent too.
EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';"));
EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie"));
// 3. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_1, "fetch('/response_1');");
response_1.WaitForRequest();
EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie"));
// 4. Navigate the iframe elsewhere.
EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_2 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie"));
// 5. Set a cookie in the main document. It doesn't affect its child.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie"));
// 6. Set a cookie in the child. It doesn't affect its parent.
EXPECT_TRUE(ExecJs(sub_document_2,
"document.cookie = 'd=0; SameSite=none; Secure';"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie"));
// 7. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_2, "fetch('/response_2');");
response_2.WaitForRequest();
EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie"));
// 8. Ask the top-level, a.com frame to navigate the subframe to about:blank.
EXPECT_TRUE(ExecJs(shell(), R"(
document.querySelector('iframe').src = "about:blank";
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_3 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url::kAboutBlankURL, sub_document_3->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(url_a),
sub_document_3->GetLastCommittedOrigin());
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_3->GetSiteInstance());
EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie"));
// 9. Set cookie in the main document.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';"));
EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie"));
// 10. Set cookie in the child document.
EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';"));
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
EvalJs(main_document, "document.cookie"));
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
EvalJs(sub_document_3, "document.cookie"));
// 11. Checks cookies are sent while requesting resources.
ExecuteScriptAsync(sub_document_3, "fetch('/response_3');");
response_3.WaitForRequest();
EXPECT_EQ("a=0; b=0; c=0; e=0; f=0",
response_3.http_request()->headers.at("Cookie"));
}
// Test how cookies are inherited in data-URL iframes.
IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, CookiesInheritedDataUrl) {
using Response = net::test_server::ControllableHttpResponse;
Response response_1(https_server(), "/response_1");
Response response_2(https_server(), "/response_2");
Response response_3(https_server(), "/response_3");
ASSERT_TRUE(https_server()->Start());
GURL url_a(https_server()->GetURL("a.com", "/title1.html"));
GURL url_b(https_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(ExecJs(shell(), R"(
let iframe = document.createElement("iframe");
iframe.src = "data:text/html,";
document.body.appendChild(iframe);
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* main_document = current_frame_host();
RenderFrameHostImpl* sub_document_1 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ("data:text/html,", sub_document_1->GetLastCommittedURL());
EXPECT_TRUE(sub_document_1->GetLastCommittedOrigin().opaque());
if (ShouldCreateSiteInstanceForDataUrls()) {
EXPECT_NE(main_document->GetSiteInstance(),
sub_document_1->GetSiteInstance());
EXPECT_EQ(main_document->GetSiteInstance()->group(),
sub_document_1->GetSiteInstance()->group());
} else {
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_1->GetSiteInstance());
}
// 1. Writing a cookie inside a data-URL document is forbidden.
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"*Failed to set the 'cookie' property on 'Document': Cookies are "
"disabled inside 'data:' URLs.*");
ExecuteScriptAsync(sub_document_1, "document.cookie = 'a=0';");
ASSERT_TRUE(console_observer.Wait());
}
// 2. Reading a cookie inside a data-URL document is forbidden.
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"*Failed to read the 'cookie' property from 'Document': Cookies are "
"disabled inside 'data:' URLs.*");
ExecuteScriptAsync(sub_document_1, "document.cookie");
ASSERT_TRUE(console_observer.Wait());
}
// 3. Set cookie in the main document. No cookies are sent when requested from
// the data-URL.
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0;SameSite=Lax'"));
EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'b=0;SameSite=Strict'"));
GURL url_response_1 = https_server()->GetURL("a.com", "/response_1");
ExecuteScriptAsync(sub_document_1, JsReplace("fetch($1)", url_response_1));
response_1.WaitForRequest();
EXPECT_EQ(0u, response_1.http_request()->headers.count("Cookie"));
// 4. Navigate the iframe elsewhere and back using history navigation.
EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* sub_document_2 =
main_document->child_at(0)->current_frame_host();
EXPECT_EQ(url_a, main_document->GetLastCommittedURL());
EXPECT_EQ("data:text/html,", sub_document_2->GetLastCommittedURL());
EXPECT_TRUE(sub_document_2->GetLastCommittedOrigin().opaque());
if (ShouldCreateSiteInstanceForDataUrls()) {
EXPECT_NE(main_document->GetSiteInstance(),
sub_document_2->GetSiteInstance());
EXPECT_EQ(main_document->GetSiteInstance()->GetSiteInstanceGroupId(),
sub_document_2->GetSiteInstance()->GetSiteInstanceGroupId());
} else {
EXPECT_EQ(main_document->GetSiteInstance(),
sub_document_2->GetSiteInstance());
}
// 5. Writing a cookie inside a data-URL document is still forbidden.
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"*Failed to set the 'cookie' property on 'Document': Cookies are "
"disabled inside 'data:' URLs.*");
ExecuteScriptAsync(sub_document_2, "document.cookie = 'c=0';");
ASSERT_TRUE(console_observer.Wait());
}
// 6. Reading a cookie inside a data-URL document is still forbidden.
{
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"*Failed to read the 'cookie' property from 'Document': Cookies are "
"disabled inside 'data:' URLs.*");
ExecuteScriptAsync(sub_document_2, "document.cookie");
ASSERT_TRUE(console_observer.Wait());
}
// 7. No cookies are sent when requested from the data-URL.
GURL url_response_2 = https_server()->GetURL("a.com", "/response_2");
ExecuteScriptAsync(sub_document_2, JsReplace("fetch($1)", url_response_2));
response_2.WaitForRequest();
EXPECT_EQ(0u, response_2.http_request()->headers.count("Cookie"));
}
// Tests for validating URL rewriting behavior like chrome://newtab to
// chrome-native://newtab.
class NavigationUrlRewriteBrowserTest : public NavigationBaseBrowserTest {
protected:
static constexpr const char* kRewriteURL = "http://a.com/rewrite";
static constexpr const char* kNoAccessScheme = "no-access";
static constexpr const char* kNoAccessURL = "no-access://testing/";
class BrowserClient : public ContentBrowserTestContentBrowserClient {
public:
void BrowserURLHandlerCreated(BrowserURLHandler* handler) override {
handler->AddHandlerPair(RewriteUrl,
BrowserURLHandlerImpl::null_handler());
fake_url_loader_factory_ = std::make_unique<FakeNetworkURLLoaderFactory>(
"HTTP/1.1 200 OK\nContent-Type: text/html\n\n", "This is a test",
/* network_accessed */ true, net::OK);
}
mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateNonNetworkNavigationURLLoaderFactory(
const std::string& scheme,
FrameTreeNodeId frame_tree_node_id) override {
if (scheme == kNoAccessScheme) {
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;
fake_url_loader_factory_->Clone(
pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
return {};
}
static bool RewriteUrl(GURL* url, BrowserContext* browser_context) {
if (*url == GURL(kRewriteURL)) {
*url = GURL(kNoAccessURL);
return true;
}
return false;
}
private:
std::unique_ptr<FakeNetworkURLLoaderFactory> fake_url_loader_factory_;
};
NavigationUrlRewriteBrowserTest() {
url::AddStandardScheme(kNoAccessScheme, url::SCHEME_WITH_HOST);
url::AddNoAccessScheme(kNoAccessScheme);
// This test needs to use an unassigned SiteInstance for kNoAccessScheme,
// which requires adding it as an empty document scheme.
url::AddEmptyDocumentScheme(kNoAccessScheme);
}
void SetUpOnMainThread() override {
NavigationBaseBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
browser_client_ = std::make_unique<BrowserClient>();
}
void TearDownOnMainThread() override {
browser_client_.reset();
NavigationBaseBrowserTest::TearDownOnMainThread();
}
GURL GetRewriteToNoAccessURL() const { return GURL(kRewriteURL); }
private:
std::unique_ptr<BrowserClient> browser_client_;
url::ScopedSchemeRegistryForTests scoped_registry_;
};
// Tests navigating to a URL that gets rewritten to a "no access" URL. This
// mimics the behavior of navigating to special URLs like chrome://newtab and
// chrome://history which get rewritten to "no access" chrome-native:// URLs.
IN_PROC_BROWSER_TEST_F(NavigationUrlRewriteBrowserTest, RewriteToNoAccess) {
// Perform an initial navigation.
{
TestNavigationObserver observer(web_contents());
GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(url, observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_FALSE(observer.last_initiator_origin().has_value());
}
// Navigate to the URL that will get rewritten to a "no access" URL.
{
TestNavigationObserver observer(web_contents());
// Note: We are using LoadURLParams here because we need to have the
// initiator_origin set and NavigateToURL() does not do that.
NavigationController::LoadURLParams params(GetRewriteToNoAccessURL());
params.initiator_origin = current_frame_host()->GetLastCommittedOrigin();
web_contents()->GetController().LoadURLWithParams(params);
web_contents()->Focus();
observer.Wait();
EXPECT_EQ(GURL(kNoAccessURL), observer.last_navigation_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_TRUE(observer.last_initiator_origin().has_value());
}
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigation) {
WebContents* wc = shell()->web_contents();
GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#frag1");
GURL url2 = embedded_test_server()->GetURL("a.com", "/title1.html#frag2");
NavigationHandleCommitObserver navigation_0(wc, url1);
NavigationHandleCommitObserver navigation_1(wc, url2);
EXPECT_TRUE(NavigateToURL(shell(), url1));
NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
EXPECT_TRUE(NavigateToURL(shell(), url2));
// The NavigationEntry changes on a same-document navigation.
EXPECT_NE(web_contents()->GetController().GetLastCommittedEntry(), entry);
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
EXPECT_TRUE(navigation_1.was_same_document());
}
// Some navigations are not allowed, such as when they fail the content security
// policy, or for trying to load about:srcdoc in the main frame. These result in
// us redirecting the navigation to an error page via
// RenderFrameHostImpl::FailedNavigation().
// Repeating the request with a different URL fragment results in attempting a
// same-document navigation, but error pages do not support such navigations. In
// this case treat each failed navigation request as a separate load, with the
// resulting navigation being performed as a cross-document navigation. This is
// regression test for https://crbug.com/1018385.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentNavigationOnBlockedPage) {
GURL url1("about:srcdoc#0");
GURL url2("about:srcdoc#1");
NavigationHandleCommitObserver navigation_0(web_contents(), url1);
NavigationHandleCommitObserver navigation_1(web_contents(), url2);
// Big warning: about:srcdoc is not supposed to be valid browser-initiated
// main-frame navigation, it is currently blocked by the NavigationRequest.
// It is used here to reproduce bug https://crbug.com/1018385. Please avoid
// copying this kind of navigation in your own tests.
EXPECT_FALSE(NavigateToURL(shell(), url1));
EXPECT_FALSE(NavigateToURL(shell(), url2));
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
EXPECT_FALSE(navigation_1.was_same_document());
}
// This navigation is allowed by the browser, but the network will not be able
// to connect to the site, so the NavigationRequest fails on the browser side
// and is redirected to an error page. Performing another navigation should
// make the full attempt again, in case the network request succeeds this time.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentNavigationOnBadServerErrorPage) {
GURL url1("http://badserver.com:9/");
GURL url2("http://badserver.com:9/#1");
NavigationHandleCommitObserver navigation_0(web_contents(), url1);
NavigationHandleCommitObserver navigation_1(web_contents(), url2);
// The navigation is okay from the browser's perspective, so NavigateToURL()
// will return true. But the network request ultimately fails, so the request
// is redirected to an error page.
EXPECT_FALSE(NavigateToURL(shell(), url1));
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
// The 2nd request shares a URL but it should be another cross-document
// navigation, rather than trying to navigate inside the error page.
EXPECT_FALSE(NavigateToURL(shell(), url2));
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_FALSE(navigation_1.was_same_document());
}
// This navigation is allowed by the browser, and the request to the server is
// successful, but it returns 404 error headers, and (optionally) an error page.
// When another request is made for the same page but with a different fragment,
// the browser will attempt to perform a same-document navigation but that
// navigation is intended for the actual document not the error page that has
// been loaded instead. A same-document navigation in the renderer-loaded error
// page should be performed as a cross-document navigation in order to attempt
// to reload the page.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentNavigationOn404ErrorPage) {
// This case is a non-empty 404 page. It makes different choices about where
// to load the page on a same-document navigation.
{
GURL url1 = embedded_test_server()->GetURL("a.com", "/page404.html");
GURL url2 = embedded_test_server()->GetURL("a.com", "/page404.html#1");
NavigationHandleCommitObserver navigation_0(web_contents(), url1);
NavigationHandleCommitObserver navigation_1(web_contents(), url2);
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
// This is another navigation to the non-existent URL, but with a different
// fragment. We have successfully loaded content from a.com. The fact that
// it is 404 response does not mean it is an error page, since the term
// "error page" is used for cases where the browser encounters an error
// loading a document from the origin. HTTP responses with >400 status codes
// are just like regular documents from the origin and we render their
// response body just like we would a 200 response. This is why it can make
// sense for a same document navigation to be performed from a 404 page.
EXPECT_TRUE(NavigateToURL(shell(), url2));
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_TRUE(navigation_1.was_same_document());
}
// This case is an empty 404 page. It makes different choices about where
// to load the page on a same-document navigation. Since the server has only
// replied with an error, the browser will display its own error page and
// therefore it is not one coming from the server's origin.
{
GURL url1 = embedded_test_server()->GetURL("a.com", "/empty404.html");
GURL url2 = embedded_test_server()->GetURL("a.com", "/empty404.html#1");
NavigationHandleCommitObserver navigation_0(web_contents(), url1);
NavigationHandleCommitObserver navigation_1(web_contents(), url2);
EXPECT_FALSE(NavigateToURL(shell(), url1));
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
// This is another navigation to the non-existent URL, but with a different
// fragment. Since we did not load a document from the server (we got
// `false` from `NavigateToURL()`) there is no server-provided document to
// navigate within. The result should be a cross-document navigation in
// order to attempt to load the document at the given path from the server
// again.
EXPECT_FALSE(NavigateToURL(shell(), url2));
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_FALSE(navigation_1.was_same_document());
}
// This case is also an empty 404 page, but we do replaceState and pushState
// afterwards, creating successful same-document navigations.
{
// Navigate to empty 404, committing an error page.
GURL url1 = embedded_test_server()->GetURL("a.com", "/empty404.html");
NavigationHandleCommitObserver navigation(web_contents(), url1);
EXPECT_FALSE(NavigateToURL(shell(), url1));
EXPECT_TRUE(navigation.has_committed());
EXPECT_FALSE(navigation.was_same_document());
// replaceState on an error page, without changing the URL.
{
FrameNavigateParamsCapturer capturer(main_frame());
capturer.set_wait_for_load(false);
EXPECT_TRUE(ExecJs(shell(), "history.replaceState('foo', '')"));
capturer.Wait();
EXPECT_TRUE(capturer.is_same_document());
}
// pushState on an error page, without changing the URL.
{
FrameNavigateParamsCapturer capturer(main_frame());
capturer.set_wait_for_load(false);
EXPECT_TRUE(ExecJs(shell(), "history.pushState('foo', '')"));
capturer.Wait();
EXPECT_TRUE(capturer.is_same_document());
}
}
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentNavigationFromCrossDocumentRedirect) {
WebContents* wc = shell()->web_contents();
GURL url0 = embedded_test_server()->GetURL("/title1.html#frag1");
GURL url1 =
embedded_test_server()->GetURL("/server-redirect?title1.html#frag2");
GURL url2 = embedded_test_server()->GetURL("/title1.html#frag2");
NavigationHandleCommitObserver navigation_0(wc, url0);
NavigationHandleCommitObserver navigation_1(wc, url1);
NavigationHandleCommitObserver navigation_2(wc, url2);
EXPECT_TRUE(NavigateToURL(shell(), url0));
// Since the redirect does not land at the URL we passed in, we get a false
// return here.
EXPECT_FALSE(NavigateToURL(shell(), url1));
// The navigation to |url1| is redirected and so |url1| does not commit. Then
// the resulting navigation to |url2| lands at the same document URL as |url0|
// which would be a same-document navigation if there wasn't a redirect
// involved. But since it started as a cross-document navigation it results in
// loading a new document instead of doing a same-document navigation.
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_1.has_committed());
EXPECT_TRUE(navigation_2.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
EXPECT_FALSE(navigation_1.was_same_document());
EXPECT_FALSE(navigation_2.was_same_document());
EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url2);
// Redirect should not record a ReceivedResponse event.
EXPECT_EQ(1u, test_ukm_recorder()
.GetEntriesByName("Navigation.ReceivedResponse")
.size());
}
// 1. The browser navigates to a.html.
// 2. The renderer uses history.pushState() to change the URL of the current
// document from a.html to b.html.
// 3. The browser tries to perform a same-document navigation to a.html#foo,
// since it did not hear about the document's URL changing yet. When it gets
// to the renderer, we discover a race has happened.
// 4. Meanwhile, the browser hears about the URL change to b.html and applies
// it.
// Now - how do we resolve the race?
// 5. We will reorder the a.html#foo navigation to start over in the browser
// after the b.html navigation.
// Technically, this is still a same-document navigation! The URL changed but
// the document did not. Currently, however, the browser only considers the URL
// when performing a non-history navigation to decide if it's a same-document
// navigation, so..
// 6. The browser will perform a cross-document navigation to a.html#foo.
//
// TODO(crbug.com/40799231): Test is flaky on various platforms.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
DISABLED_SameDocumentNavigationRacesPushStateURLChange) {
WebContents* wc = shell()->web_contents();
GURL url0 = embedded_test_server()->GetURL("/title1.html");
GURL url1 = embedded_test_server()->GetURL("/title2.html");
GURL url2 = embedded_test_server()->GetURL("/title1.html#frag2");
NavigationHandleCommitObserver navigation_0(wc, url0);
NavigationHandleCommitObserver navigation_1(wc, url1);
NavigationHandleCommitObserver navigation_2(wc, url2);
// Start at `url0`.
EXPECT_TRUE(NavigateToURL(shell(), url0));
// Have the renderer `history.pushState()` to `url1`, which leaves it on the
// `url0` document, but with a different URL now.
ExecuteScriptAsync(shell(), JsReplace("history.pushState('', '', $1);"
"window.location.href == $1;",
url1));
// The browser didn't hear about the change yet.
EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url0);
{
// We will wait for 2 navigations: one will be the pushState() and the other
// will be the navigation to `url2` started below.
TestNavigationObserver nav_observer(wc, 2);
// Start a same-document navigation to url2 that is racing with the
// renderer's history.pushState().
shell()->LoadURL(url2);
nav_observer.Wait();
}
// The last navigation to resolve is the one to `url2` as it's reordered to
// come after the race with the already-completed history.pushState().
EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url2);
// Navigation 0 was a cross-document navigation, to initially load the
// document.
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
// Navigation 1 was a same-document navigation, from the renderer's
// history.pushState() call.
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_TRUE(navigation_1.was_same_document());
// Navigation 2 was restarted and came after. When it restarted, it saw the
// URL did not match and did a cross-document navigation. Technically the same
// document was still loaded from `url0`, but the browser makes its choice
// on the document's current URL.
EXPECT_TRUE(navigation_2.has_committed());
EXPECT_FALSE(navigation_2.was_same_document());
}
class GetEffectiveUrlClient : public ContentBrowserTestContentBrowserClient {
public:
GURL GetEffectiveURL(content::BrowserContext* browser_context,
const GURL& url) override {
if (effective_url_)
return *effective_url_;
return url;
}
bool IsSuitableHost(RenderProcessHost* process_host,
const GURL& site_url) override {
if (!disallowed_process_id_)
return true;
return process_host->GetDeprecatedID() != disallowed_process_id_;
}
void set_effective_url(const GURL& url) { effective_url_ = url; }
void set_disallowed_process(int id) { disallowed_process_id_ = id; }
private:
std::optional<GURL> effective_url_;
int disallowed_process_id_ = 0;
};
// While a document is open, state in the browser may change such that loading
// the document would choose a different SiteInstance. A cross-document
// navigation would pick up this different SiteInstance, but a same-document
// navigation should not. It should just navigate inside the currently loaded
// document instead of reloading the document.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentNavigationWhenSiteInstanceWouldChange) {
auto* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
GURL url0 = embedded_test_server()->GetURL("a.com", "/title1.html#ref1");
GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#ref2");
GetEffectiveUrlClient new_client;
NavigationHandleCommitObserver navigation_0(wc, url0);
EXPECT_TRUE(NavigateToURL(shell(), url0));
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
RenderFrameHost* main_frame_host = wc->GetPrimaryMainFrame();
RenderProcessHost* main_frame_process_host = main_frame_host->GetProcess();
// When we both change the effective URL and also disallow the current
// renderer process, a new load of the current document would get a different
// SiteInstance.
GURL modified_url0 =
embedded_test_server()->GetURL("c.com", "/title1.html#ref1");
new_client.set_effective_url(modified_url0);
new_client.set_disallowed_process(main_frame_process_host->GetDeprecatedID());
NavigationHandleCommitObserver navigation_1(wc, url1);
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_TRUE(navigation_1.was_same_document());
// The RenderFrameHost should not have changed, we should perform the
// navigation in the currently loaded document.
EXPECT_EQ(main_frame_host, wc->GetPrimaryMainFrame());
EXPECT_EQ(main_frame_process_host, wc->GetPrimaryMainFrame()->GetProcess());
}
// This tests the same ideas as the above test except in this case the same-
// document navigation is done through a history navigation, which exercises
// different codepaths in the NavigationControllerImpl.
IN_PROC_BROWSER_TEST_F(
NavigationBrowserTest,
SameDocumentHistoryNavigationWhenSiteInstanceWouldChange) {
auto* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
GURL url0 = embedded_test_server()->GetURL("a.com", "/title1.html#ref1");
GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#ref2");
NavigationHandleCommitObserver navigation_0(wc, url0);
NavigationHandleCommitObserver navigation_1(wc, url1);
GetEffectiveUrlClient new_client;
EXPECT_TRUE(NavigateToURL(shell(), url0));
EXPECT_TRUE(navigation_0.has_committed());
EXPECT_FALSE(navigation_0.was_same_document());
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_TRUE(navigation_1.was_same_document());
RenderFrameHost* main_frame_host = wc->GetPrimaryMainFrame();
RenderProcessHost* main_frame_process_host = main_frame_host->GetProcess();
// When we both change the effective URL and also disallow the current
// renderer process, a new load of the current document would get a different
// SiteInstance.
GURL modified_url0 =
embedded_test_server()->GetURL("c.com", "/title1.html#ref1");
new_client.set_effective_url(modified_url0);
new_client.set_disallowed_process(main_frame_process_host->GetDeprecatedID());
// Navigates to the same-document. Since the SiteInstance changed, we would
// normally try isolate this navigation by using a different RenderProcessHost
// and RenderFrameHost. But since it is same-document, we want to avoid that
// and perform the navigation inside the loaded |url0| document.
wc->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(wc));
// The RenderFrameHost should not have changed, we should perform the
// navigation in the currently loaded document.
EXPECT_EQ(main_frame_host, wc->GetPrimaryMainFrame());
EXPECT_EQ(main_frame_process_host, wc->GetPrimaryMainFrame()->GetProcess());
}
// Verify that actual renderer-initiated navigations to about:blank#blocked
// are respected, even though both the browser and renderer rewrite some illegal
// navigations to that URL as well. See https://crbug.com/40066983.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentHashNavigationToBlockedFragmentAllowed) {
const GURL url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
TestNavigationObserver blank_observer(web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.href = 'about:blank';"));
blank_observer.Wait();
GURL blocked_url = GURL(kBlockedURL);
TestNavigationObserver blocked_observer(web_contents());
EXPECT_TRUE(ExecJs(shell(), JsReplace("location.href = $1", blocked_url)));
blocked_observer.Wait();
// If the browser process receives a request to same-document navigate to
// about:blank#blocked, the URL should be used and not ignored (as in a
// blocked case like the SameDocumentLongURLHashNavigation test).
EXPECT_EQ(blocked_url, web_contents()->GetLastCommittedURL());
}
// Verify that same-document navigations from about:blank to an excessively long
// fragment do not crash the browser.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentAboutBlankLongURLHashNavigation) {
const GURL blank_url(url::kAboutBlankURL);
EXPECT_TRUE(NavigateToURL(shell(), blank_url));
std::string long_url = "#";
long_url.append(2 * url::kMaxURLChars, 'a');
EXPECT_TRUE(ExecJs(shell(), JsReplace("location = $1", long_url)));
// If the renderer attempts to navigate same-document from about:blank to a
// too long hash (>2 MB), the URL (and base URL) will be blocked and rewritten
// in the renderer to avoid the Mojo serialization limit. Ensure that the
// browser process does not crash due to an empty base URL, and that the
// blocked URL is used, unlike in the non-about:blank case in the
// SameDocumentLongURLHashNavigation test.
// TODO(crbug.com/40067230): Ideally this would be blocked earlier in the
// renderer process, failing the navigation.
EXPECT_EQ(GURL(kBlockedURL), web_contents()->GetLastCommittedURL());
// The renderer process considers the same-document navigation to the long URL
// to have successfully completed.
EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash"));
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentLongURLHashNavigation) {
const GURL url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string long_url = "#";
long_url.append(2 * url::kMaxURLChars, 'a');
EXPECT_TRUE(ExecJs(shell(), JsReplace("location = $1", long_url)));
// If the browser process receives a request to same-document navigate to a
// too long URL (>2 MB), it simply pretends that the renderer performed a
// same-document navigation to the currently committed URL (previously, it was
// mapped to about:blank#blocked, which could be confusing).
// TODO(crbug.com/40067230): Ideally this would be blocked in the renderer
// instead of having special browser-side handling.
EXPECT_EQ(url, web_contents()->GetLastCommittedURL());
// The renderer process enforces no such limit and should consider the
// same-document navigation to have successfully completed.
EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash"));
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentLongURLPushState) {
const GURL url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string long_url = "#";
long_url.append(2 * url::kMaxURLChars, 'a');
EXPECT_TRUE(ExecJs(
shell(), JsReplace("history.pushState('state', '', $1)", long_url)));
// If the browser process receives a request to same-document navigate to a
// too long URL (>2 MB), it simply pretends that the renderer performed a
// same-document navigation to the currently committed URL (previously, it was
// mapped to about:blank#blocked, which could be confusing).
// TODO(crbug.com/40067230): Ideally this would be blocked in the renderer
// instead of having special browser-side handling.
EXPECT_EQ(url, web_contents()->GetLastCommittedURL());
// The renderer process enforces no such limit and should consider the
// same-document navigation to have successfully completed.
EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash"));
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentLongURL204PopupHashNavigation) {
const GURL url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Open a popup window with a navigation that will result in a 204. This will
// result in a WebContents where the last committed URL is the empty URL.
const GURL nocontent_url(embedded_test_server()->GetURL("/nocontent"));
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", nocontent_url)));
Shell* opened_shell = new_shell_observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents()));
std::string long_url = "#";
long_url.append(2 * url::kMaxURLChars, 'a');
EXPECT_TRUE(ExecJs(opened_shell, JsReplace("location = $1", long_url)));
// Hash navigations in a popup in this state (incorrectly) perform a
// cross-document navigation. This is because the check for whether or not to
// perform a same-document navigation uses the initial empty Document's actual
// URL (which is, surprisingly enough, the empty URL) rather than URL the web
// platform generally sees (which is about:blank). As a result, the check ends
// up comparing the empty URL against the completed URL of about:blank#...,
// which means the URLs are not equal ignoring fragments, and Blink performs a
// cross-document navigation instead.
//
// TODO(crbug.com/40922971): This probably should be fixed to be treated as a
// same-document navigation.
EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents()));
EXPECT_EQ(GURL(kBlockedURL),
opened_shell->web_contents()->GetLastCommittedURL());
EXPECT_EQ(kBlockedURL, EvalJs(opened_shell->web_contents(), "location.href"));
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameDocumentLongURL204PopupPushState) {
const GURL url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Open a popup window with a navigation that will result in a 204. This will
// result in a WebContents where the last committed URL is the empty URL.
const GURL nocontent_url(embedded_test_server()->GetURL("/nocontent"));
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", nocontent_url)));
Shell* opened_shell = new_shell_observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents()));
std::string long_url = "#";
long_url.append(2 * url::kMaxURLChars, 'a');
// Blink incorrectly disallows pushState() because the security check is
// broken, since the security check uses the initial empty Document's actual
// URL (which is, surprisingly enough, the empty URL) rather than the URL the
// web platform generally sees (which is about:blank).
//
// TODO(crbug.com/40922971): This pushState() should probably be allowed.
EXPECT_EQ(
"SecurityError",
EvalJs(
opened_shell,
JsReplace(
"try { history.pushState('state', '', $1) } catch (e) { e.name }",
long_url)));
}
// Ensure that no crash occurs when doing a same-document navigation within a
// site-less SiteInstance, such as for a browser-initiated about:blank.
// See https://crbug.com/359807735.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentSitelessNavigation) {
WebContents* web_contents = shell()->web_contents();
GURL url1 = GURL("about:blank#1");
GURL url2 = GURL("about:blank#2");
NavigationHandleCommitObserver navigation_1(web_contents, url1);
NavigationHandleCommitObserver navigation_2(web_contents, url2);
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(NavigateToURL(shell(), url2));
EXPECT_TRUE(navigation_1.has_committed());
EXPECT_TRUE(navigation_2.has_committed());
EXPECT_FALSE(navigation_1.was_same_document());
EXPECT_TRUE(navigation_2.was_same_document());
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
NonDeterministicUrlRewritesUseLastUrl) {
// Lambda expressions cannot be assigned to function pointers if they use
// captures, so track how many times the handler is called using a non-const
// static variable.
static int rewrite_count;
rewrite_count = 0;
BrowserURLHandler::URLHandler handler_method =
[](GURL* url, BrowserContext* browser_context) {
GURL::Replacements replace_path;
if (rewrite_count > 0) {
replace_path.SetPathStr("title2.html");
} else {
replace_path.SetPathStr("title1.html");
}
*url = url->ReplaceComponents(replace_path);
rewrite_count++;
return true;
};
BrowserURLHandler::GetInstance()->AddHandlerPair(
handler_method, BrowserURLHandler::null_handler());
TestNavigationObserver observer(web_contents());
shell()->LoadURL(embedded_test_server()->GetURL("/virtual-url.html"));
observer.Wait();
EXPECT_EQ("/title2.html", observer.last_navigation_url().path());
EXPECT_EQ(2, rewrite_count);
}
// Create two windows. When the second is deleted, it initiates a navigation in
// the first. This is a situation where the navigation has an initiator frame
// token, but no corresponding RenderFrameHost.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
RendererInitiatedCrossWindowNavigationInPagehide) {
GURL url(embedded_test_server()->GetURL("/empty.html"));
GURL always_referrer_url(embedded_test_server()->GetURL(
"/set-header?Referrer-Policy: unsafe-url"));
// Setup the opener window.
EXPECT_TRUE(NavigateToURL(shell(), url));
// Setup the openee window;
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(
ExecJs(shell(), JsReplace("window.open($1);", always_referrer_url)));
Shell* openee_shell = new_shell_observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents()));
// When deleted, the openee will initiate a navigation in its opener.
EXPECT_TRUE(ExecJs(openee_shell, R"(
window.addEventListener("pagehide", () => {
opener.location.href = "about:blank";
})
)"));
RenderFrameHost* openee_rfh =
static_cast<WebContentsImpl*>(openee_shell->web_contents())
->GetPrimaryMainFrame();
// Issue a KeepAlive for the navigation state so that the PolicyContainerHost
// will still exist after the initiator RenderFrameHost is gone.
mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle> keep_alive;
static_cast<RenderFrameHostImpl*>(openee_rfh)
->IssueKeepAliveHandle(keep_alive.InitWithNewPipeAndPassReceiver());
auto initiator_global_token = openee_rfh->GetGlobalFrameToken();
base::RunLoop loop;
DidStartNavigationCallback callback(
web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) {
auto* request = NavigationRequest::From(handle);
const std::optional<blink::LocalFrameToken>& frame_token =
request->GetInitiatorFrameToken();
EXPECT_TRUE(frame_token.has_value());
EXPECT_EQ(initiator_global_token.frame_token, frame_token.value());
EXPECT_EQ(initiator_global_token.child_id,
request->GetInitiatorProcessId());
auto* initiator_rfh = RenderFrameHostImpl::FromFrameToken(
request->GetInitiatorProcessId(), *frame_token);
ASSERT_FALSE(initiator_rfh);
// Even if the initiator RenderFrameHost is gone, its policy container
// should still be around since the LocalFrame has not been destroyed
// yet.
PolicyContainerHost* initiator_policy_container =
RenderFrameHostImpl::GetPolicyContainerHost(
base::OptionalToPtr(frame_token),
request->GetInitiatorProcessId(),
web_contents()->GetPrimaryMainFrame()->GetStoragePartition());
ASSERT_TRUE(initiator_policy_container);
ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways,
initiator_policy_container->referrer_policy());
// Even if the initiator RenderFrameHost is gone, the navigation request
// (to "about:blank") should have inherited its policy container.
auto* initiator_policies =
request->GetInitiatorPolicyContainerPolicies();
ASSERT_TRUE(initiator_policies);
ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways,
initiator_policies->referrer_policy);
loop.Quit();
}));
// Delete the openee, which trigger the navigation in the opener.
openee_shell->Close();
loop.Run();
}
// A document initiates a form submission in another frame, then deletes itself.
// Check the initiator frame token.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FormSubmissionThenDeleteFrame) {
GURL url(embedded_test_server()->GetURL("/empty.html"));
GURL always_referrer_url(embedded_test_server()->GetURL(
"/set-header?Referrer-Policy: unsafe-url"));
// Setup the opener window.
EXPECT_TRUE(NavigateToURL(shell(), url));
// Setup the openee window;
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", url)));
Shell* openee_shell = new_shell_observer.GetShell();
// Create a 'named' iframe in the first window. This will be the target of the
// form submission.
EXPECT_TRUE(ExecJs(shell(), R"(
new Promise(resolve => {
let iframe = document.createElement("iframe");
iframe.onload = resolve;
iframe.name = 'form-submission-target';
iframe.src = location.href;
document.body.appendChild(iframe);
});
)"));
// Create an iframe in the second window. It will be initiating a form
// submission and removing itself before the scheduled form navigation occurs.
// This iframe will have referrer policy "unsafe-url".
EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents()));
EXPECT_TRUE(ExecJs(openee_shell, JsReplace(R"(
new Promise(resolve => {
let iframe = document.createElement('iframe');
iframe.onload = resolve;
iframe.src = $1;
document.body.appendChild(iframe);
});
)",
always_referrer_url)));
EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents()));
RenderFrameHost* initiator_rfh =
static_cast<WebContentsImpl*>(openee_shell->web_contents())
->GetPrimaryMainFrame()
->child_at(0)
->current_frame_host();
auto initiator_global_token = initiator_rfh->GetGlobalFrameToken();
base::RunLoop loop;
DidStartNavigationCallback callback(
web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) {
auto* request = NavigationRequest::From(handle);
ASSERT_TRUE(request->IsPost());
const std::optional<blink::LocalFrameToken>& frame_token =
request->GetInitiatorFrameToken();
EXPECT_TRUE(frame_token.has_value());
EXPECT_EQ(initiator_global_token.frame_token, frame_token.value());
EXPECT_EQ(initiator_global_token.child_id,
request->GetInitiatorProcessId());
auto* deleted_initiator_rfh = RenderFrameHostImpl::FromFrameToken(
request->GetInitiatorProcessId(), frame_token.value());
ASSERT_FALSE(deleted_initiator_rfh);
// Even if the initiator RenderFrameHost is gone, its policy container
// should still be around since the LocalFrame has not been destroyed
// yet.
PolicyContainerHost* initiator_policy_container =
RenderFrameHostImpl::GetPolicyContainerHost(
base::OptionalToPtr(frame_token),
request->GetInitiatorProcessId(),
web_contents()->GetPrimaryMainFrame()->GetStoragePartition());
ASSERT_TRUE(initiator_policy_container);
EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways,
initiator_policy_container->referrer_policy());
auto* initiator_policies =
request->GetInitiatorPolicyContainerPolicies();
ASSERT_TRUE(initiator_policies);
ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways,
initiator_policies->referrer_policy);
loop.Quit();
}));
// Initiate a form submission into the first window and delete the initiator.
EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents()));
ExecuteScriptAsync(initiator_rfh, R"(
let input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "my_token");
input.setAttribute("value", "my_value");
// Schedule a form submission navigation (which will occur in a separate
// task).
let form = document.createElement('form');
form.appendChild(input);
form.setAttribute("method", "POST");
form.setAttribute("action", "about:blank");
form.setAttribute("target", "form-submission-target");
document.body.appendChild(form);
form.submit();
// Delete this frame before the scheduled navigation occurs in the target
// frame.
parent.document.querySelector("iframe").remove();
)");
loop.Run();
}
// Same as the previous test, but for a remote frame navigation:
// A document initiates a form submission in a cross-origin frame, then deletes
// itself. Check the initiator frame token.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
FormSubmissionInRemoteFrameThenDeleteFrame) {
GURL url(embedded_test_server()->GetURL("/empty.html"));
GURL cross_origin_always_referrer_url(embedded_test_server()->GetURL(
"foo.com", "/set-header?Referrer-Policy: unsafe-url"));
// Setup the main page.
EXPECT_TRUE(NavigateToURL(shell(), url));
// Create a cross origin child iframe. This iframe will embed another iframe,
// which will initiate the navigation. The only purpose of this iframe is to
// allow its child to delete itself by issuing
// parent.document.querySelector("iframe").remove();
// (The main frame cannot do it because it is cross-origin.)
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(
let iframe = document.createElement('iframe');
iframe.src = $1;
document.body.appendChild(iframe);
)",
cross_origin_always_referrer_url)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHostImpl* middle_rfh =
current_frame_host()->child_at(0)->current_frame_host();
// Now create a grandchild iframe, which is same-origin with the parent (but
// cross-origin with the grandparent). The grandchild will initiate a form
// submission in the top frame and remove itself before the scheduled form
// navigation occurs. This iframe will have referrer policy "unsafe-url".
EXPECT_TRUE(ExecJs(middle_rfh, JsReplace(R"(
let iframe = document.createElement('iframe');
iframe.src = $1;
document.body.appendChild(iframe);
)",
cross_origin_always_referrer_url)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
RenderFrameHost* initiator_rfh =
middle_rfh->child_at(0)->current_frame_host();
auto initiator_global_token = initiator_rfh->GetGlobalFrameToken();
base::RunLoop loop;
DidStartNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
auto* request = NavigationRequest::From(handle);
ASSERT_TRUE(request->IsPost());
const std::optional<blink::LocalFrameToken>& frame_token =
request->GetInitiatorFrameToken();
EXPECT_TRUE(frame_token.has_value());
EXPECT_EQ(initiator_global_token.frame_token, frame_token.value());
EXPECT_EQ(initiator_global_token.child_id,
request->GetInitiatorProcessId());
auto* deleted_initiator_rfh = RenderFrameHostImpl::FromFrameToken(
request->GetInitiatorProcessId(), frame_token.value());
ASSERT_FALSE(deleted_initiator_rfh);
// Even if the initiator RenderFrameHost is gone, its policy container
// should still be around since the LocalFrame has not been destroyed
// yet.
PolicyContainerHost* initiator_policy_container =
RenderFrameHostImpl::GetPolicyContainerHost(
base::OptionalToPtr(frame_token),
request->GetInitiatorProcessId(),
web_contents()->GetPrimaryMainFrame()->GetStoragePartition());
ASSERT_TRUE(initiator_policy_container);
EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways,
initiator_policy_container->referrer_policy());
EXPECT_EQ(
network::mojom::ReferrerPolicy::kAlways,
request->GetInitiatorPolicyContainerPolicies()->referrer_policy);
loop.Quit();
}));
// Initiate a form submission into the main frame and delete the initiator.
ExecuteScriptAsync(initiator_rfh, R"(
let input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "my_token");
input.setAttribute("value", "my_value");
// Schedule a form submission navigation (which will occur in a separate
// task).
let form = document.createElement('form');
form.appendChild(input);
form.setAttribute("method", "POST");
form.setAttribute("action", "about:blank");
form.setAttribute("target", "_top");
document.body.appendChild(form);
form.submit();
// Delete this frame before the scheduled navigation occurs in the main
// frame.
parent.document.querySelector("iframe").remove();
)");
loop.Run();
}
// A class to intercept RemoteFrameHost IPCs, specifically OpenURL. When an
// OpenURL IPC is received, this interceptor closes the initiator's Shell,
// `shell_to_close`, and ensures the corresponding process exits before
// proceeding with the OpenURL call.
class InitiatorClosingOpenURLInterceptor
: public blink::mojom::RemoteFrameHostInterceptorForTesting {
public:
// `this` takes ownership of `shell_to_close` and eventually deletes it.
InitiatorClosingOpenURLInterceptor(content::RenderFrameProxyHost* proxy_host,
std::unique_ptr<Shell> shell_to_close,
RenderProcessHost* renderer_to_exit)
: shell_to_close_(std::move(shell_to_close)),
renderer_to_exit_(renderer_to_exit),
swapped_impl_(std::make_unique<mojo::test::ScopedSwapImplForTesting<
blink::mojom::RemoteFrameHost>>(
proxy_host->frame_host_receiver_for_testing(),
this)) {}
~InitiatorClosingOpenURLInterceptor() override = default;
blink::mojom::RemoteFrameHost* GetForwardingInterface() override {
return swapped_impl_->old_impl();
}
// This closes `shell_to_close_` and causes `renderer_to_exit_` to exit
// before forwarding the call to the RenderFrameProxyHost. This mimics the
// case where the frame that sent the OpenURL gets closed before the IPC
// reaches its destination. Once the OpenURL IPC is sent, the proxy should
// receive it, even if the sender is gone.
void OpenURL(blink::mojom::OpenURLParamsPtr params) override {
// `Close()` internally deletes the pointer, so it must be released so
// `shell_to_close_` doesn't point to a deleted value.
shell_to_close_.release()->Close();
renderer_to_exit_->Shutdown(content::RESULT_CODE_KILLED);
GetForwardingInterface()->OpenURL(std::move(params));
// Delete the swapped impl while the real RenderFrameProxyHost still exists,
// since we only need to intercept a single OpenURL call. The next task may
// delete the real impl.
swapped_impl_.reset();
// Clear the other raw_ptrs to avoid dangling pointers.
renderer_to_exit_ = nullptr;
}
private:
std::unique_ptr<Shell> shell_to_close_;
raw_ptr<RenderProcessHost> renderer_to_exit_;
// The `swapped_impl_` is a unique_ptr, so the member can be deleted before
// `this` gets destroyed. The original implementation would normally be
// swapped back in when `this` is destroyed. However, in this test, the
// `RenderFrameProxyHost` is deleted shortly after the OpenURL IPC is handled,
// and relying on normal scoper cleanup would cause a use-after-free. To avoid
// this, we early delete the `swapped_impl_` to swap back the original
// implementation as soon as `OpenURL()` has been processed.
std::unique_ptr<
mojo::test::ScopedSwapImplForTesting<blink::mojom::RemoteFrameHost>>
swapped_impl_;
};
// Test the case that once an OpenURL IPC is sent, it is received and the
// navigation occurs even if the sender is deleted while the IPC is in flight.
// This test opens a main frame, which opens a cross-site popup. The test then
// does a form submission to the popup and closes the main frame.
// Unlike FormSubmissionInRemoteFrameThenDeleteFrame, the initiator is the last
// (and only) frame of that SiteInstance. Deleting it usually causes proxies in
// the same SiteInstanceGroup to be deleted, meaning the OpenURL IPC may never
// be received.
//
// Fails on linux-bfcache-rel and android-bfcache-rel. See crbug.com/336671248.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_ANDROID)
#define MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL \
DISABLED_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL
#else
#define MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL \
FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL
#endif
IN_PROC_BROWSER_TEST_F(
NavigationBrowserTest,
MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL) {
// We crash a renderer in the OpenURL interceptor.
content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
content::IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
// Get a unique_ptr to the shell, which will be used to transfer ownership
// later on.
std::unique_ptr<Shell> shell_a = base::WrapUnique(CreateBrowser());
// Setup the main page, a.com. The referrer policy is needed to test the
// keep alive part of PolicyContainerHost.
GURL always_referrer_url_a(embedded_test_server()->GetURL(
"a.com", "/set-header?Referrer-Policy: unsafe-url"));
EXPECT_TRUE(NavigateToURL(shell_a.get(), always_referrer_url_a));
EXPECT_TRUE(WaitForLoadStop(shell_a->web_contents()));
// The a.com's RenderFrameHost will be the initiator of the form submission.
RenderFrameHostImpl* rfh_a = static_cast<RenderFrameHostImpl*>(
shell_a->web_contents()->GetPrimaryMainFrame());
// Create a cross origin popup that will be the target of the form submission.
// This is cross-site so we can test the case where the last RenderFrameHost
// for the initiator's site is gone when the initiator deletes itself, causing
// all proxies in its SiteInstanceGroup to potentially delete themselves
// before the OpenURL call can be received.
// However, we also need to be able to navigate the target frame, so this is
// opened as a popup.
GURL url_b(embedded_test_server()->GetURL("b.com", "/empty.html"));
WebContentsImpl* web_contents_b = nullptr;
{
WebContentsAddedObserver observer_b;
ASSERT_TRUE(ExecJs(rfh_a, JsReplace("window.open($1, '_bpopup')", url_b)));
web_contents_b = static_cast<WebContentsImpl*>(observer_b.GetWebContents());
}
WaitForLoadStop(web_contents_b);
base::RunLoop loop;
auto initiator_global_token = rfh_a->GetGlobalFrameToken();
// Register a callback to make sure the script below triggers a
// DidStartNavigation event to fire. This indicates that the popup main
// frame's proxy in A's SiteInstanceGroup received and ran the OpenURL call.
DidStartNavigationCallback callback(
web_contents_b, base::BindLambdaForTesting([&](NavigationHandle* handle) {
auto* request = NavigationRequest::From(handle);
ASSERT_TRUE(request->IsPost());
const std::optional<blink::LocalFrameToken>& frame_token =
request->GetInitiatorFrameToken();
EXPECT_TRUE(frame_token.has_value());
EXPECT_EQ(initiator_global_token.frame_token, frame_token.value());
EXPECT_EQ(initiator_global_token.child_id,
request->GetInitiatorProcessId());
// This is the RenderFrameHost in the WebContents that was forced to
// `Close()` in the interceptor, so it should be deleted.
auto* initiator_rfh = RenderFrameHostImpl::FromFrameToken(
request->GetInitiatorProcessId(), frame_token.value());
EXPECT_FALSE(initiator_rfh);
// Even if the initiator RenderFrameHost is gone, its
// PolicyContainerHost should still be around since the LocalFrame has
// not been destroyed yet.
PolicyContainerHost* initiator_policy_container =
RenderFrameHostImpl::GetPolicyContainerHost(
base::OptionalToPtr(frame_token),
request->GetInitiatorProcessId(),
web_contents()->GetPrimaryMainFrame()->GetStoragePartition());
ASSERT_TRUE(initiator_policy_container);
EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways,
initiator_policy_container->referrer_policy());
EXPECT_EQ(
network::mojom::ReferrerPolicy::kAlways,
request->GetInitiatorPolicyContainerPolicies()->referrer_policy);
loop.Quit();
}));
// Intercept the OpenURL call to the proxy for the popup in A's
// SiteInstanceGroup, which will happen in a separate navigation task posted
// from the form submission task in the script below.
// Ownership of `shell_a` is being transferred to the interceptor. The
// interceptor will delete `shell_a` so it should not be used after this.
SiteInstanceGroup* a_sig = rfh_a->GetSiteInstance()->group();
auto proxy_host_interceptor =
std::make_unique<InitiatorClosingOpenURLInterceptor>(
web_contents_b->GetPrimaryMainFrame()
->browsing_context_state()
->GetRenderFrameProxyHost(a_sig),
std::move(shell_a), a_sig->process());
// Initiate a form submission into the b.com popup that will navigate the
// popup to about:blank.
// We want the initiator to be closed between the time the about:blank OpenURL
// IPC is sent and received. This is done in the interceptor by closing the
// shell the initiator belongs to. The timing means window.close() is not a
// viable option: it would post a task after the OpenURL navigation task, so
// we can't ensure the window is closed before OpenURL runs.
ExecuteScriptAsync(rfh_a, R"(
let input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "my_token");
input.setAttribute("value", "my_value");
// Schedule a form submission navigation (which will occur in a separate
// task).
let form = document.createElement('form');
form.appendChild(input);
form.setAttribute("method", "POST");
form.setAttribute("action", "about:blank");
form.setAttribute("target", "_bpopup");
document.body.appendChild(form);
form.submit();
)");
loop.Run();
// Make sure the about:blank navigation finishes successfully.
WaitForLoadStop(web_contents_b);
EXPECT_EQ(GURL("about:blank"), web_contents_b->GetLastCommittedURL());
}
// Check that when RenderProcessHostImpl::DisableRefCounts is called while a
// NavigationStateKeepAlive exists, the navigation still succeeds. This is a
// regression test for crbug.com/348150830.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
DisableRefCountsWhileKeepAliveExists) {
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// This test needs the browser process to call DisableRefCounts after the form
// submission's NavigationStateKeepAlive is created and before the task that
// sends the BeginNavigation IPC. To do this, use EvalJS to return a string to
// the test framework between those two renderer-side tasks, allowing the
// browser process to reset the counts before the BeginNavigation IPC is
// received and the NavigationStateKeepAlive is destroyed.
std::string expected_str("Placeholder value");
std::string js_str = base::StringPrintf(
"f = document.createElement('form');"
"f.action = 'about:blank';"
"document.body.appendChild(f);"
"f.submit();"
"'%s';",
expected_str.c_str());
TestNavigationObserver observer(shell()->web_contents());
EXPECT_EQ(expected_str, EvalJs(shell(), js_str).ExtractString());
// Expect at this point that a NavigationStateKeepAlive has been created for
// the form submission.
NavigationStateKeepAlive* keep_alive =
current_frame_host()->GetStoragePartition()->GetNavigationStateKeepAlive(
current_frame_host()->GetFrameToken());
ASSERT_TRUE(keep_alive);
// Disable ref counts on the process, which resets all ref counts to 0. This
// seems to happen in practice in https://crbug.com/348150830 when a
// BrowserContext is closed before all of its frames are properly cleaned up,
// but the exact repro steps for this aren't known, so simulate this behavior
// with an explicit DisableRefCounts() call.
current_frame_host()->GetProcess()->DisableRefCounts();
// Wait for the navigation to complete. At that point, the
// NavigationStateKeepAlive goes away, which can possibly decrement the
// associated ref count. Since DisableRefCounts() was called, the ref count
// should not be further decremented, and the navigation should complete
// successfully.
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_TRUE(current_frame_host()->GetLastCommittedURL().IsAboutBlank());
}
using MediaNavigationBrowserTest = NavigationBaseBrowserTest;
// Media navigations synchronously complete the time of the `CommitNavigation`
// IPC call. Ensure that the renderer does not crash if the media navigation
// results in an HTTP error with no body, since the renderer will reentrantly
// commit an error page while handling the `CommitNavigation` IPC.
IN_PROC_BROWSER_TEST_F(MediaNavigationBrowserTest, FailedNavigation) {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
[](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_NOT_FOUND);
response->set_content_type("video/mp4");
return response;
}));
ASSERT_TRUE(embedded_test_server()->Start());
const GURL error_url(embedded_test_server()->GetURL("/moo.mp4"));
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_EQ(error_url, current_frame_host()->GetLastCommittedURL());
NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
}
using DocumentPolicyBrowserTest = NavigationBaseBrowserTest;
// Test that scroll restoration can be disabled with
// Document-Policy: force-load-at-top
IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest,
ScrollRestorationDisabledByDocumentPolicy) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/target.html");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target.html"));
TestNavigationManager navigation_manager(web_contents(), url);
// This test expects the document is freshly loaded on the back navigation
// so that the document policy to force-load-at-top will run. This will not
// happen if the document is back-forward cached, so we need to disable it.
DisableBackForwardCacheForTesting(web_contents(),
BackForwardCache::TEST_REQUIRES_NO_CACHING);
// Load the document with document policy force-load-at-top
shell()->LoadURL(url);
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top\r\n"
"\r\n"
"<p style='position: absolute; top: 10000px;'>Some text</p>");
response.Done();
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
{
RenderFrameSubmissionObserver frame_observer(web_contents());
// Scroll down the page a bit
EXPECT_TRUE(ExecJs(web_contents(), "window.scrollTo(0, 1000)"));
frame_observer.WaitForScrollOffsetAtTop(false);
}
// Navigate away
EXPECT_TRUE(ExecJs(web_contents(), "window.location = 'about:blank'"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
// Navigate back
EXPECT_TRUE(ExecJs(web_contents(), "history.back()"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
RunUntilInputProcessed(RenderWidgetHostImpl::From(
web_contents()->GetRenderViewHost()->GetWidget()));
const cc::RenderFrameMetadata& last_metadata =
RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata();
EXPECT_TRUE(last_metadata.is_scroll_offset_at_top);
}
// Test that scroll restoration works as expected with
// Document-Policy: force-load-at-top=?0
IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest,
ScrollRestorationEnabledByDocumentPolicy) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/target.html");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target.html"));
RenderFrameSubmissionObserver frame_observer(web_contents());
TestNavigationManager navigation_manager(web_contents(), url);
// Load the document with document policy force-load-at-top set to false.
shell()->LoadURL(url);
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top=?0\r\n"
"\r\n"
"<p style='position: absolute; top: 10000px;'>Some text</p>");
response.Done();
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
// Scroll down the page a bit
EXPECT_TRUE(ExecJs(web_contents(), "window.scrollTo(0, 1000)"));
frame_observer.WaitForScrollOffsetAtTop(false);
// Navigate away
EXPECT_TRUE(ExecJs(web_contents(), "window.location = 'about:blank'"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
// Navigate back
EXPECT_TRUE(ExecJs(web_contents(), "history.back()"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
// Ensure scroll restoration activated
frame_observer.WaitForScrollOffsetAtTop(false);
const cc::RenderFrameMetadata& last_metadata =
RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata();
EXPECT_FALSE(last_metadata.is_scroll_offset_at_top);
}
// Test that element fragment anchor scrolling can be disabled with
// Document-Policy: force-load-at-top
IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest,
FragmentAnchorDisabledByDocumentPolicy) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/target.html");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target.html#text"));
// Load the target document
TestNavigationManager navigation_manager(web_contents(), url);
shell()->LoadURL(url);
// Start navigation
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
// Send Document-Policy header
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top\r\n"
"\r\n"
"<p id='text' style='position: absolute; top: 10000px;'>Some text</p>");
response.Done();
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
// Wait a short amount of time to ensure the page does not scroll.
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
RunUntilInputProcessed(RenderWidgetHostImpl::From(
web_contents()->GetRenderViewHost()->GetWidget()));
const cc::RenderFrameMetadata& last_metadata =
RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata();
EXPECT_TRUE(last_metadata.is_scroll_offset_at_top);
}
// Test that element fragment anchor scrolling works as expected with
// Document-Policy: force-load-at-top=?0
IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest,
FragmentAnchorEnabledByDocumentPolicy) {
net::test_server::ControllableHttpResponse response(embedded_test_server(),
"/target.html");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/target.html#text"));
RenderFrameSubmissionObserver frame_observer(web_contents());
// Load the target document
TestNavigationManager navigation_manager(web_contents(), url);
shell()->LoadURL(url);
// Start navigation
EXPECT_TRUE(navigation_manager.WaitForRequestStart());
navigation_manager.ResumeNavigation();
// Send Document-Policy header
response.WaitForRequest();
response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Document-Policy: force-load-at-top=?0\r\n"
"\r\n"
"<p id='text' style='position: absolute; top: 10000px;'>Some text</p>");
response.Done();
EXPECT_TRUE(navigation_manager.WaitForResponse());
navigation_manager.ResumeNavigation();
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host()));
frame_observer.WaitForScrollOffsetAtTop(
/*expected_scroll_offset_at_top=*/false);
const cc::RenderFrameMetadata& last_metadata =
RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata();
EXPECT_FALSE(last_metadata.is_scroll_offset_at_top);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitBasic) {
GURL url = embedded_test_server()->GetURL("a.com", "/empty.html");
auto origin_expected = url::Origin::Create(url);
TestNavigationManager manager(web_contents(), url);
shell()->LoadURL(url);
EXPECT_TRUE(manager.WaitForResponse());
NavigationRequest* navigation = main_frame()->navigation_request();
std::optional<url::Origin> origin_to_commit = navigation->GetOriginToCommit();
ASSERT_TRUE(origin_to_commit.has_value());
ASSERT_TRUE(manager.WaitForNavigationFinished());
url::Origin origin_committed = current_frame_host()->GetLastCommittedOrigin();
EXPECT_FALSE(origin_to_commit->opaque());
EXPECT_FALSE(origin_committed.opaque());
EXPECT_EQ(origin_expected, *origin_to_commit);
EXPECT_EQ(origin_expected, origin_committed);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommit204) {
GURL url = embedded_test_server()->GetURL("a.com", "/nocontent");
TestNavigationManager manager(web_contents(), url);
shell()->LoadURL(url);
EXPECT_TRUE(manager.WaitForResponse());
NavigationRequest* navigation = main_frame()->navigation_request();
std::optional<url::Origin> origin_to_commit = navigation->GetOriginToCommit();
EXPECT_FALSE(origin_to_commit.has_value());
ASSERT_TRUE(manager.WaitForNavigationFinished());
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
OriginToCommitSandboxFromResponse) {
GURL url = embedded_test_server()->GetURL(
"a.com", "/set-header?Content-Security-Policy: sandbox");
TestNavigationManager manager(web_contents(), url);
shell()->LoadURL(url);
EXPECT_TRUE(manager.WaitForResponse());
NavigationRequest* navigation = main_frame()->navigation_request();
url::Origin origin_to_commit = navigation->GetOriginToCommit().value();
ASSERT_TRUE(manager.WaitForNavigationFinished());
url::Origin origin_committed = current_frame_host()->GetLastCommittedOrigin();
EXPECT_TRUE(origin_to_commit.opaque());
EXPECT_TRUE(origin_committed.opaque());
// TODO(crbug.com/40092527). The nonce must match.
EXPECT_NE(origin_to_commit, origin_committed);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
OriginToCommitSandboxFromParentDocument) {
GURL url_top = embedded_test_server()->GetURL(
"a.com", "/set-header?Content-Security-Policy: sandbox allow-scripts");
EXPECT_TRUE(NavigateToURL(shell(), url_top));
GURL url_iframe = embedded_test_server()->GetURL("a.com", "/empty.html");
TestNavigationManager manager(web_contents(), url_iframe);
ExecuteScriptAsync(current_frame_host(), R"(
let iframe = document.createElement("iframe");
iframe.src = "./empty.html";
document.body.appendChild(iframe);
)");
EXPECT_TRUE(manager.WaitForResponse());
FrameTreeNode* iframe = current_frame_host()->child_at(0);
NavigationRequest* navigation = iframe->navigation_request();
url::Origin origin_to_commit = navigation->GetOriginToCommit().value();
ASSERT_TRUE(manager.WaitForNavigationFinished());
url::Origin origin_committed =
iframe->current_frame_host()->GetLastCommittedOrigin();
EXPECT_TRUE(origin_to_commit.opaque());
EXPECT_TRUE(origin_committed.opaque());
// TODO(crbug.com/40092527). The nonce must match.
EXPECT_NE(origin_to_commit, origin_committed);
// Both document have the same URL. Only the first sets CSP:sandbox, but both
// are sandboxed. They get an opaque origin different from each others.
EXPECT_NE(current_frame_host()->GetLastCommittedOrigin(), origin_committed);
}
// Regression test for https://crbug.com/1158306.
// Navigate to a response, which set Content-Security-Policy: sandbox AND block
// the response. The error page shouldn't set sandbox flags.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ErrorPageFromCspSandboxResponse) {
// Block every navigation in WillProcessResponse.
std::unique_ptr<content::TestNavigationThrottleInserter> blocker =
BlockNavigationWillProcessResponse(web_contents());
// Navigate toward a document witch sets CSP:sandbox.
GURL url = embedded_test_server()->GetURL(
"a.com", "/set-header?Content-Security-Policy: sandbox");
TestNavigationManager manager(web_contents(), url);
shell()->LoadURL(url);
ASSERT_TRUE(manager.WaitForNavigationFinished());
// An error page committed. It doesn't have any sandbox flags, despite the
// original response headers.
EXPECT_TRUE(current_frame_host()->IsErrorDocument());
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
current_frame_host()->active_sandbox_flags());
EXPECT_EQ(url, current_frame_host()->GetLastCommittedURL());
EXPECT_TRUE(current_frame_host()->GetLastCommittedOrigin().opaque());
EXPECT_TRUE(
current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(url));
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
ProcessShutdownDuringDeferredNavigationThrottle) {
GURL url = embedded_test_server()->GetURL("a.com", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
class ShutdownThrottle : public TaskRunnerDeferringThrottle,
WebContentsObserver {
public:
explicit ShutdownThrottle(WebContents* web_contents,
NavigationHandle* handle)
: TaskRunnerDeferringThrottle(
base::SingleThreadTaskRunner::GetCurrentDefault(),
/*defer_start=*/false,
/*defer_redirect=*/false,
/*defer_response=*/true,
handle),
web_contents_(web_contents) {
WebContentsObserver::Observe(web_contents_);
}
void AsyncResume() override {
// Shutdown the renderer and delay Resume() until then.
web_contents_->GetPrimaryMainFrame()->GetProcess()->Shutdown(1);
}
void RenderFrameDeleted(RenderFrameHost* frame_host) override {
TaskRunnerDeferringThrottle::AsyncResume();
}
private:
raw_ptr<WebContents> web_contents_;
};
auto inserter = std::make_unique<TestNavigationThrottleInserter>(
shell()->web_contents(),
base::BindLambdaForTesting(
[&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> {
return std::make_unique<ShutdownThrottle>(shell()->web_contents(),
handle);
}));
class DoesNotReadyToCommitObserver : public WebContentsObserver {
public:
explicit DoesNotReadyToCommitObserver(WebContents* contents)
: WebContentsObserver(contents) {}
// WebContentsObserver overrides.
void ReadyToCommitNavigation(NavigationHandle* handle) override {
// This method should not happen. Since the process is destroyed before
// we become ready to commit, we can not ever reach
// ReadyToCommitNavigation. Doing so would fail because the renderer is
// gone.
ADD_FAILURE() << "ReadyToCommitNavigation but renderer has crashed. "
"IsRenderFrameLive: "
<< handle->GetRenderFrameHost()->IsRenderFrameLive();
navigation_was_ready_to_commit_ = true;
}
void DidFinishNavigation(NavigationHandle* handle) override {
navigation_finished_ = true;
navigation_committed_ = handle->HasCommitted();
}
bool navigation_was_ready_to_commit() {
return navigation_was_ready_to_commit_;
}
bool navigation_finished() { return navigation_finished_; }
bool navigation_committed() { return navigation_committed_; }
private:
bool navigation_was_ready_to_commit_ = false;
bool navigation_finished_ = false;
bool navigation_committed_ = false;
};
// Watch that ReadyToCommitNavigation() will not happen when the renderer is
// gone.
DoesNotReadyToCommitObserver no_commit_obs(shell()->web_contents());
// We will shutdown the renderer during this navigation.
ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
// Important: This is a browser-initiated navigation, so the NavigationRequest
// does not have an open connection (NavigationClient) to the renderer that it
// is listening to for termination while running NavigationThrottles.
//
// Expect this navigation to be aborted, so we stop waiting after the
// uncommitted navigation is done.
GURL url2 = embedded_test_server()->GetURL("a.com", "/title1.html");
NavigateToURLBlockUntilNavigationsComplete(
shell(), url2, /*number_of_navigations=*/1,
/*ignore_uncommitted_navigations=*/false);
// The renderer was shutdown mid-navigation.
EXPECT_FALSE(
shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive());
// The navigation was aborted, which means it finished but did not commit, and
// _importantly_ it never reported "ReadyToCommitNavigation" without a live
// renderer.
EXPECT_TRUE(no_commit_obs.navigation_finished());
EXPECT_FALSE(no_commit_obs.navigation_was_ready_to_commit());
EXPECT_FALSE(no_commit_obs.navigation_committed());
}
// Sandbox flags defined by the parent must not apply to Chrome's error page.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ErrorPageFromInSandboxedIframe) {
GURL url = embedded_test_server()->GetURL("a.com", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
// Block every navigation in WillProcessResponse.
std::unique_ptr<content::TestNavigationThrottleInserter> blocker =
BlockNavigationWillProcessResponse(web_contents());
TestNavigationManager manager(web_contents(), url);
ExecuteScriptAsync(current_frame_host(), R"(
let iframe = document.createElement("iframe");
iframe.src = location.href;
iframe.sandbox = "allow-orientation-lock";
document.body.appendChild(iframe);
)");
ASSERT_TRUE(manager.WaitForNavigationFinished());
RenderFrameHostImpl* child_rfh =
current_frame_host()->child_at(0)->current_frame_host();
EXPECT_TRUE(child_rfh->IsErrorDocument());
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
child_rfh->active_sandbox_flags());
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitSandboxFromFrame) {
GURL url = embedded_test_server()->GetURL("a.com", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
TestNavigationManager manager(web_contents(), url);
ExecuteScriptAsync(current_frame_host(), R"(
let iframe = document.createElement("iframe");
iframe.src = location.href;
iframe.sandbox = "";
document.body.appendChild(iframe);
)");
EXPECT_TRUE(manager.WaitForResponse());
FrameTreeNode* iframe = current_frame_host()->child_at(0);
NavigationRequest* navigation = iframe->navigation_request();
url::Origin origin_to_commit = navigation->GetOriginToCommit().value();
ASSERT_TRUE(manager.WaitForNavigationFinished());
url::Origin origin_committed =
iframe->current_frame_host()->GetLastCommittedOrigin();
EXPECT_TRUE(origin_to_commit.opaque());
EXPECT_TRUE(origin_committed.opaque());
// TODO(crbug.com/40092527). Make the nonce to match.
EXPECT_NE(origin_to_commit, origin_committed);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
NavigateToAboutBlankWhileFirstNavigationPending) {
GURL url_a = embedded_test_server()->GetURL("a.com", "/empty.html");
GURL url_b = embedded_test_server()->GetURL("b.com", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), url_a));
ShellAddedObserver new_shell_observer;
ExecuteScriptAsync(
current_frame_host(),
JsReplace("window.open($1, '_blank').location = 'about:blank'", url_b));
WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
TestNavigationManager manager_1(popup_contents, url_b);
TestNavigationManager manager_2(popup_contents, GURL("about:blank"));
ASSERT_TRUE(manager_1.WaitForNavigationFinished());
ASSERT_TRUE(manager_2.WaitForNavigationFinished());
EXPECT_EQ(popup_contents->GetLastCommittedURL(), "about:blank");
}
class NetworkIsolationSplitCacheAppendIframeOrigin
: public NavigationBaseBrowserTest {
public:
NetworkIsolationSplitCacheAppendIframeOrigin() {
feature_list_.InitAndEnableFeature(
net::features::kSplitCacheByNetworkIsolationKey);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Make a main document, have it request a cacheable subresources. Then make a
// same-site document in an iframe that serves the CSP:Sandbox header. Stop the
// test server, have the sandboxed document requests the same subresource. The
// request should fail. To make sure the request is actually in the cache, the
// main document should be able to request it again.
IN_PROC_BROWSER_TEST_F(NetworkIsolationSplitCacheAppendIframeOrigin,
SandboxedUsesDifferentCache) {
auto server = std::make_unique<net::EmbeddedTestServer>();
server->AddDefaultHandlers(GetTestDataFilePath());
EXPECT_TRUE(server->Start());
GURL url_main_document = server->GetURL("a.com", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), url_main_document));
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
new Promise(resolve => {
let iframe = document.createElement("iframe");
iframe.onload = resolve;
iframe.src = "/set-header?Content-Security-Policy: sandbox allow-scripts";
document.body.appendChild(iframe);
})
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHostImpl* main_rfh = current_frame_host();
RenderFrameHostImpl* sub_rfh = main_rfh->child_at(0)->current_frame_host();
EXPECT_FALSE(main_rfh->GetLastCommittedOrigin().opaque());
EXPECT_TRUE(sub_rfh->GetLastCommittedOrigin().opaque());
const char* fetch_cacheable = R"(
fetch("cacheable.svg")
.then(() => "success")
.catch(() => "error")
)";
EXPECT_EQ("success", EvalJs(main_rfh, fetch_cacheable));
server.reset();
EXPECT_EQ("error", EvalJs(sub_rfh, fetch_cacheable));
EXPECT_EQ("success", EvalJs(main_rfh, fetch_cacheable));
}
// The Content Security Policy directive 'treat-as-public-address' is parsed
// into the parsed headers by services/network and applied there. That directive
// is ignored in report-only policies. Here we check that Blink reports a
// console message if 'treat-as-public-address' is delivered in a report-only
// policy. This serves also as a regression test for https://crbug.com/1150314
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
TreatAsPublicAddressInReportOnly) {
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"The Content Security Policy directive 'treat-as-public-address' is "
"ignored when delivered in a report-only policy.");
GURL url = embedded_test_server()->GetURL(
"/set-header?"
"Content-Security-Policy-Report-Only: treat-as-public-address");
EXPECT_TRUE(NavigateToURL(shell(), url));
ASSERT_TRUE(console_observer.Wait());
}
// The Content Security Policy directive 'plugin-types' has been removed. Here
// we check that Blink reports a console message if 'plugin-type' is delivered
// in a policy.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
ContentSecurityPolicyErrorPluginTypes) {
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(
"The Content-Security-Policy directive 'plugin-types' has been removed "
"from the specification. "
"If you want to block plugins, consider specifying \"object-src 'none'\" "
"instead.");
GURL url = embedded_test_server()->GetURL(
"/set-header?"
"Content-Security-Policy: plugin-types application/pdf");
EXPECT_TRUE(NavigateToURL(shell(), url));
ASSERT_TRUE(console_observer.Wait());
}
class SubresourceLoadingTest : public NavigationBrowserTest {
public:
SubresourceLoadingTest() = default;
SubresourceLoadingTest(const SubresourceLoadingTest&) = delete;
SubresourceLoadingTest& operator=(const SubresourceLoadingTest&) = delete;
void DontTestNetworkServiceCrashes() {
test_network_service_crashes_ = false;
}
void VerifyResultsOfAboutBlankNavigation(RenderFrameHost* target_frame,
RenderFrameHost* initiator_frame) {
// Verify that `target_frame` has been navigated to "about:blank".
EXPECT_EQ(GURL(url::kAboutBlankURL), target_frame->GetLastCommittedURL());
// Verify that "about:blank" committed with the expected origin, and in the
// expected SiteInstance.
EXPECT_EQ(target_frame->GetLastCommittedOrigin(),
initiator_frame->GetLastCommittedOrigin());
EXPECT_EQ(target_frame->GetSiteInstance(),
initiator_frame->GetSiteInstance());
// Ask for cookies in the `target_frame`. One implicit verification here
// is whether this step will hit any `cookie_url`-related NOTREACHED or DwoC
// in RestrictedCookieManager::ValidateAccessToCookiesAt. This verification
// is non-racey, because `document.cookie` must have heard back from the
// RestrictedCookieManager before returning the value of cookies (this
// ignores possible Blink-side caching, but this is the first time the
// renderer needs the cookies and so this is okay for this test).
EXPECT_EQ("", EvalJs(target_frame, "document.cookie"));
// Verify that the "about:blank" frame is able to load an image.
VerifyImageSubresourceLoads(target_frame);
}
void VerifyImageSubresourceLoads(
const ToRenderFrameHost& target,
const std::string& target_document = "document") {
RenderFrameHostImpl* target_frame =
static_cast<RenderFrameHostImpl*>(target.render_frame_host());
VerifySingleImageSubresourceLoad(target_frame, target_document);
// Verify detecting and recovering from a NetworkService crash (e.g. via the
// `network_service_disconnect_handler_holder_mojo` field and the
// UpdateSubresourceLoaderFactories method of RenderFrameHostImpl).
if (!IsInProcessNetworkService() && test_network_service_crashes_) {
SimulateNetworkServiceCrash();
// In addition to waiting (inside SimulateNetworkServiceCrash above) for
// getting notified about being disconnected from
// network::mojom::NetworkServiceTest, we also want to make sure that the
// relevant RenderFrameHost realizes that the NetworkService has crashed.
// Which RenderFrameHost is relevant varies from test to test, so we
// flush multiple frames and use kDoNothingIfNoNetworkServiceConnection.
FlushNetworkInterfacesInOpenerChain(target_frame);
// Rerun the test after the NetworkService crash.
VerifySingleImageSubresourceLoad(target_frame, target_document);
}
}
private:
void FlushNetworkInterfacesInOpenerChain(RenderFrameHostImpl* current_frame) {
std::set<WebContents*> visited_contents;
while (true) {
// Check if we've already visited the current frame tree.
DCHECK(current_frame);
WebContents* current_contents =
WebContents::FromRenderFrameHost(current_frame);
DCHECK(current_contents);
if (base::Contains(visited_contents, current_contents))
break;
visited_contents.insert(current_contents);
// Flush all the frames in the `current_contents's active page.
current_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost(
[](RenderFrameHost* frame_to_flush) {
constexpr bool kDoNothingIfNoNetworkServiceConnection = true;
frame_to_flush->FlushNetworkAndNavigationInterfacesForTesting(
kDoNothingIfNoNetworkServiceConnection);
});
// Traverse the `current_frame`'s opener chain.
if (FrameTreeNode* opener_node =
current_frame->frame_tree_node()->opener()) {
current_frame = opener_node->current_frame_host();
} else {
break; // Break out of the loop if there is no opener.
}
}
}
void VerifySingleImageSubresourceLoad(RenderFrameHost* target,
std::string_view target_document) {
// Use a random, GUID-based hostname, to avoid hitting the network cache.
GURL image_url = embedded_test_server()->GetURL(
base::Uuid::GenerateRandomV4().AsLowercaseString() + ".com",
"/blank.jpg");
const std::string script = base::StrCat({
R"(new Promise(resolve => {
let img = document.createElement('img'); )",
JsReplace("img.src = $1;", image_url),
R"( img.addEventListener('load', () => {
resolve('allowed');
});
img.addEventListener('error', err => {
resolve(`error: ${err}`);
}); )",
target_document, R"(.body.appendChild(img);
}); )"});
EXPECT_EQ("allowed", EvalJs(target, script));
}
bool test_network_service_crashes_ = true;
};
// The test below verifies that an "about:blank" navigation commits with the
// right origin, even when the initiator of the navigation is not the parent or
// opener of the frame targeted by the navigation. In the
// GrandchildToAboutBlank... testcases, the navigation is initiated by the
// grandparent of the target frame.
//
// In this test case there are no process swaps and the parent of the navigated
// frame is a local frame (even in presence of site-per-process). See also
// GrandchildToAboutBlank_ABA_CrossSite and
// GrandchildToAboutBlank_ABB_CrossSite.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
GrandchildToAboutBlank_ABA_SameSite) {
GURL url(embedded_test_server()->GetURL(
"a.example.com",
"/cross_site_iframe_factory.html?"
"a.example.com(b.example.com(a.example.com))"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Verify the desired properties of the test setup.
RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderFrameHostImpl* child_frame =
main_frame->child_at(0)->current_frame_host();
RenderFrameHostImpl* grandchild_frame =
child_frame->child_at(0)->current_frame_host();
EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
EXPECT_EQ(main_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance());
EXPECT_EQ(main_frame->GetLastCommittedOrigin(),
grandchild_frame->GetLastCommittedOrigin());
EXPECT_NE(main_frame->GetLastCommittedOrigin(),
child_frame->GetLastCommittedOrigin());
// Navigate the grandchild frame to about:blank
ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'"));
TestNavigationObserver nav_observer(shell()->web_contents(), 1);
ASSERT_TRUE(
ExecJs(main_frame,
"grandchild_window = window.open('about:blank', 'grandchild')"));
nav_observer.Wait();
// Verify that the grandchild has the same origin as the main frame (*not* the
// origin of the parent frame).
main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
child_frame = main_frame->child_at(0)->current_frame_host();
grandchild_frame = child_frame->child_at(0)->current_frame_host();
VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame);
}
// The test below verifies that an "about:blank" navigation commits with the
// right origin, even when the initiator of the navigation is not the parent or
// opener of the frame targeted by the navigation. In the
// GrandchildToAboutBlank... testcases, the navigation is initiated by the
// grandparent of the target frame.
//
// In this test case there are no process swaps and the parent of the navigated
// frame is a remote frame (in presence of site-per-process). See also
// GrandchildToAboutBlank_ABA_SameSite and GrandchildToAboutBlank_ABB_CrossSite.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
GrandchildToAboutBlank_ABA_CrossSite) {
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(a))"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Verify the desired properties of the test setup.
RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderFrameHostImpl* child_frame =
main_frame->child_at(0)->current_frame_host();
RenderFrameHostImpl* grandchild_frame =
child_frame->child_at(0)->current_frame_host();
if (AreStrictSiteInstancesEnabled()) {
EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
} else {
EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
}
EXPECT_EQ(main_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance());
EXPECT_EQ(main_frame->GetLastCommittedOrigin(),
grandchild_frame->GetLastCommittedOrigin());
EXPECT_NE(main_frame->GetLastCommittedOrigin(),
child_frame->GetLastCommittedOrigin());
// Navigate the grandchild frame to about:blank
ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'"));
TestNavigationObserver nav_observer(shell()->web_contents(), 1);
ASSERT_TRUE(
ExecJs(main_frame,
"grandchild_window = window.open('about:blank', 'grandchild')"));
nav_observer.Wait();
// Verify that the grandchild has the same origin as the main frame (*not* the
// origin of the parent frame).
main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
child_frame = main_frame->child_at(0)->current_frame_host();
grandchild_frame = child_frame->child_at(0)->current_frame_host();
VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame);
}
// The test below verifies that an "about:blank" navigation commits with the
// right origin, even when the initiator of the navigation is not the parent or
// opener of the frame targeted by the navigation. In the
// GrandchildToAboutBlank... testcases, the navigation is initiated by the
// grandparent of the target frame.
//
// In this test case the navigation forces a process swap of the target frame.
// See also GrandchildToAboutBlank_ABA_SameSite and
// GrandchildToAboutBlank_ABA_CrossSite.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
GrandchildToAboutBlank_ABB_CrossSite) {
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(b))"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Verify the desired properties of the test setup.
RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderFrameHostImpl* child_frame =
main_frame->child_at(0)->current_frame_host();
RenderFrameHostImpl* grandchild_frame =
child_frame->child_at(0)->current_frame_host();
if (AreStrictSiteInstancesEnabled()) {
EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
} else {
EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
}
EXPECT_EQ(child_frame->GetSiteInstance(),
grandchild_frame->GetSiteInstance());
EXPECT_EQ(child_frame->GetLastCommittedOrigin(),
grandchild_frame->GetLastCommittedOrigin());
EXPECT_NE(main_frame->GetLastCommittedOrigin(),
grandchild_frame->GetLastCommittedOrigin());
// Navigate the grandchild frame to about:blank
ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'"));
TestNavigationObserver nav_observer(shell()->web_contents(), 1);
ASSERT_TRUE(
ExecJs(main_frame,
"grandchild_window = window.open('about:blank', 'grandchild')"));
nav_observer.Wait();
// Verify that the grandchild has the same origin as the main frame (*not* the
// origin of the parent frame).
main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
child_frame = main_frame->child_at(0)->current_frame_host();
grandchild_frame = child_frame->child_at(0)->current_frame_host();
VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame);
}
// The test below verifies that an "about:blank" navigation commits with the
// right origin, even when the initiator of the navigation is not the parent or
// opener of the frame targeted by the navigation. In the
// TopToAboutBlank_CrossSite testcase, the top-level navigation is initiated by
// a cross-site subframe.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, TopToAboutBlank_CrossSite) {
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Verify the desired properties of the test setup.
RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderFrameHostImpl* child_frame =
main_frame->child_at(0)->current_frame_host();
if (AreStrictSiteInstancesEnabled()) {
EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
} else {
EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
}
url::Origin a_origin =
url::Origin::Create(embedded_test_server()->GetURL("a.com", "/"));
url::Origin b_origin =
url::Origin::Create(embedded_test_server()->GetURL("b.com", "/"));
EXPECT_EQ(a_origin, main_frame->GetLastCommittedOrigin());
EXPECT_EQ(b_origin, child_frame->GetLastCommittedOrigin());
// Have the subframe initiate navigation of the main frame to about:blank.
//
// (Note that this scenario is a bit artificial/silly, because the final
// about:blank frame won't have any same-origin friends that could populate
// it. OTOH, it is still important to maintain all the invariants in this
// scenario. And it is still possible that a same-origin frame (e.g. in
// another window in the same BrowsingInstance) exists and can populate the
// about:blank frame.
TestNavigationObserver nav_observer(shell()->web_contents(), 1);
ASSERT_TRUE(ExecJs(child_frame, "window.top.location = 'about:blank'"));
nav_observer.Wait();
// Verify that the main frame is the only remaining frame and that it has the
// same origin as the navigation initiator.
main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
EXPECT_EQ(0u, main_frame->child_count());
EXPECT_EQ(b_origin, main_frame->GetLastCommittedOrigin());
EXPECT_EQ(GURL(url::kAboutBlankURL), main_frame->GetLastCommittedURL());
}
// The test below verifies that an "about:blank" navigation commits with the
// right origin, even when the initiator of the navigation is not the parent or
// opener of the frame targeted by the navigation. In the
// SameSiteSiblingToAboutBlank_CrossSiteTop testcase, the navigation is
// initiated by a same-origin sibling (notably, not by one of target frame's
// ancestors) and both siblings are subframes of a cross-site main frame.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
SameSiteSiblingToAboutBlank_CrossSiteTop) {
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,b)"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Name the 2nd child.
RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderFrameHostImpl* child_frame1 =
main_frame->child_at(0)->current_frame_host();
RenderFrameHostImpl* child_frame2 =
main_frame->child_at(1)->current_frame_host();
ASSERT_TRUE(ExecJs(child_frame2, "window.name = 'child2'"));
// Grab `child2` window from the 1st child...
ASSERT_TRUE(ExecJs(child_frame1, "child2 = window.open('', 'child2')"));
// ...but make sure that child2's opener doesn't point to child1.
ASSERT_TRUE(ExecJs(main_frame, "child2 = window.open('', 'child2')"));
EXPECT_EQ(true, EvalJs(child_frame2, "window.opener == window.top"));
// From child1 initiate navigation of child2 to about:blank.
TestNavigationObserver nav_observer(shell()->web_contents(), 1);
ASSERT_TRUE(ExecJs(child_frame1, "child2.location = 'about:blank'"));
nav_observer.Wait();
// Verify that child2 has the origin of the initiator of the navigation.
main_frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
child_frame1 = main_frame->child_at(0)->current_frame_host();
child_frame2 = main_frame->child_at(1)->current_frame_host();
VerifyResultsOfAboutBlankNavigation(child_frame2, child_frame1);
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory. Note that some aspects of the current behavior (e.g. the
// synchronous re-navigation) are not spec-compliant - see
// https://crbug.com/778318 and https://github.com/whatwg/html/issues/3267.
// Note that the same behavior is expected in the ...NewFrameWithoutSrc and
// in the ...NewFrameWithAboutBlank testcases.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_NewFrameWithoutSrc) {
GURL opener_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), opener_url));
// This inserts an `iframe` element without an `src` attribute. According to
// some specs "the browsing context will remain at the initial about:blank
// page", although other specs suggest that there is an explicit, separate
// navigation. See:
// https://html.spec.whatwg.org/dev/iframe-embed-object.html#the-iframe-element
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#shared-attribute-processing-steps-for-iframe-and-frame-elements
ASSERT_TRUE(ExecJs(shell(), R"( let ifr = document.createElement('iframe');
document.body.appendChild(ifr); )"));
WaitForLoadStop(shell()->web_contents());
RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame();
RenderFrameHost* subframe = ChildFrameAt(main_frame, 0);
VerifyResultsOfAboutBlankNavigation(subframe, main_frame);
}
// See the doc comment for the
// URLLoaderFactoryInInitialEmptyDoc_NewFrameWithoutSrc test case.
IN_PROC_BROWSER_TEST_F(
SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_NewFrameWithAboutBlank) {
GURL opener_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), opener_url));
ASSERT_TRUE(ExecJs(shell(), R"( ifr = document.createElement('iframe');
ifr.src = 'about:blank';
document.body.appendChild(ifr); )"));
WaitForLoadStop(shell()->web_contents());
RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame();
RenderFrameHost* subframe = ChildFrameAt(main_frame, 0);
VerifyResultsOfAboutBlankNavigation(subframe, main_frame);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameOriginFlagOfSameOriginAboutBlankNavigation) {
GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
GURL iframe_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), parent_url));
EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"(
let iframe = document.createElement('iframe');
iframe.src = $1;
document.body.appendChild(iframe);
)",
iframe_url)));
WaitForLoadStop(shell()->web_contents());
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_TRUE(handle->IsSameOrigin());
loop.Quit();
}));
// Changing the src to trigger DidFinishNavigationCallback
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
document.querySelector("iframe").src = 'about:blank';
)"));
loop.Run();
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameOriginFlagOfCrossOriginAboutBlankNavigation) {
GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
GURL iframe_url(embedded_test_server()->GetURL("b.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), parent_url));
EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"(
let iframe = document.createElement('iframe');
iframe.src = $1;
document.body.appendChild(iframe);
)",
iframe_url)));
WaitForLoadStop(shell()->web_contents());
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_FALSE(handle->IsSameOrigin());
loop.Quit();
}));
// Changing the src to trigger DidFinishNavigationCallback
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
document.querySelector("iframe").src = 'about:blank';
)"));
loop.Run();
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameOriginFlagOfSrcdocNavigation) {
GURL url = embedded_test_server()->GetURL("a.com", "/empty.html");
GURL cross_origin = embedded_test_server()->GetURL("b.com", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
// Navigating to about:srcdoc from the initial empty document is always a
// same-origin navigation:
// - about:srcdoc is same-origin with the parent.
// - the initial empty document is same-origin with the parent.
{
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_TRUE(handle->IsSameOrigin());
loop.Quit();
}));
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
let iframe = document.createElement('iframe');
iframe.srcdoc = "dummy content";
document.body.appendChild(iframe);
)"));
loop.Run();
}
// Now, navigate cross-origin, and back to about:srcdoc with a brand new
// iframe. The navigation is now considered cross-origin.
// - the previous document is cross-origin with the parent.
// - about:srcdoc is same-origin with the parent.
{
EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"(
let iframe2 = document.createElement('iframe');
iframe2.src = $1;
iframe2.id = 'iframe2';
document.body.appendChild(iframe2);
)",
cross_origin)));
WaitForLoadStop(shell()->web_contents());
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_FALSE(handle->IsSameOrigin());
loop.Quit();
}));
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
document.getElementById("iframe2").srcdoc = "dummy content";
)"));
loop.Run();
}
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
SameOriginFlagOfAboutBlankToAboutBlankNavigation) {
GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
GURL iframe_url(embedded_test_server()->GetURL("b.com", "/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), parent_url));
EXPECT_TRUE(ExecJs(main_frame(), JsReplace(R"(
let iframe = document.createElement('iframe');
iframe.src = $1;
document.body.appendChild(iframe);
)",
iframe_url)));
WaitForLoadStop(shell()->web_contents());
// Test a same-origin about:blank navigation
{
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_TRUE(handle->IsSameOrigin());
loop.Quit();
}));
RenderFrameHostImpl* child_document =
current_frame_host()->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(child_document, R"(location.href = "about:blank";)"));
loop.Run();
}
// Test another same-origin about:blank navigation
{
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_TRUE(handle->IsSameOrigin());
loop.Quit();
}));
RenderFrameHostImpl* child_document =
current_frame_host()->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(child_document, R"(location.href = "about:blank";)"));
loop.Run();
}
// Test a cross-origin about:blank navigation
{
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
EXPECT_FALSE(handle->IsSameOrigin());
loop.Quit();
}));
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
document.querySelector('iframe').src = "about:blank";
)"));
loop.Run();
}
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginOfSandboxedIframe) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
base::RunLoop loop;
DidFinishNavigationCallback callback(
shell()->web_contents(),
base::BindLambdaForTesting([&](NavigationHandle* handle) {
ASSERT_TRUE(handle->HasCommitted());
// TODO(crbug.com/40092527) Take sandbox into account. Same Origin
// should be true
EXPECT_FALSE(handle->IsSameOrigin());
loop.Quit();
}));
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
let iframe = document.createElement('iframe');
iframe.sandbox = "allow-scripts";
iframe.src = "/empty.html";
document.body.appendChild(iframe);
)"));
loop.Run();
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_NewPopupToEmptyUrl) {
GURL opener_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), opener_url));
WebContentsImpl* popup = nullptr;
{
WebContentsAddedObserver popup_observer;
ASSERT_TRUE(ExecJs(shell(), "window.open('', '_blank')"));
popup = static_cast<WebContentsImpl*>(popup_observer.GetWebContents());
}
WaitForLoadStop(popup);
// Verify that we are at the initial empty document.
EXPECT_EQ(1, popup->GetController().GetEntryCount());
EXPECT_TRUE(popup->GetController().GetLastCommittedEntry()->IsInitialEntry());
EXPECT_TRUE(
popup->GetPrimaryFrameTree().root()->is_on_initial_empty_document());
// Verify that the `popup` is at "about:blank", with expected origin, with
// working `document.cookie`, and with working subresource loads.
VerifyResultsOfAboutBlankNavigation(
popup->GetPrimaryMainFrame(),
shell()->web_contents()->GetPrimaryMainFrame());
}
// See the doc comment for the
// URLLoaderFactoryInInitialEmptyDoc_NewPopupToEmptyUrl test case.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_NewPopupToAboutBlank) {
GURL opener_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), opener_url));
WebContentsImpl* popup = nullptr;
{
WebContentsAddedObserver popup_observer;
ASSERT_TRUE(ExecJs(shell(), "window.open('about:blank', '_blank')"));
popup = static_cast<WebContentsImpl*>(popup_observer.GetWebContents());
}
WaitForLoadStop(popup);
// Verify that we are at the synchronously committed about:blank document.
EXPECT_EQ(1, popup->GetController().GetEntryCount());
EXPECT_TRUE(popup->GetController().GetLastCommittedEntry()->IsInitialEntry());
EXPECT_TRUE(
popup->GetPrimaryFrameTree().root()->is_on_initial_empty_document());
// Verify other about:blank things.
VerifyResultsOfAboutBlankNavigation(
popup->GetPrimaryMainFrame(),
shell()->web_contents()->GetPrimaryMainFrame());
}
// The test below verifies that error pages have a functional URLLoaderFactory.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInErrorPage) {
GURL error_url(embedded_test_server()->GetURL("/close-socket"));
EXPECT_FALSE(NavigateToURL(shell(), error_url));
VerifyImageSubresourceLoads(shell()->web_contents()->GetPrimaryMainFrame());
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory.
IN_PROC_BROWSER_TEST_F(
SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_HungNavigationInSubframe) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Add a subframe that will never commit a navigation (i.e. that will be stuck
// on the initial empty document).
const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung");
ASSERT_TRUE(
ExecJs(shell(), JsReplace(R"(ifr = document.createElement('iframe');
ifr.src = $1;
document.body.appendChild(ifr); )",
hung_url)));
// No process swaps are expected before ReadyToCommit (which will never happen
// for a navigation to "/hung"). This test assertion double-checks that the
// test will cover inheriting URLLoaderFactory from the creator/opener/parent
// frame.
RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame();
RenderFrameHost* subframe = ChildFrameAt(main_frame, 0);
EXPECT_EQ(main_frame->GetProcess()->GetDeprecatedID(),
subframe->GetProcess()->GetDeprecatedID());
// Ask the parent to script the same-origin subframe and trigger some HTTP
// subresource loads within the subframe.
//
// This tests the functionality of the URLLoaderFactory that gets used by the
// initial empty document. In this test, the `request_initiator` will be a
// non-opaque origin - it requires that the URLLoaderFactory will have a
// matching `request_initiator_origin_lock` (e.g. inherited from the parent).
VerifyImageSubresourceLoads(shell(), "ifr.contentDocument");
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory.
IN_PROC_BROWSER_TEST_F(
SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_HungNavigationInPopup) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a popup window that will never commit a navigation (i.e. that will be
// stuck on the initial empty document).
const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung");
WebContents* popup = nullptr;
{
WebContentsAddedObserver popup_observer;
ASSERT_TRUE(
ExecJs(shell(), JsReplace("popup = window.open($1)", hung_url)));
popup = popup_observer.GetWebContents();
}
// No process swaps are expected before ReadyToCommit (which will never happen
// for a navigation to "/hung"). This test assertion double-checks that the
// test will cover inheriting URLLoaderFactory from the creator/opener/parent
// frame.
RenderFrameHost* opener_frame =
shell()->web_contents()->GetPrimaryMainFrame();
RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame();
EXPECT_EQ(opener_frame->GetProcess()->GetDeprecatedID(),
popup_frame->GetProcess()->GetDeprecatedID());
// Ask the opener to script the (same-origin) popup window and trigger some
// HTTP subresource loads within the popup.
//
// This tests the functionality of the URLLoaderFactory that gets used by the
// initial empty document. In this test, the `request_initiator` will be a
// non-opaque origin - it requires that the URLLoaderFactory will have a
// matching `request_initiator_origin_lock` (e.g. inherited from the opener).
VerifyImageSubresourceLoads(shell(), "popup.document");
// TODO(crbug.com/40758605): Crash recovery doesn't work when there is
// no opener.
DontTestNetworkServiceCrashes();
// Test again after closing the opener..
shell()->Close();
VerifyImageSubresourceLoads(popup);
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory. The ...WithClearedOpener testcase is a regression test for
// https://crbug.com/1191203.
IN_PROC_BROWSER_TEST_F(
SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_HungNavigationInPopupWithClearedOpener) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a new window that will never commit a navigation (i.e. that will be
// stuck on the initial empty document). Clearing of `popup.opener` tests if
// inheriting of URLLoaderFactory from the opener will work when the opener
// has been cleared in DOM/Javascript.
const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung");
const char kScriptTemplate[] = R"(
popup = window.open($1);
popup.opener = null;
)";
content::WebContents* popup = nullptr;
{
WebContentsAddedObserver popup_observer;
ASSERT_TRUE(ExecJs(shell(), JsReplace(kScriptTemplate, hung_url)));
popup = popup_observer.GetWebContents();
}
// No process swaps are expected before ReadyToCommit (which will never happen
// for a navigation to "/hung"). This test assertion double-checks that the
// test will cover inheriting URLLoaderFactory from the creator/opener/parent
// frame. This differentiates the test from the "noopener" case covered in
// another testcase.
RenderFrameHost* opener_frame =
shell()->web_contents()->GetPrimaryMainFrame();
RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame();
EXPECT_EQ(opener_frame->GetProcess()->GetDeprecatedID(),
popup_frame->GetProcess()->GetDeprecatedID());
// Double-check that the popup didn't commit any navigation and that it has
// an the same origin as the initial opener.
EXPECT_EQ(GURL(), popup->GetPrimaryMainFrame()->GetLastCommittedURL());
EXPECT_NE("null", EvalJs(popup, "window.origin"));
EXPECT_EQ(shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin()
.Serialize(),
EvalJs(popup, "window.origin"));
// Use the parent frame's `popup` reference to script the same-origin popup
// window and trigger some HTTP subresource loads within the popup.
//
// This tests the functionality of the URLLoaderFactory that gets used by the
// initial empty document. In this test, the `request_initiator` will be a
// non-opaque origin - it requires that the URLLoaderFactory will have a
// matching `request_initiator_origin_lock` (e.g. inherited from the opener).
VerifyImageSubresourceLoads(popup);
// TODO(crbug.com/40758605): Crash recovery doesn't work when there is
// no opener.
DontTestNetworkServiceCrashes();
// Test again after closing the opener..
shell()->Close();
VerifyImageSubresourceLoads(popup);
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory.
IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_204NoOpenerPopup) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a new window by following a no-opener link to /nocontent (204).
const GURL no_content_url =
embedded_test_server()->GetURL("a.com", "/nocontent");
const char kScriptTemplate[] = R"(
let anchor = document.createElement('a');
anchor.href = $1;
anchor.rel = 'noopener';
anchor.target = '_blank';
anchor.innerText = 'test link';
document.body.appendChild(anchor);
anchor.click();
)";
content::WebContents* popup = nullptr;
{
WebContentsAddedObserver popup_observer;
ASSERT_TRUE(ExecJs(shell(), JsReplace(kScriptTemplate, no_content_url)));
popup = popup_observer.GetWebContents();
}
WaitForLoadStop(popup);
// Double-check that the `popup` didn't commit any navigation and that it has
// an opaque origin.
EXPECT_EQ(GURL(), popup->GetPrimaryMainFrame()->GetLastCommittedURL());
EXPECT_EQ("null", EvalJs(popup, "window.origin"));
// Process swap is expected because of 'noopener'. This test assertion
// double-checks that in the test it is not possible to inheriting
// URLLoaderFactory from the creator/opener/parent frame (because the popup is
// in another process).
RenderFrameHost* opener_frame =
shell()->web_contents()->GetPrimaryMainFrame();
RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame();
EXPECT_NE(opener_frame->GetProcess()->GetDeprecatedID(),
popup_frame->GetProcess()->GetDeprecatedID());
// Inject Javascript that triggers some subresource loads over HTTP.
//
// To some extent, this simulates an ability of 1) Android WebView (see
// https://crbug.com/1189838) and 2) Chrome Extensions, to inject Javascript
// into an initial empty document (even when no web/renderer content has
// access to the document).
//
// This tests the functionality of the URLLoaderFactory that gets used by the
// initial empty document. In this test, the `request_initiator` will be an
// opaque, unique origin (since nothing has committed yet) and will be
// compatible with `request_initiator_origin_lock` of the URLLoaderFactory.
VerifyImageSubresourceLoads(popup);
}
// The test below verifies that an initial empty document has a functional
// URLLoaderFactory.
IN_PROC_BROWSER_TEST_F(
SubresourceLoadingTest,
URLLoaderFactoryInInitialEmptyDoc_HungNavigationInNewWindow) {
// Open a new shell, starting at the "/hung" URL.
const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung");
Shell* new_shell =
Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
hung_url, nullptr, gfx::Size());
// Wait until the renderer process launches (this will flush the CreateView
// IPC and make sure that ExecJs and EvalJs are able to work).
RenderFrameHost* main_frame =
new_shell->web_contents()->GetPrimaryMainFrame();
{
RenderProcessHostWatcher process_watcher(
main_frame->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY);
process_watcher.Wait();
}
// Double-check that the new shell didn't commit any navigation and that it
// has an opaque origin.
EXPECT_EQ(1, new_shell->web_contents()->GetController().GetEntryCount());
EXPECT_TRUE(new_shell->web_contents()
->GetController()
.GetLastCommittedEntry()
->IsInitialEntry());
EXPECT_EQ(GURL(), main_frame->GetLastCommittedURL());
EXPECT_EQ("null", EvalJs(main_frame, "window.origin"));
// Inject Javascript that triggers some subresource loads over HTTP.
//
// To some extent, this simulates an ability of 1) Android WebView (see
// https://crbug.com/1189838) and 2) Chrome Extensions, to inject Javascript
// into an initial empty document (even when no web/renderer content has
// access to the document).
//
// This tests the functionality of the URLLoaderFactory that gets used by the
// initial empty document. In this test, the `request_initiator` will be an
// opaque, unique origin (since nothing has committed yet) and will be
// compatible with `request_initiator_origin_lock` of the URLLoaderFactory.
VerifyImageSubresourceLoads(main_frame);
}
namespace {
struct Result {
GURL url;
std::optional<url::Origin> origin;
bool committed;
};
class NavigationLogger : public WebContentsObserver {
public:
explicit NavigationLogger(WebContents* contents)
: WebContentsObserver(contents) {}
// WebContentsObserver overrides:
void DidFinishNavigation(NavigationHandle* handle) override {
if (handle->HasCommitted()) {
EXPECT_EQ(handle->GetRenderFrameHost()->GetLastCommittedURL(),
handle->GetURL());
RenderFrameHost* rfh = handle->GetRenderFrameHost();
results_.push_back({.url = rfh->GetLastCommittedURL(),
.origin = rfh->GetLastCommittedOrigin(),
.committed = true});
} else {
results_.push_back({.url = handle->GetURL(), .committed = false});
}
}
const std::vector<Result>& results() const { return results_; }
private:
std::vector<Result> results_;
};
} // namespace
class UndoCommitNavigationBrowserTest : public NavigationBrowserTest {
public:
UndoCommitNavigationBrowserTest() {
std::map<std::string, std::string> parameters = {
{"queueing_level", "none"},
};
// Note that RenderDocument needs to be disabled so that it won't enable
// navigation queueing automatically.
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/{{features::kQueueNavigationsWhileWaitingForCommit,
parameters}},
/*disabled_features=*/{features::kRenderDocument});
}
void SetUpOnMainThread() override {
// These navigation tests require full site isolation since they test races
// with committing a navigation in a speculative RenderFrameHost..
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Site isolation is not enabled!";
}
NavigationBrowserTest::SetUpOnMainThread();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
NavigationBrowserTest::SetUpCommandLine(command_line);
// PerformanceManager maintains its own parallel frame tree and has
// sometimes been confused by things like `UndoCommitNavigation()`.
// Force-enable it for test coverage; otherwise, by default,
// PerformanceManager uses the dummy implementation.
//
// TODO(crbug.com/40187286): Enable this by default in content_shell.
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"PerformanceManagerInstrumentation");
}
private:
base::test::ScopedFeatureList feature_list_;
};
// A helper that invokes `functor` on the next `DidStartNavigation()`.
template <typename F>
void OnNextDidStartNavigation(WebContents* web_contents, F&& functor) {
class Observer : public WebContentsObserver {
public:
using Callback = base::OnceCallback<void(NavigationHandle*)>;
explicit Observer(WebContents* web_contents, Callback callback)
: WebContentsObserver(web_contents), callback_(std::move(callback)) {}
// WebContentsObserver overrides:
void DidStartNavigation(NavigationHandle* handle) override {
std::move(callback_).Run(handle);
delete this;
}
private:
Callback callback_;
};
new Observer(web_contents,
base::BindLambdaForTesting(std::forward<F>(functor)));
}
IN_PROC_BROWSER_TEST_F(UndoCommitNavigationBrowserTest,
PerformanceManagerFrameTreeConsistency) {
// PerformanceManager reports when a remote frame is attached to a local
// parent, and it was previously getting confused by the fact that a
// `blink::RemoteFrame` with matching RemoteFrameTokens was being reported as
// attached twice: once by the initial page loaded in the next statement, and
// the next when the browser needs to send a `UndoCommitNavigation()` to the
// a.com renderer.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* first_subframe_node =
web_contents->GetPrimaryMainFrame()->child_at(0);
RenderProcessHost* const a_com_render_process_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->current_frame_host()
->GetProcess();
NavigationLogger logger(web_contents);
// Start a navigation that will create a speculative RFH in the existing
// render process for a.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("a.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(web_contents,
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node,
infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process.
RenderFrameHostImpl* speculative_render_frame_host =
first_subframe_node->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(a_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and ignore) the next `DidCommitProvisionalLoad()` for a.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
// Update the id attribute to exercise a PerformanceManager-specific code
// path: when the renderer swaps in a `blink::RemoteFrame` to undo the
// `CommitNavigation()`, it will report the iframe attribution data again.
// PerformanceManager should not complain that V8ContextTracker already has
// the iframe attribution data, nor should it update the iframe attribution
// data, to preserve existing behavior (unfortunately, the latter part is not
// really tested in this browser test).
EXPECT_TRUE(ExecJs(web_contents,
"document.querySelector('iframe').id = 'new-name';"));
// Now begin a new navigation to c.com while the previous a.com navigation
// above is paused in the pending commit state.
const GURL final_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url));
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, first_subframe_node->render_manager()
->current_frame_host()
->GetLastCommittedURL());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
// This test always uses UndoCommitNavigation, so navigation corresponding to
// the paused commit should never commit.
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
EXPECT_EQ(infinitely_loading_url, results[0].url);
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin);
EXPECT_EQ(final_url, results[1].url);
}
class ResumeCommitClosureSetWaiter {
public:
explicit ResumeCommitClosureSetWaiter(NavigationHandle* handle) {
// `Helper` "observes" the set via its own destruction, since the navigation
// code setting a resume commit closure will replace the testing one
// installed here.
NavigationRequest::From(handle)->set_resume_commit_closure(
base::DoNothingWithBoundArgs(std::make_unique<Helper>(loop_)));
}
void Wait() { loop_.Run(); }
private:
class Helper {
public:
explicit Helper(base::RunLoop& loop) : loop_(loop) {}
~Helper() { loop_->Quit(); }
private:
raw_ref<base::RunLoop> loop_;
};
base::RunLoop loop_;
};
class NavigationQueueingBrowserTest : public NavigationBrowserTest {
public:
NavigationQueueingBrowserTest() {
feature_list_.InitAndEnableFeatureWithParameters(
features::kQueueNavigationsWhileWaitingForCommit,
{{"queueing_level", "full"}});
}
void SetUpOnMainThread() override {
// These navigation tests require full site isolation since they test races
// with committing a navigation in a speculative RenderFrameHost.
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP();
}
NavigationBrowserTest::SetUpOnMainThread();
}
const base::HistogramTester& histogram_tester() const {
return histogram_tester_;
}
private:
base::test::ScopedFeatureList feature_list_;
base::HistogramTester histogram_tester_;
};
IN_PROC_BROWSER_TEST_F(NavigationQueueingBrowserTest, Regular) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
NavigationLogger logger(shell()->web_contents());
// Start a navigation that will create a speculative RFH.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url));
rfh_observer.Wait();
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* speculative_render_frame_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
// Pause the next `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
// Now begin a new navigation to c.com while the previous b.com navigation
// above is paused in the pending commit state.
{
const GURL next_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
std::optional<ResumeCommitClosureSetWaiter>
resume_commit_closure_set_waiter;
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), next_url));
resume_commit_closure_set_waiter->Wait();
}
// Cancel the c.com navigation that was previously queued and (nearly) ready
// for commit, to make sure one pending commit navigation that causes multiple
// subsequent navigations to queue is correctly counted in the metrics.
const GURL final_url =
embedded_test_server()->GetURL("d.com", "/title1.html");
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), final_url));
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, web_contents->GetLastCommittedURL());
// Both the b.com commit and the d.com commit should record the PendingCommit
// time.
histogram_tester().ExpectTotalCount(
"Navigation.PendingCommit.Duration.Regular", 2);
// The d.com navigation should not have blocked any navigation requests from
// making progress.
histogram_tester().ExpectBucketCount(
"Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.Regular",
false, 1);
// But the b.com navigation blocked c.com and d.com.
histogram_tester().ExpectBucketCount(
"Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.Regular",
true, 1);
if (base::FeatureList::IsEnabled(features::kDeferSpeculativeRFHCreation)) {
// For 2 blocked navigations, 2 total blocks are expected when trying to
// pick a final RenderFrameHost to commit the navigation. The attempt to
// create a RenderFrameHost when starting the navigation will be skipped.
histogram_tester().ExpectBucketCount(
"Navigation.PendingCommit.BlockedCount.Regular", 2, 1);
} else {
// For 2 blocked navigations, 4 total blocks are expected: 2 when trying to
// assign a RenderFrameHost when starting a navigation, and 2 when trying to
// pick a final RenderFrameHost to commit the navigation.
histogram_tester().ExpectBucketCount(
"Navigation.PendingCommit.BlockedCount.Regular", 4, 1);
}
histogram_tester().ExpectBucketCount(
"Navigation.PendingCommit.BlockedCommitCount.Regular", 2, 1);
}
class CommitNavigationRaceBrowserTest
: public NavigationBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
CommitNavigationRaceBrowserTest() {
std::map<std::string, std::string> parameters = {
{"queueing_level", GetParam() ? "full" : "none"},
};
feature_list_.InitAndEnableFeatureWithParameters(
features::kQueueNavigationsWhileWaitingForCommit, parameters);
}
void SetUpOnMainThread() override {
// These navigation tests require full site isolation since they test races
// with committing a navigation in a speculative RenderFrameHost..
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP();
}
NavigationBrowserTest::SetUpOnMainThread();
}
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return info.param ? "NavigationQueueing" : "UndoCommitNavigation";
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test for https://crbug.com/40187807 and https://crbug.com/332746903.
//
// Ensure that racing a navigation commit in a speculative/provisional child
// frame in render process B with a detach IPC from render process A (i.e. the
// child frame's parent is in render process A and has removed the frame owner
// element—e.g. <iframe>—from the DOM) does not result in the detach IPC being
// discarded and never received by render process B.
IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest,
DetachAfterCommitNavigationInSubFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,a)")));
WebContentsImpl* const web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* const first_subframe_node =
web_contents->GetPrimaryMainFrame()->child_at(0);
FrameTreeNode* const second_subframe_node =
web_contents->GetPrimaryMainFrame()->child_at(1);
RenderProcessHost* const b_com_render_process_host =
first_subframe_node->render_manager()->current_frame_host()->GetProcess();
// Start a navigation in the second child frame that will create a speculative
// RFH in the existing render process for b.com. The first child frame is
// already hosted in the render process for b.com: this is to ensure the
// render process remains live even after the second child frame is detached
// later in this test.
GURL b_url = embedded_test_server()->GetURL("b.com", "/title1.html");
SpeculativeRenderFrameHostObserver observer(shell()->web_contents(), b_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(second_subframe_node, b_url));
// Ensure the speculative RFH is in the expected process.
observer.Wait();
RenderFrameHostImpl* speculative_render_frame_host =
second_subframe_node->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and ignore) the next `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
// At this point, the b.com renderer has already committed the RenderFrame,
// but on the browser side, the RenderFrameHost is still speculative.
// Intentionally do not wait for script completion here. This runs an event
// loop that pumps incoming messages, but IPCs from b.com would be processed
// out of order, since the `DidCommitProvisionalLoad()` attempt was previously
// paused above.
ExecuteScriptAsync(
web_contents,
JsReplace("document.querySelectorAll('iframe')[1].remove()"));
// However, since it's not possible to wait for `remove()` to take effect,
// the test must cheat a little and directly call the Mojo IPC that the JS
// above would eventually trigger.
second_subframe_node->render_manager()->current_frame_host()->Detach();
EXPECT_TRUE(WaitForLoadStop(web_contents));
// Validate that render process for b.com has handled the detach message for
// the provisional frame that was committing. Before the fix:
// - without navigation queueing, the render process for b.com still had the
// proxy for the second child frame, because the browser process's request
// to delete it was sent via a broken message pipe. Thus, the frame tree in
// the render process for b.com incorrectly thought there were still two
// child frames.
// - with navigation queueing, the render process for b.com has already
// committed the navigation in the second child frame, so the renderer-side
// proxy has already been destroyed and replaced. However, the browser
// process has not heard about the commit yet and sends a detach to the
// proxy, which the renderer ignores since it no longer exists.
EXPECT_EQ(1, EvalJs(first_subframe_node, "top.length"));
}
IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest,
BeginNewNavigationDuringCommitNavigationInMainFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Prior to implementing the UndoCommitNavigation() workaround, the race
// condition being tested would result in a crash in the b.com renderer. Open
// another b.com window in the same browsing instance to verify that the b.com
// renderer does not unexpectedly crash even if the b.com speculative
// RenderFrameHost is discarded.
ASSERT_TRUE(ExecJs(
shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL(
"b.com", "/title1.html"))));
ASSERT_EQ(2u, Shell::windows().size());
WebContentsImpl* new_web_contents =
static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents());
WaitForLoadStop(new_web_contents);
RenderProcessHost* const b_com_render_process_host =
new_web_contents->GetPrimaryMainFrame()->GetProcess();
NavigationLogger logger(shell()->web_contents());
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process (i.e. the b.com
// process that was created for the navigation in the new window earlier).
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* speculative_render_frame_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the next navigation, since the resume commit closure may be synchronously
// set while handling the `BeginNavigation()` IPC in the browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
}
// Now begin a new navigation to c.com while the previous b.com navigation
// above is paused in the pending commit state.
const GURL final_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, web_contents->GetLastCommittedURL());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
EXPECT_EQ(infinitely_loading_url, results[0].url);
// If navigation queueing is enabled, the first navigation will complete the
// commit as the new navigation gets queued until the first navigation's
// commit finished. If navigation queueing is disabled, the pending commit
// navigation will be cancelled.
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
}
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin);
EXPECT_EQ(final_url, results[1].url);
}
IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest,
BeginNewNavigationDuringCommitNavigationInSubFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b(a)")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* first_subframe_node =
web_contents->GetPrimaryMainFrame()->child_at(0);
RenderProcessHost* const b_com_render_process_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->current_frame_host()
->GetProcess();
NavigationLogger logger(web_contents);
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node,
infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process.
RenderFrameHostImpl* speculative_render_frame_host =
first_subframe_node->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the next navigation, since the resume commit closure may be synchronously
// set while handling the `BeginNavigation()` IPC in the browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
}
// Now begin a new navigation to c.com while the previous b.com navigation
// above is paused in the pending commit state.
const GURL final_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, first_subframe_node->render_manager()
->current_frame_host()
->GetLastCommittedURL());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
// If navigation queueing is enabled, the first navigation will complete the
// commit as the new navigation gets queued until the first navigation's
// commit finished. If navigation queueing is disabled, the pending commit
// navigation will be cancelled.
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
}
EXPECT_EQ(infinitely_loading_url, results[0].url);
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin);
EXPECT_EQ(final_url, results[1].url);
}
// Test the behavior of a navigation that gets suspended in the pending commit
// state followed by another navigation that results in a failed navigation. A
// failed navigation is not a navigation that results in an HTTP error page; it
// is a situation where the network request itself fails, e.g. DNS resolution
// failed, and Chrome commits an error page instead.
IN_PROC_BROWSER_TEST_P(
CommitNavigationRaceBrowserTest,
BeginNewNavigationDuringCommitFailedNavigationInMainFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Prior to implementing the UndoCommitNavigation() workaround, the race
// condition being tested would result in a crash in the b.com renderer. Open
// another b.com window in the same browsing instance to verify that the b.com
// renderer does not unexpectedly crash even if the b.com speculative
// RenderFrameHost is discarded.
ASSERT_TRUE(ExecJs(
shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL(
"b.com", "/title1.html"))));
ASSERT_EQ(2u, Shell::windows().size());
WebContentsImpl* new_web_contents =
static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents());
WaitForLoadStop(new_web_contents);
RenderProcessHost* const b_com_render_process_host =
new_web_contents->GetPrimaryMainFrame()->GetProcess();
NavigationLogger logger(shell()->web_contents());
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process (i.e. the b.com
// process that was created for the navigation in the new window earlier).
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* speculative_render_frame_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the next navigation, since the resume commit closure may be synchronously
// set while handling the `BeginNavigation()` IPC in the browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
}
// Now begin a new navigation to c.com while the previous b.com navigation
// above is paused in the pending commit state. This navigation will fail and
// commit an error page.
const GURL final_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
URLLoaderInterceptor::SetupRequestFailForURL(final_url,
net::ERR_DNS_TIMED_OUT);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
// The top-level page completes loading but is an error page, so
// `WaitForLoadStop()` should return false, since the navigation entry will
// have `PAGE_TYPE_ERROR`.
EXPECT_FALSE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, web_contents->GetLastCommittedURL());
EXPECT_TRUE(web_contents->GetPrimaryMainFrame()->IsErrorDocument());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
EXPECT_EQ(infinitely_loading_url, results[0].url);
// If navigation queueing is enabled, the first navigation will complete the
// commit as the new navigation gets queued until the first navigation's
// commit finished. If navigation queueing is disabled, the pending commit
// navigation will be cancelled.
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
}
EXPECT_TRUE(results[1].committed);
EXPECT_TRUE(results[1].origin->opaque());
EXPECT_EQ(embedded_test_server()
->GetOrigin("c.com")
.GetTupleOrPrecursorTupleIfOpaque(),
results[1].origin->GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(final_url, results[1].url);
}
IN_PROC_BROWSER_TEST_P(
CommitNavigationRaceBrowserTest,
BeginNewNavigationDuringCommitFailedNavigationInSubFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b(a)")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* first_subframe_node =
web_contents->GetPrimaryMainFrame()->child_at(0);
RenderProcessHost* const b_com_render_process_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->current_frame_host()
->GetProcess();
NavigationLogger logger(web_contents);
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(web_contents,
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node,
infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process.
RenderFrameHostImpl* speculative_render_frame_host =
first_subframe_node->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the next navigation, since the resume commit closure may be synchronously
// set while handling the `BeginNavigation()` IPC in the browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
}
// Now begin a new navigation to c.com while the previous b.com navigation
// above is paused in the pending commit state. This navigation will fail and
// commit an error page.
const GURL final_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
URLLoaderInterceptor::SetupRequestFailForURL(final_url,
net::ERR_DNS_TIMED_OUT);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
// The top-level page completes loading. Unlike the main frame variant of this
// test, `WaitForLoadStop()` should return true, since the navigation entry
// will have `PAGE_TYPE_NORMAL` as only a subframe failed to load.
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, first_subframe_node->render_manager()
->current_frame_host()
->GetLastCommittedURL());
EXPECT_TRUE(first_subframe_node->render_manager()
->current_frame_host()
->IsErrorDocument());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
// If navigation queueing is enabled, the first navigation will complete the
// commit as the new navigation gets queued until the first navigation's
// commit finished. If navigation queueing is disabled, the pending commit
// navigation will be cancelled.
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
}
EXPECT_EQ(infinitely_loading_url, results[0].url);
EXPECT_TRUE(results[1].committed);
EXPECT_TRUE(results[1].origin->opaque());
EXPECT_EQ(embedded_test_server()
->GetOrigin("c.com")
.GetTupleOrPrecursorTupleIfOpaque(),
results[1].origin->GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(final_url, results[1].url);
}
// about:blank navigations do not require a URL loader and go through a
// different path to commit the navigation in the renderer.
IN_PROC_BROWSER_TEST_P(
CommitNavigationRaceBrowserTest,
BeginNewNavigationWithNoUrlLoaderDuringCommitNavigationInMainFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Prior to implementing the UndoCommitNavigation() workaround, the race
// condition being tested would result in a crash in the b.com renderer. Open
// another b.com window in the same browsing instance to verify that the b.com
// renderer does not unexpectedly crash even if the b.com speculative
// RenderFrameHost is discarded.
ASSERT_TRUE(ExecJs(
shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL(
"b.com", "/title1.html"))));
ASSERT_EQ(2u, Shell::windows().size());
WebContentsImpl* new_web_contents =
static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents());
EXPECT_TRUE(WaitForLoadStop(new_web_contents));
RenderProcessHost* const b_com_render_process_host =
new_web_contents->GetPrimaryMainFrame()->GetProcess();
NavigationLogger logger(shell()->web_contents());
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process (i.e. the b.com
// process that was created for the navigation in the new window earlier).
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* speculative_render_frame_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the next navigation, since the resume commit closure may be synchronously
// set while handling the `BeginNavigation()` IPC in the browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
}
// Note that this navigation is initiated by the a.com renderer, as the a.com
// renderer is still the current frame host for the main frame.
const GURL final_url("about:blank");
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, web_contents->GetLastCommittedURL());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
EXPECT_EQ(infinitely_loading_url, results[0].url);
// If navigation queueing is enabled, the first navigation will complete the
// commit as the new navigation gets queued until the first navigation's
// commit finished. If navigation queueing is disabled, the pending commit
// navigation will be cancelled.
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
}
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin);
EXPECT_EQ(final_url, results[1].url);
}
IN_PROC_BROWSER_TEST_P(
CommitNavigationRaceBrowserTest,
BeginNewNavigationWithNoUrlLoaderDuringCommitNavigationInSubFrame) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b(a)")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* first_subframe_node =
web_contents->GetPrimaryMainFrame()->child_at(0);
RenderProcessHost* const b_com_render_process_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->current_frame_host()
->GetProcess();
NavigationLogger logger(web_contents);
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL infinitely_loading_url =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
infinitely_loading_url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node,
infinitely_loading_url));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process.
RenderFrameHostImpl* speculative_render_frame_host =
first_subframe_node->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the next navigation, since the resume commit closure may be synchronously
// set while handling the `BeginNavigation()` IPC in the browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
resume_commit_closure_set_waiter.emplace(handle);
});
}
// Note that this navigation is initiated by the a.com renderer, as the a.com
// renderer is still the current frame host for the main frame.
const GURL final_url("about:blank");
ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(final_url, first_subframe_node->render_manager()
->current_frame_host()
->GetLastCommittedURL());
auto results = logger.results();
ASSERT_EQ(2u, results.size());
// If navigation queueing is enabled, the first navigation will complete the
// commit as the new navigation gets queued until the first navigation's
// commit finished. If navigation queueing is disabled, the pending commit
// navigation will be cancelled.
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
}
EXPECT_EQ(infinitely_loading_url, results[0].url);
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin);
EXPECT_EQ(final_url, results[1].url);
}
// Tests when a navigation is pending commit, two new navigations start one
// after another in the same frame.
IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest,
BeginTwoNavigationsDuringCommitNavigation) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Prior to implementing the UndoCommitNavigation() workaround, the race
// condition being tested would result in a crash in the b.com renderer. Open
// another b.com window in the same browsing instance to verify that the b.com
// renderer does not unexpectedly crash even if the b.com speculative
// RenderFrameHost is discarded.
ASSERT_TRUE(ExecJs(
shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL(
"b.com", "/title1.html"))));
ASSERT_EQ(2u, Shell::windows().size());
WebContentsImpl* new_web_contents =
static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents());
WaitForLoadStop(new_web_contents);
RenderProcessHost* const b_com_render_process_host =
new_web_contents->GetPrimaryMainFrame()->GetProcess();
NavigationLogger logger(shell()->web_contents());
// Start a navigation that will create a speculative RFH in the existing
// render process for b.com.
const GURL url_b =
embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html");
SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(),
url_b);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), url_b));
rfh_observer.Wait();
// Ensure the speculative RFH is in the expected process (i.e. the b.com
// process that was created for the navigation in the new window earlier).
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderFrameHostImpl* speculative_render_frame_host =
root->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
commit_pauser.WaitForCommitAndPause();
// Now begin a new navigation to c.com while the previous b.com navigation
// above is paused in the pending commit state.
std::optional<ResumeCommitClosureSetWaiter>
url_c_resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the `url_c` navigation, since the resume commit closure may be
// synchronously set while handling the `BeginNavigation()` IPC in the
// browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
url_c_resume_commit_closure_set_waiter.emplace(handle);
});
}
const GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html");
TestNavigationManager url_c_nav(web_contents, url_c);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_c));
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
ASSERT_TRUE(url_c_nav.WaitForRequestStart());
} else {
url_c_nav.WaitForSpeculativeRenderFrameHostCreation();
}
EXPECT_EQ(url_c, root->navigation_request()->GetURL());
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// The navigation to c.com should be queued.
url_c_nav.ResumeNavigation();
url_c_resume_commit_closure_set_waiter->Wait();
}
// Now begin another navigation to d.com, which will cancel the navigation to
// c.com.
std::optional<ResumeCommitClosureSetWaiter>
url_d_resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// Install a commit closure watched for the `url_d` navigation too.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
url_d_resume_commit_closure_set_waiter.emplace(handle);
});
}
const GURL url_d = embedded_test_server()->GetURL("d.com", "/title1.html");
TestNavigationManager url_d_nav(web_contents, url_d);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_d));
ASSERT_TRUE(url_d_nav.WaitForRequestStart());
EXPECT_EQ(url_d, root->navigation_request()->GetURL());
// The navigation to c.com didn't commit as it was replaced by the d.com
// navigation.
EXPECT_TRUE(url_c_nav.WaitForNavigationFinished());
EXPECT_FALSE(url_c_nav.was_committed());
// Continue the d.com navigation.
url_d_nav.ResumeNavigation();
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// Wait for the `url_d` navigation to be queued, and finish the pending
// commit b.com navigation.
url_d_resume_commit_closure_set_waiter->Wait();
commit_pauser.ResumePausedCommit();
}
// After all the navigations finished, we will end up in d.com.
EXPECT_TRUE(url_d_nav.WaitForNavigationFinished());
EXPECT_EQ(url_d, web_contents->GetLastCommittedURL());
// Check the order of navigations finishing.
auto results = logger.results();
ASSERT_EQ(3u, results.size());
EXPECT_FALSE(results[0].committed);
EXPECT_EQ(std::nullopt, results[0].origin);
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// When navigation queueing is enabled, the pending commit navigation to
// b.com won't get canceled when the c.com navigation starts. Then when the
// d.com navigation starts, the c.com navigation will get canceled and
// finishes first without commmitting (while the b.com navigation stays as
// it is pending commit).
EXPECT_EQ(url_c, results[0].url);
EXPECT_TRUE(results[1].committed);
// After continuing b.com's commit, it finishes and commits succesfully.
EXPECT_EQ(url_b, results[1].url);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[1].origin);
} else {
// When navigation queueing is disabled, the pending commit navigation to
// b.com gets canceled when the c.com navigation starts. Then when the
// d.com navigation starts, the c.com navigation will get canceled too.
EXPECT_EQ(url_b, results[0].url);
EXPECT_FALSE(results[1].committed);
EXPECT_EQ(url_c, results[1].url);
EXPECT_EQ(std::nullopt, results[1].origin);
}
// Finally, the d.com navigation finishes and commits last.
EXPECT_TRUE(results[2].committed);
EXPECT_EQ(url_d, results[2].url);
EXPECT_EQ(embedded_test_server()->GetOrigin("d.com"), results[2].origin);
}
// Verify that a speculative RFH in the pending commit state is still cleaned up
// if the renderer crashes.
IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest,
CrashedInPendingCommit) {
GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html");
GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), url_a));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
SpeculativeRenderFrameHostObserver rfh_observer(web_contents, url_b);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), url_b));
rfh_observer.Wait();
base::WeakPtr<RenderFrameHostImpl> speculative_render_frame_host =
web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host()
->GetWeakPtr();
ASSERT_TRUE(speculative_render_frame_host);
// Wait for the next `DidCommitProvisionalLoad()` and ignore it.
CommitNavigationPauser commit_pauser(speculative_render_frame_host.get());
commit_pauser.WaitForCommitAndPause();
ASSERT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit,
speculative_render_frame_host->lifecycle_state());
// Terminate the renderer process while `speculative_render_frame_host` is in
// `kPendingCommit`.
RenderProcessHostWatcher watcher(
speculative_render_frame_host->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
speculative_render_frame_host->GetProcess()->ShutdownForBadMessage(
RenderProcessHost::CrashReportMode::NO_CRASH_DUMP);
watcher.Wait();
// The speculative RFH should be gone:
ASSERT_FALSE(speculative_render_frame_host);
EXPECT_FALSE(web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host());
// And a new navigation should not hit any DCHECKs in
// `GetFrameHostForNavigation()`.
EXPECT_TRUE(NavigateToURLFromRenderer(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
}
// Tests when a back navigation is pending commit, then another back navigation
// starts.
IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest,
MultipleBackNavigation) {
// This test expects the document is freshly loaded on the back navigation.
DisableBackForwardCacheForTesting(web_contents(),
BackForwardCache::TEST_REQUIRES_NO_CACHING);
// Navigate to a.com, then b.com.
const GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html");
const GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), url_a));
ASSERT_TRUE(NavigateToURL(shell(), url_b));
// Prior to implementing the UndoCommitNavigation() workaround, the race
// condition being tested would result in a crash in the b.com renderer. Open
// another b.com window in the same browsing instance to verify that the b.com
// renderer does not unexpectedly crash even if the b.com speculative
// RenderFrameHost is discarded.
ASSERT_TRUE(ExecJs(shell(), JsReplace("window.open($1)", url_b)));
ASSERT_EQ(2u, Shell::windows().size());
WebContentsImpl* new_web_contents =
static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents());
EXPECT_TRUE(WaitForLoadStop(new_web_contents));
RenderProcessHost* const b_com_render_process_host =
new_web_contents->GetPrimaryMainFrame()->GetProcess();
// Navigate to c.com.
const GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), url_c));
NavigationLogger logger(shell()->web_contents());
// Start a back navigation that will create a speculative RFH in the existing
// render process for b.com.
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
TestNavigationManager first_back_nav(web_contents, url_b);
ASSERT_TRUE(ExecJs(shell(), "history.back()"));
ASSERT_TRUE(first_back_nav.WaitForResponse());
EXPECT_EQ(url_b, root->navigation_request()->GetURL());
// Ensure the speculative RFH is in the expected process (i.e. the b.com
// process that was created for the navigation in the new window earlier).
RenderFrameHostImpl* speculative_render_frame_host =
root->render_manager()->speculative_frame_host();
ASSERT_TRUE(speculative_render_frame_host);
EXPECT_EQ(b_com_render_process_host,
speculative_render_frame_host->GetProcess());
// Pause (and potentially ignore, if navigation queueing is disabled) the next
// `DidCommitProvisionalLoad()` for b.com.
CommitNavigationPauser commit_pauser(speculative_render_frame_host);
first_back_nav.ResumeNavigation();
commit_pauser.WaitForCommitAndPause();
// Now begin a new back navigation while the previous back navigation above is
// paused in the pending commit state.
std::optional<ResumeCommitClosureSetWaiter>
second_back_nav_resume_commit_closure_set_waiter;
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queueing is enabled, the test should verify that a resume
// commit closure is actually set. Install a watcher now, before beginning
// the second back navigation, since the resume commit closure may be
// synchronously set while handling the `BeginNavigation()` IPC in the
// browser.
OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) {
second_back_nav_resume_commit_closure_set_waiter.emplace(handle);
});
}
TestNavigationManager second_back_nav(web_contents, url_a);
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
controller.GoBack();
ASSERT_TRUE(second_back_nav.WaitForRequestStart());
EXPECT_EQ(url_a, root->navigation_request()->GetURL());
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// The second back navigation should be queued.
second_back_nav.ResumeNavigation();
second_back_nav_resume_commit_closure_set_waiter->Wait();
// Continue the first navigation's commit.
commit_pauser.ResumePausedCommit();
}
// After all the navigations finished, we will end up in a.com.
EXPECT_TRUE(second_back_nav.WaitForNavigationFinished());
EXPECT_EQ(url_a, web_contents->GetLastCommittedURL());
// Check the order of navigations finishing.
auto results = logger.results();
ASSERT_EQ(2u, results.size());
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// When navigation queueing is enabled, the pending commit back navigation
// to b.com won't get canceled when the second back navigation starts. After
// continuing the second back navigation, it finishes and commits
// successfully to a.com.
EXPECT_EQ(url_b, results[0].url);
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin);
} else {
// When navigation queueing is disabled, the pending commit back navigation
// to b.com gets canceled when the second back navigation starts. Then the
// second back navigation will successfully commit to a.com.
EXPECT_EQ(url_b, results[0].url);
EXPECT_FALSE(results[0].committed);
}
EXPECT_EQ(url_a, results[1].url);
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin);
}
INSTANTIATE_TEST_SUITE_P(,
CommitNavigationRaceBrowserTest,
::testing::Bool(),
&CommitNavigationRaceBrowserTest::DescribeParams);
// Validate browser-side state when a pending commit RFH sends a bad
// CommitNavigation() IPC. Immediately after the bad message is reported, the
// speculative RFH should remain in the kPendingCommit state, but with no
// pending commit for a cross-document navigation. This somewhat odd state comes
// about because processing the commit navigation ack consumes the
// NavigationRequest early on, before the bad message is reported. Reporting the
// bad message cancels any further processing of the commit navigation ack, but
// does not directly clear any other navigation-related state.
//
// Instead, the pending commit speculative RFH will be asynchronously torn down
// later, when the browser process observes the renderer process going away,
// which then implicitly ends the navigation.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
CommitBadNavigationInPendingCommitRFHCleanup) {
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP();
}
// Populate the main window with something so a subsequent navigation will
// create a speculative RFH.
EXPECT_TRUE(NavigateToURL(
web_contents(), embedded_test_server()->GetURL("b.com", "/title1.html")));
class CommitBadOriginInterceptor : public DidCommitNavigationInterceptor {
public:
using DidCommitNavigationInterceptor::DidCommitNavigationInterceptor;
WebContentsImpl* web_contents() {
return static_cast<WebContentsImpl*>(
DidCommitNavigationInterceptor::web_contents());
}
bool WillProcessDidCommitNavigation(
RenderFrameHost* render_frame_host,
NavigationRequest* navigation_request,
mojom::DidCommitProvisionalLoadParamsPtr* params,
mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
override {
// Mismatch from the expected origin for the renderer process, which
// should trigger a bad message kill.
(*params)->origin = url::Origin::Create(GURL("https://example.com/"));
frame_watcher_.emplace(render_frame_host);
process_watcher_.emplace(
render_frame_host->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([this]() {
// This task should run after the browser has reported a bad
// message, but before the browser process has observed render
// process termination, so the pending commit speculative RFH should
// still be present...
auto* speculative_rfh = web_contents()
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
ASSERT_TRUE(speculative_rfh);
EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit,
speculative_rfh->lifecycle_state());
// But it should not have any pending cross-document navigation
// commits, since the NavigationRequest is consumed from
// `RenderFrameHostImpl::navigation_requests_` as one of the first
// parts of handling the commit navigation ack from the renderer.
EXPECT_FALSE(
speculative_rfh->HasPendingCommitForCrossDocumentNavigation());
validated_speculative_rfh_state_ = true;
}));
return true;
}
void CheckPendingCommitRenderFrameHostIsGone() const {
// Make sure the validations in the posted callback actually ran.
EXPECT_TRUE(validated_speculative_rfh_state_);
EXPECT_TRUE(frame_watcher_->IsDestroyed());
}
void WaitForRenderProcessExit() { process_watcher_->Wait(); }
private:
bool validated_speculative_rfh_state_ = false;
std::optional<RenderFrameHostWrapper> frame_watcher_;
std::optional<RenderProcessHostWatcher> process_watcher_;
};
CommitBadOriginInterceptor interceptor(web_contents());
content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
// The infinitely loading page is load-bearing here: `NavigateToURL()` waits
// for `DidStopLoading()`, which can be triggered by:
// 1. The renderer completing the load and sending `DidStopLoading()` (this is
// typical).
// 2. The renderer process going away (e.g. crashing) and the browser manually
// triggering `DidStopLoading()` (this is unusual).
//
// However, this test is specifically testing case #2. To avoid the potential
// of #1 and #2 racing (and causing the test to flakily fail if #1 wins the
// race), set up the test so that the renderer will never call
// `DidStopLoading()`.
EXPECT_FALSE(NavigateToURL(web_contents(),
embedded_test_server()->GetURL(
"a.com", "/infinitely_loading_image.html")));
// NavigateToURL() should fail; at this point, make sure the speculative RFH
// was in the expected state after the browser reported a bad message.
//
// Furthermore, after the navigation completes, the speculative RFH should
// also be destroyed (implicitly, by the renderer process being terminated for
// a bad message).
interceptor.CheckPendingCommitRenderFrameHostIsGone();
// This should actually be signalled inside `NavigateToURL()`, since it waits
// for the navigation to finish (whether successful or not) before returning.
// Make sure the render process host actually exited: if it didn't, then the
// test will timeout here.
interceptor.WaitForRenderProcessExit();
}
// The following test checks what happens if a WebContentsDelegate navigates
// away in response to the NavigationStateChanged event. Previously
// (https://crbug.com/1210234), this was triggering a crash when creating the
// new NavigationRequest, because it was trying to access the current
// RenderFrameHost's PolicyContainerHost, which had not been set up yet by
// RenderFrameHostImpl::DidNavigate.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, Bug1210234) {
class NavigationWebContentsDelegate : public WebContentsDelegate {
public:
NavigationWebContentsDelegate(const GURL& url_to_intercept,
const GURL& url_to_navigate_to)
: url_to_intercept_(url_to_intercept),
url_to_navigate_to_(url_to_navigate_to) {}
void NavigationStateChanged(WebContents* source,
InvalidateTypes changed_flags) override {
if (!navigated_ && source->GetLastCommittedURL() == url_to_intercept_) {
navigated_ = true;
source->GetController().LoadURL(url_to_navigate_to_, Referrer(),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
std::string());
}
}
private:
bool navigated_ = false;
GURL url_to_intercept_;
GURL url_to_navigate_to_;
};
GURL warmup_url = embedded_test_server()->GetURL("a.com", "/title1.html");
GURL initial_url = embedded_test_server()->GetURL("b.com", "/title1.html");
GURL redirection_url =
embedded_test_server()->GetURL("c.com", "/title1.html");
NavigationWebContentsDelegate delegate(initial_url, redirection_url);
web_contents()->SetDelegate(&delegate);
ASSERT_TRUE(NavigateToURL(shell(), warmup_url));
// Note that since we committed a navigation, the next cross-origin navigation
// will create a speculative RenderFrameHost (when site isolation is enabled).
// Start the navigation to `initial_url` and wait until the web contents
// navigates to `redirection_url`. We cannot use helper functions like
// `NavigateToURLBlockUntilNavigationsComplete` because they wait for
// DidStopLoading and check the LastCommittedURL when they receive it.
// However, without SiteIsolation, an earlier DidStopLoading might be received
// when the WebContents has not yet committed the `redirection_url`.
// Prepare for the navigation.
WaitForLoadStop(web_contents());
TestNavigationObserver navigation_observer(redirection_url);
navigation_observer.WatchExistingWebContents();
shell()->LoadURL(initial_url);
navigation_observer.Wait();
EXPECT_TRUE(IsLastCommittedEntryOfPageType(web_contents(), PAGE_TYPE_NORMAL));
EXPECT_EQ(redirection_url, web_contents()->GetLastCommittedURL());
}
class NavigationBrowserTestCredentiallessIframe : public NavigationBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
NavigationBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kEnableBlinkTestFeatures);
}
};
IN_PROC_BROWSER_TEST_F(NavigationBrowserTestCredentiallessIframe,
CheckCookiesForCredentiallessIframeNavigation) {
GURL main_url =
embedded_test_server()->GetURL("/page_with_credentialless_iframe.html");
GURL iframe_url_1 = embedded_test_server()->GetURL("/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Set a cookie on the main frame
EXPECT_TRUE(ExecJs(main_frame(), "document.cookie = 'name=main;';"));
EXPECT_EQ("name=main", EvalJs(main_frame(), "document.cookie;"));
// The main page has a child iframe with url `iframe_url_1`.
EXPECT_EQ(1U, main_frame()->child_count());
FrameTreeNode* child = main_frame()->child_at(0);
EXPECT_EQ(iframe_url_1, child->current_url());
EXPECT_TRUE(child->Credentialless());
EXPECT_TRUE(child->current_frame_host()->IsCredentialless());
EXPECT_TRUE(ExecJs(child->current_frame_host(), "window.credentialless"));
// Set up devtools client so we can check which cookies were sent and blocked
// with frame navigation
TestDevToolsProtocolClient fenced_frame_devtools_client;
fenced_frame_devtools_client.AttachToFrameTreeHost(
child->current_frame_host());
fenced_frame_devtools_client.SendCommandAsync("Network.enable");
fenced_frame_devtools_client.ClearNotifications();
// Set a cookie on the frame and reload so we can see which cookies are
// included in the network request
EXPECT_TRUE(ExecJs(child->current_frame_host(),
"document.cookie = 'name=credentialless;';"));
EXPECT_EQ("name=credentialless",
EvalJs(child->current_frame_host(), "document.cookie;"));
EXPECT_TRUE(ExecJs(child->current_frame_host(), "location.reload();"));
// Check associated cookies according to devtools
base::Value::Dict params = fenced_frame_devtools_client.WaitForNotification(
"Network.requestWillBeSentExtraInfo", /*allow_existing=*/true);
const base::Value::List* associated_cookies =
params.FindList("associatedCookies");
EXPECT_THAT(
associated_cookies,
testing::Pointee(testing::UnorderedElementsAre(
base::test::IsSupersetOfValue(base::test::ParseJsonDict(R"({
"blockedReasons": [ "AnonymousContext" ],
"cookie" : {
"name": "name",
"value": "main"
}
})")),
testing::AllOf(
base::test::IsSupersetOfValue(base::test::ParseJsonDict(R"({
"blockedReasons": [ ],
"cookie" : {
"name": "name",
"value": "credentialless"
}
})")),
testing::ResultOf(
[](const base::Value& dict) {
return dict.GetDict().FindList("blockedReasons");
},
testing::Pointee(testing::IsEmpty()))))));
fenced_frame_devtools_client.DetachProtocolClient();
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTestCredentiallessIframe,
CredentiallessAttributeIsHonoredByNavigation) {
GURL main_url = embedded_test_server()->GetURL("/page_with_iframe.html");
GURL iframe_url_1 = embedded_test_server()->GetURL("/title1.html");
GURL iframe_url_2 = embedded_test_server()->GetURL("/title2.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// The main page has a child iframe with url `iframe_url_1`.
EXPECT_EQ(1U, main_frame()->child_count());
FrameTreeNode* child = main_frame()->child_at(0);
EXPECT_EQ(iframe_url_1, child->current_url());
EXPECT_FALSE(child->Credentialless());
EXPECT_FALSE(child->current_frame_host()->IsCredentialless());
EXPECT_EQ(false,
EvalJs(child->current_frame_host(), "window.credentialless"));
// Changes to the iframe 'credentialless' attribute are propagated to the
// FrameTreeNode. The RenderFrameHost, however, is updated only on navigation.
EXPECT_TRUE(
ExecJs(main_frame(),
"document.getElementById('test_iframe').credentialless = true;"));
EXPECT_TRUE(child->Credentialless());
EXPECT_FALSE(child->current_frame_host()->IsCredentialless());
EXPECT_EQ(false,
EvalJs(child->current_frame_host(), "window.credentialless"));
// Create a grandchild iframe.
EXPECT_TRUE(ExecJs(
child, JsReplace("let grandchild = document.createElement('iframe');"
"grandchild.src = $1;"
"document.body.appendChild(grandchild);",
iframe_url_2)));
WaitForLoadStop(web_contents());
EXPECT_EQ(1U, child->child_count());
FrameTreeNode* grandchild = child->child_at(0);
// The grandchild FrameTreeNode does not set the 'credentialless'
// attribute. The grandchild RenderFrameHost is not credentialless, since its
// parent RenderFrameHost is not credentialless.
EXPECT_FALSE(grandchild->Credentialless());
EXPECT_FALSE(grandchild->current_frame_host()->IsCredentialless());
EXPECT_EQ(false,
EvalJs(grandchild->current_frame_host(), "window.credentialless"));
// Navigate the child iframe same-document. This does not change anything.
EXPECT_TRUE(ExecJs(main_frame(),
JsReplace("document.getElementById('test_iframe')"
" .contentWindow.location.href = $1;",
iframe_url_1.Resolve("#here").spec())));
WaitForLoadStop(web_contents());
EXPECT_TRUE(child->Credentialless());
EXPECT_FALSE(child->current_frame_host()->IsCredentialless());
EXPECT_EQ(false,
EvalJs(child->current_frame_host(), "window.credentialless"));
// Now navigate the child iframe cross-document.
EXPECT_TRUE(ExecJs(
main_frame(), JsReplace("document.getElementById('test_iframe').src = $1",
iframe_url_2)));
WaitForLoadStop(web_contents());
EXPECT_TRUE(child->Credentialless());
EXPECT_TRUE(child->current_frame_host()->IsCredentialless());
EXPECT_EQ(true, EvalJs(child->current_frame_host(), "window.credentialless"));
// A credentialless document has a storage key with a nonce.
EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value());
base::UnguessableToken credentialless_nonce =
current_frame_host()->GetPage().credentialless_iframes_nonce();
EXPECT_EQ(credentialless_nonce,
child->current_frame_host()->GetStorageKey().nonce().value());
// Create a grandchild iframe.
EXPECT_TRUE(ExecJs(
child, JsReplace("let grandchild = document.createElement('iframe');"
"grandchild.id = 'grandchild_iframe';"
"document.body.appendChild(grandchild);",
iframe_url_1)));
EXPECT_EQ(1U, child->child_count());
grandchild = child->child_at(0);
// The grandchild does not set the 'credentialless' attribute, but the
// grandchild document is credentialless.
EXPECT_FALSE(grandchild->Credentialless());
EXPECT_TRUE(grandchild->current_frame_host()->IsCredentialless());
EXPECT_EQ(true,
EvalJs(grandchild->current_frame_host(), "window.credentialless"));
// The storage key's nonce is the same for all credentialless documents in the
// same page.
EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value());
EXPECT_EQ(credentialless_nonce,
child->current_frame_host()->GetStorageKey().nonce().value());
// Now navigate the grandchild iframe.
EXPECT_TRUE(ExecJs(
child, JsReplace("document.getElementById('grandchild_iframe').src = $1",
iframe_url_2)));
WaitForLoadStop(web_contents());
EXPECT_TRUE(grandchild->current_frame_host()->IsCredentialless());
EXPECT_EQ(true,
EvalJs(grandchild->current_frame_host(), "window.credentialless"));
// The storage key's nonce is still the same.
EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value());
EXPECT_EQ(credentialless_nonce,
child->current_frame_host()->GetStorageKey().nonce().value());
// Remove the 'credentialless' attribute from the iframe. This propagates to
// the FrameTreeNode. The RenderFrameHost, however, is updated only on
// navigation.
EXPECT_TRUE(
ExecJs(main_frame(),
"document.getElementById('test_iframe').credentialless = false;"));
EXPECT_FALSE(child->Credentialless());
EXPECT_TRUE(child->current_frame_host()->IsCredentialless());
EXPECT_EQ(true, EvalJs(child->current_frame_host(), "window.credentialless"));
EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value());
EXPECT_EQ(credentialless_nonce,
child->current_frame_host()->GetStorageKey().nonce().value());
// Create another grandchild iframe. Even if the parent iframe element does
// not have the 'credentialless' attribute anymore, the grandchild document is
// still loaded inside of a credentialless RenderFrameHost, so it will be
// credentialless.
EXPECT_TRUE(ExecJs(
child, JsReplace("let grandchild2 = document.createElement('iframe');"
"document.body.appendChild(grandchild2);",
iframe_url_1)));
EXPECT_EQ(2U, child->child_count());
FrameTreeNode* grandchild2 = child->child_at(1);
EXPECT_FALSE(grandchild2->Credentialless());
EXPECT_TRUE(grandchild2->current_frame_host()->IsCredentialless());
EXPECT_EQ(true,
EvalJs(grandchild2->current_frame_host(), "window.credentialless"));
EXPECT_TRUE(
grandchild2->current_frame_host()->GetStorageKey().nonce().has_value());
EXPECT_EQ(credentialless_nonce,
grandchild2->current_frame_host()->GetStorageKey().nonce().value());
// Navigate the child iframe. Since the iframe element does not set the
// 'credentialless' attribute, the resulting RenderFrameHost will not be
// credentialless.
EXPECT_TRUE(
ExecJs(main_frame(),
JsReplace("document.getElementById('test_iframe').src = $1;",
iframe_url_2)));
WaitForLoadStop(web_contents());
EXPECT_FALSE(child->Credentialless());
EXPECT_FALSE(child->current_frame_host()->IsCredentialless());
EXPECT_EQ(false,
EvalJs(child->current_frame_host(), "window.credentialless"));
EXPECT_FALSE(
child->current_frame_host()->GetStorageKey().nonce().has_value());
// Now navigate the whole page away.
GURL main_url_b = embedded_test_server()->GetURL(
"b.com", "/page_with_credentialless_iframe.html");
GURL iframe_url_b = embedded_test_server()->GetURL("b.com", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url_b));
// The main page has a credentialless child iframe with url `iframe_url_b`.
EXPECT_EQ(1U, main_frame()->child_count());
FrameTreeNode* child_b = main_frame()->child_at(0);
EXPECT_EQ(iframe_url_b, child_b->current_url());
EXPECT_TRUE(child_b->Credentialless());
EXPECT_TRUE(child_b->current_frame_host()->IsCredentialless());
EXPECT_EQ(true,
EvalJs(child_b->current_frame_host(), "window.credentialless"));
EXPECT_TRUE(
child_b->current_frame_host()->GetStorageKey().nonce().has_value());
base::UnguessableToken credentialless_nonce_b =
current_frame_host()->GetPage().credentialless_iframes_nonce();
EXPECT_NE(credentialless_nonce, credentialless_nonce_b);
EXPECT_EQ(credentialless_nonce_b,
child_b->current_frame_host()->GetStorageKey().nonce().value());
}
// Ensures that OpenURLParams::FromNavigationHandle translates navigation params
// correctly when used to initiate a navigation in another WebContents.
IN_PROC_BROWSER_TEST_F(
NavigationBrowserTest,
FromNavigationHandleTranslatesNavigationParamsCorrectly) {
// Test that the params are translated correctly for a redirected navigation.
const GURL kRedirectedURL(
embedded_test_server()->GetURL("/server-redirect?/simple_page.html"));
NavigationController::LoadURLParams load_params(kRedirectedURL);
TestNavigationManager first_tab_manager(web_contents(), kRedirectedURL);
web_contents()->GetController().LoadURLWithParams(load_params);
// Wait for response to allow the navigation to resolve the redirect.
EXPECT_TRUE(first_tab_manager.WaitForResponse());
// Create LoadURLParams from the navigation after redirection.
NavigationController::LoadURLParams load_url_params(
OpenURLParams::FromNavigationHandle(
first_tab_manager.GetNavigationHandle()));
// Check that the FrameTreeNode id is set, but then clear it since we'll be
// navigating another tab with the rest of the params.
EXPECT_EQ(load_url_params.frame_tree_node_id,
main_frame()->frame_tree_node_id());
load_url_params.frame_tree_node_id = content::FrameTreeNodeId();
Shell* second_tab = CreateBrowser();
TestNavigationManager second_tab_manager(second_tab->web_contents(),
load_url_params.url);
second_tab->web_contents()->GetController().LoadURLWithParams(
load_url_params);
EXPECT_TRUE(second_tab_manager.WaitForResponse());
// Ensure params from the navigation in the first tab are translated to the
// navigation in the second tab as expected.
auto* first_tab_handle = first_tab_manager.GetNavigationHandle();
auto* second_tab_handle = second_tab_manager.GetNavigationHandle();
EXPECT_EQ(embedded_test_server()->GetURL("/simple_page.html"),
second_tab_handle->GetURL());
EXPECT_EQ(first_tab_handle->GetReferrer(), second_tab_handle->GetReferrer());
EXPECT_TRUE(
ui::PageTransitionCoreTypeIs(first_tab_handle->GetPageTransition(),
second_tab_handle->GetPageTransition()));
EXPECT_EQ(first_tab_handle->IsRendererInitiated(),
second_tab_handle->IsRendererInitiated());
EXPECT_EQ(first_tab_handle->GetInitiatorOrigin(),
second_tab_handle->GetInitiatorOrigin());
EXPECT_EQ(first_tab_handle->GetSourceSiteInstance(),
second_tab_handle->GetSourceSiteInstance());
EXPECT_EQ(first_tab_handle->HasUserGesture(),
second_tab_handle->HasUserGesture());
EXPECT_EQ(first_tab_handle->WasStartedFromContextMenu(),
second_tab_handle->WasStartedFromContextMenu());
EXPECT_EQ(first_tab_handle->GetHrefTranslate(),
second_tab_handle->GetHrefTranslate());
EXPECT_EQ(first_tab_handle->GetReloadType(),
second_tab_handle->GetReloadType());
EXPECT_EQ(first_tab_handle->GetRedirectChain(),
second_tab_handle->GetRedirectChain());
}
// Regression test for https://crbug.com/1392653. Ensure that loading a URL
// that doesn't go through the network stack but does assign a site for its
// SiteInstance in an unassigned SiteInstance does not fail. An example of
// such a URL is about:srcdoc. This ensures that the SiteInstance's site is set
// even on the WillCommitWithoutUrlLoader() path in NavigationRequest.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
AboutSrcdocInjectedOnAboutBlankPage) {
// Start on an about:blank page, which should stay in an unassigned
// SiteInstance.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
SiteInstanceImpl* site_instance = current_frame_host()->GetSiteInstance();
EXPECT_FALSE(site_instance->HasSite());
// The process should be considered unused at this point.
EXPECT_TRUE(site_instance->GetProcess()->IsUnused());
// Inject a srcdoc iframe into the blank document. This shouldn't really be
// possible on the open web, since an about:blank page with an unassigned
// SiteInstance shouldn't be scriptable by other pages, but it could still
// happen in automation scenarios or through DevTools.
TestNavigationObserver navigation_observer(web_contents());
EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"(
let frame = document.createElement('iframe');
frame.srcdoc = 'test';
document.body.appendChild(frame);
)")));
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ("about:srcdoc", navigation_observer.last_navigation_url());
// The srcdoc child should stay in its about:blank parent SiteInstance.
EXPECT_EQ(1U, main_frame()->child_count());
FrameTreeNode* child = main_frame()->child_at(0);
EXPECT_EQ(child->current_frame_host()->GetSiteInstance(), site_instance);
// Committing an about:srcdoc navigation currently forces the SiteInstance's
// site to be set. Prior to fixing https://crbug.com/1392653, this happened
// after the actual commit was processed at DidNavigate() time, which is a
// path that is no longer supported, and hence this triggered a NOTREACHED().
// Now, the site should be set before we send the CommitNavigation IPC.
EXPECT_TRUE(site_instance->HasSite());
if (AreStrictSiteInstancesEnabled()) {
// When we get into this situation with strict site isolation, the site URL
// currently used is "about:". This may be changed in the future (e.g., to
// an opaque ID).
EXPECT_EQ("about:", site_instance->GetSiteInfo().site_url());
} else {
EXPECT_TRUE(site_instance->IsDefaultSiteInstance());
EXPECT_EQ(SiteInstanceImpl::GetDefaultSiteURL(),
site_instance->GetSiteInfo().site_url());
}
// Ensure that the process was marked as used as part of setting the site.
EXPECT_FALSE(site_instance->GetProcess()->IsUnused());
}
class NavigationBrowserTestWarnSandboxIneffective
: public NavigationBrowserTest {
public:
static constexpr char kSandboxEscapeWarningMessage[] =
"An iframe which has both allow-scripts and allow-same-origin for its "
"sandbox attribute can escape its sandboxing.";
};
IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective,
WarnEscapableSandboxSameOrigin) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(kSandboxEscapeWarningMessage);
// Create same-origin iframe.
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
const iframe = document.createElement("iframe");
iframe.src = location.href; // Same-origin iframe.
iframe.sandbox = "allow-same-origin allow-scripts";
document.body.appendChild(iframe);
)"));
ASSERT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective,
WarnEscapableSandboxCrossOrigin) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(kSandboxEscapeWarningMessage);
// Create cross-origin iframe.
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
const iframe = document.createElement("iframe");
// Cross-origin iframe:
iframe.src = location.href.replace("a.com", "b.com");
iframe.sandbox = "allow-same-origin allow-scripts";
document.body.appendChild(iframe);
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(console_observer.messages().size(), 0u);
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective,
WarnEscapableSandboxSameOriginGrandChild) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetPattern(kSandboxEscapeWarningMessage);
// Create a same-origin doubly nested sandboxed iframe.
EXPECT_TRUE(ExecJs(current_frame_host(), R"(
const child = document.createElement("iframe");
document.body.appendChild(child);
const grand_child = child.contentDocument.createElement("iframe");
grand_child.src = location.href;
grand_child.sandbox = "allow-same-origin allow-scripts";
child.contentDocument.body.appendChild(grand_child);
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(console_observer.messages().size(), 0u);
}
// We may have an unload handler in the main frame or a subframe or nowhere.
enum class UnloadFrameType {
kMainFrame,
kSubFrame,
kNone,
};
static std::string ToString(UnloadFrameType v) {
switch (v) {
case UnloadFrameType::kMainFrame:
return "MainFrame";
case UnloadFrameType::kSubFrame:
return "SubFrame";
case UnloadFrameType::kNone:
return "None";
}
}
// We may navigate the main frame, the subframe that may have an unload handler
// or another subframe that will never have an unload handler.
enum class NavigateFrameType {
kMainFrame,
kSubFrame,
kOther,
};
static std::string ToString(NavigateFrameType v) {
switch (v) {
case NavigateFrameType::kMainFrame:
return "MainFrame";
case NavigateFrameType::kSubFrame:
return "SubFrame";
case NavigateFrameType::kOther:
return "Other";
}
}
void AddUnloadHandler(RenderFrameHostImpl* rfh) {
ASSERT_TRUE(ExecJs(rfh, "addEventListener('unload', () => {})"));
ASSERT_TRUE(rfh->GetSuddenTerminationDisablerState(
blink::mojom::SuddenTerminationDisablerType::kUnloadHandler));
}
class NavigationSuddenTerminationDisablerTypeBrowserTest
: public NavigationBrowserTest {
public:
NavigationSuddenTerminationDisablerTypeBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/{},
/*disabled_features=*/{network::features::kDeprecateUnload});
}
private:
base::test::ScopedFeatureList feature_list_;
};
class NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest
: public NavigationSuddenTerminationDisablerTypeBrowserTest,
public ::testing::WithParamInterface<
std::tuple<UnloadFrameType, NavigateFrameType>> {
public:
static std::string DescribeParams(
const ::testing::TestParamInfo<ParamType>& info) {
return "Unload" + ToString(std::get<0>(info.param)) + "Navigate" +
ToString(std::get<1>(info.param));
}
protected:
UnloadFrameType GetUnloadFrameType() { return std::get<0>(GetParam()); }
NavigateFrameType GetNavigateFrameType() { return std::get<1>(GetParam()); }
void MaybeAddUnloadHandler() {
RenderFrameHostImpl* rfh = nullptr;
switch (GetUnloadFrameType()) {
case UnloadFrameType::kMainFrame:
rfh = current_frame_host();
break;
case UnloadFrameType::kSubFrame:
rfh = DescendantRenderFrameHostImplAt(current_frame_host(), {0});
break;
case UnloadFrameType::kNone:
break;
}
if (rfh) {
AddUnloadHandler(rfh);
}
}
RenderFrameHostImpl* GetFrameToNavigate() {
switch (GetNavigateFrameType()) {
case NavigateFrameType::kMainFrame:
return current_frame_host();
case NavigateFrameType::kSubFrame:
return DescendantRenderFrameHostImplAt(current_frame_host(), {0});
case NavigateFrameType::kOther:
return DescendantRenderFrameHostImplAt(current_frame_host(), {1});
}
}
bool NavigatedFrameHasUnload() {
switch (GetNavigateFrameType()) {
case NavigateFrameType::kOther:
// It never has unload.
return false;
case NavigateFrameType::kMainFrame:
// Navigating the main will have unload if there is any unload.
return GetUnloadFrameType() != UnloadFrameType::kNone;
case NavigateFrameType::kSubFrame:
// Navigating the subframe will have unload if the subframe has unload.
return GetUnloadFrameType() == UnloadFrameType::kSubFrame;
}
}
};
INSTANTIATE_TEST_SUITE_P(
All,
NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest,
::testing::Combine(::testing::Values(UnloadFrameType::kMainFrame,
UnloadFrameType::kSubFrame,
UnloadFrameType::kNone),
::testing::Values(NavigateFrameType::kMainFrame,
NavigateFrameType::kSubFrame,
NavigateFrameType::kOther)),
&NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest::
DescribeParams);
// Set up a page with 2 subframes. The main frame or one of the subframes may
// have an unload handler. Then navigate one of the frames and verify that we
// correctly record which type of frame navigates combined with whether it
// involved an unload handler.
IN_PROC_BROWSER_TEST_P(
NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest,
RecordUma) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a,a)")));
current_frame_host()->DisableUnloadTimerForTesting();
// Set up the unload handler if needed.
MaybeAddUnloadHandler();
// Navigate the relevant frame and capture histograms.
base::HistogramTester histograms;
ASSERT_TRUE(NavigateFrameToURL(GetFrameToNavigate()->frame_tree_node(),
GURL("about:blank")));
// Check that we got the expected histogram value.
uint32_t expected_histogram_value = 0;
if (GetNavigateFrameType() == NavigateFrameType::kMainFrame) {
expected_histogram_value |= RenderFrameHostImpl::
NavigationSuddenTerminationDisablerType::kMainFrame;
}
if (NavigatedFrameHasUnload()) {
expected_histogram_value |=
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload;
}
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.AllOrigins",
expected_histogram_value, 1);
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.SameOrigin",
expected_histogram_value, 1);
}
// Test that "SameOrigin" only considers frames that have an unbroken path of
// same-origin frames from the frame that navigates.
IN_PROC_BROWSER_TEST_F(
NavigationSuddenTerminationDisablerTypeBrowserTest,
NavigationSuddenTerminationDisablerTypeRecordUmaSameOrigin) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(a))")));
// Set up the unload handler in the a.com subframe.
AddUnloadHandler(
DescendantRenderFrameHostImplAt(current_frame_host(), {0, 0}));
// Navigate the main frame and capture histograms.
base::HistogramTester histograms;
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.AllOrigins",
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload,
1);
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.SameOrigin",
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame,
1);
}
// Test that we record when the navigation involves restoring from BFCache.
// This is tested because the code path for a navigation involving activation
// is different from one involving a pageload.
IN_PROC_BROWSER_TEST_F(
NavigationSuddenTerminationDisablerTypeBrowserTest,
NavigationSuddenTerminationDisablerTypeRecordUmaActivation) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
// Set up the unload handler in the b.com page.
AddUnloadHandler(current_frame_host());
// Navigate the main frame and capture histograms.
base::HistogramTester histograms;
ASSERT_TRUE(HistoryGoBack(web_contents()));
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.AllOrigins",
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload,
1);
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.SameOrigin",
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload,
1);
}
// Ensure that the first navigation of a subframe away from the initial empty
// document is recorded correctly. This does not test all possibilities of
// histogram value, just that the scenario is counted under the correct
// histogram.
IN_PROC_BROWSER_TEST_F(
NavigationSuddenTerminationDisablerTypeBrowserTest,
NavigationSuddenTerminationDisablerTypeRecordUmaInitialEmptyDocument) {
GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), url));
// Create a subframe with an unload handler.
ASSERT_TRUE(ExecJs(web_contents(), R"(
var i = document.createElement("iframe");
document.body.appendChild(i);
)"));
AddUnloadHandler(DescendantRenderFrameHostImplAt(current_frame_host(), {0}));
// Navigate the subframe and capture histograms.
base::HistogramTester histograms;
uint32_t expected_histogram_value =
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::
kInitialEmptyDocument |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kNotHttp;
ASSERT_TRUE(NavigateToURLFromRenderer(
DescendantRenderFrameHostImplAt(current_frame_host(), {0}), url));
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.AllOrigins",
expected_histogram_value, 1);
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.SameOrigin",
expected_histogram_value, 1);
}
// Ensure that navigations from non-HTTP(S) pages are recorded correctly.
IN_PROC_BROWSER_TEST_F(
NavigationSuddenTerminationDisablerTypeBrowserTest,
NavigationSuddenTerminationDisablerTypeRecordUmaNotHttp) {
GURL blank_url("about:blank");
GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), blank_url));
AddUnloadHandler(current_frame_host());
// Navigate the subframe and capture histograms.
base::HistogramTester histograms;
uint32_t expected_histogram_value =
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload |
RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kNotHttp;
ASSERT_TRUE(NavigateToURL(web_contents(), url));
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.AllOrigins",
expected_histogram_value, 1);
histograms.ExpectUniqueSample(
"Navigation.SuddenTerminationDisabler.SameOrigin",
expected_histogram_value, 1);
}
// This is a regression test against https://crbug.com/1145717 - navigating to
// invalid/weird URLs (e.g. `about:mumble` or `about://mumble`) shouldn't crash.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutMumble) {
// First navigate to an arbitrary http site to lock the renderer process.
GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), http_url));
// Verify that browser-initiated navigation to `about:mumble` doesn't crash.
//
// The renderer will try to commit the original "about:mumble" URL, which will
// still show up via `window.location.href`, but the URL passed back in the
// DidCommit IPC will be rewritten to `about:blank#blocked` due to
// `RenderFrameImpl`'s `IsValidCommitUrl` and therefore this is what we expect
// in `GetLastCommittedURL`.
ASSERT_FALSE(NavigateToURL(shell(), GURL("about:mumble")));
EXPECT_EQ(EvalJs(shell(), "window.location.href"), "about:mumble");
EXPECT_EQ(
shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(),
GURL("about:blank#blocked"));
// Verify that browser-initiated navigation to `about://mumble` doesn't crash.
ASSERT_FALSE(NavigateToURL(shell(), GURL("about://mumble")));
EXPECT_EQ(EvalJs(shell(), "window.location.href"), "about://mumble");
EXPECT_EQ(
shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(),
GURL("about:blank#blocked"));
}
// Ensure that the browser process doesn't see a javascript: URL when opening a
// new window to a javascript: URL. These URLs are typically handled on the
// renderer side, and the renderer should not send the javascript: URL to the
// browser in a navigation request. Previously, this was not correctly handled
// for initial navigations to javascript: URLs. See https://crbug.com/1357515.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FilterURL_JavascriptURLs) {
GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), http_url));
{
SCOPED_TRACE(testing::Message() << "Testing opener case.");
base::HistogramTester histograms;
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(
ExecJs(shell(), "window.open('javascript:window.foo=\"bar\"');"));
WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href"));
// No commit message is sent to the browser in this case, so the last
// committed URL is still empty.
EXPECT_EQ(GURL(), popup_contents->GetLastCommittedURL());
histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL",
0);
// The javascript: URL should have run.
EXPECT_EQ("bar", EvalJs(popup_contents, "window.foo"));
}
{
SCOPED_TRACE(testing::Message() << "Testing noopener case.");
base::HistogramTester histograms;
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(
shell(),
"window.open('javascript:window.foo=\"bar\"', '', 'noopener');"));
WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href"));
EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL());
histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL",
0);
// The Javascript URL should not have run in the noopener case, because the
// origin should not be inherited according to spec. See:
// https://html.spec.whatwg.org/multipage/document-sequences.html#navigable-target-names%3Acreating-a-new-top-level-traversable
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context
// TODO(crbug.com/40236679): Also prevent the origin from being
// inherited.
EXPECT_EQ(nullptr, EvalJs(popup_contents, "window.foo"));
}
}
// Ensure that opening popups to empty URLs does not fail FilterURL. The
// renderer process treats empty URLs as about:blank, but the browser process
// does not consider them valid and may treat them as attempts to go to the NTP
// in some cases. As a result, the renderer should map empty URLs to about:blank
// before making navigation requests. See https://crbug.com/1357515.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FilterURL_EmptyURL) {
GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), http_url));
{
SCOPED_TRACE(testing::Message() << "Testing opener case.");
base::HistogramTester histograms;
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(shell(), "window.open('');"));
WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href"));
EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL());
histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL",
0);
}
{
SCOPED_TRACE(testing::Message() << "Testing noopener case.");
base::HistogramTester histograms;
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(shell(), "window.open('', '', 'noopener');"));
WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href"));
EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL());
histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL",
0);
}
}
// Check that an about:blank popup opened from a WebUI page is not allowed to
// execute Javascript URLs. chrome:// pages don't allow executing javascript:
// URLs, so an about:blank popup opened by one should not be allowed to either,
// despite its URL not having the chrome: scheme. See
// https://crbug.com/1471305.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
JavascriptURLBlockedInAboutBlankWebUiPopup) {
GURL webui_url = GetWebUIURL(kChromeUIGpuHost);
ASSERT_TRUE(NavigateToURL(shell(), webui_url));
// Open an about:blank popup which should inherit the WebUI origin, and set
// some state in window.foo.
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "popup");
EXPECT_TRUE(new_shell);
EXPECT_TRUE(ExecJs(new_shell, "window.foo = 123;"));
EXPECT_EQ(123, EvalJs(new_shell, "window.foo"));
// Try to execute a Javascript URL that modifies window.foo in the popup via
// a browser-initiated navigation. This should be blocked, and the value of
// window.foo should stay unchanged.
new_shell->LoadURL(GURL("javascript:window.foo=456"));
EXPECT_EQ(123, EvalJs(new_shell, "window.foo"));
}
// Same test as above, but with a sandboxed about:blank WebUI popup, which
// should still not be allowed to execute Javascript URLs.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
JavascriptURLBlockedInSandboxedWebUiPopup) {
GURL webui_url = GetWebUIURL(kChromeUIGpuHost);
ASSERT_TRUE(NavigateToURL(shell(), webui_url));
// Add a sandboxed about:blank iframe.
{
std::string script =
"var frame = document.createElement('iframe');\n"
"frame.sandbox = 'allow-scripts allow-popups';\n"
"document.body.appendChild(frame);\n";
EXPECT_TRUE(ExecJs(shell(), script));
}
// Open an about:blank popup from that sandboxed iframe, which should have an
// opaque origin with the WebUI origin as the precursor. Set some state in
// window.foo in that popup.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
Shell* new_shell =
OpenPopup(root->child_at(0), GURL(url::kAboutBlankURL), "popup");
EXPECT_TRUE(new_shell);
EXPECT_TRUE(ExecJs(new_shell, "window.foo = 123;"));
EXPECT_EQ(123, EvalJs(new_shell, "window.foo"));
// Try to execute a Javascript URL that modifies window.foo in the popup via
// a browser-initiated navigation. This should be blocked, and the value of
// window.foo should stay unchanged.
new_shell->LoadURL(GURL("javascript:window.foo=456"));
EXPECT_EQ(123, EvalJs(new_shell, "window.foo"));
}
// Test navigation with site instances whose storage partitions are fixed.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FixedStoragePartition) {
auto* browser_context = shell()->web_contents()->GetBrowserContext();
auto storage_partition_config = StoragePartitionConfig::Create(
browser_context, "NavigationBrowserTest", "FixedStoragePartition", true);
auto url = embedded_test_server()->GetURL("/");
auto* shell = Shell::CreateNewWindow(
browser_context, url,
SiteInstanceImpl::CreateForFixedStoragePartition(
browser_context, url, storage_partition_config),
gfx::Size());
auto GetSiteInstance = [](Shell* shell) {
return static_cast<SiteInstanceImpl*>(
shell->web_contents()->GetSiteInstance());
};
EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition());
// Check navigation.
ASSERT_TRUE(
NavigateToURL(shell, embedded_test_server()->GetURL("/title1.html")));
EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition());
// Check opening a window. The new window should stay in the same
// BrowsingInstance and StoragePartition.
{
ShellAddedObserver observer;
auto destination = embedded_test_server()->GetURL("a.com", "/title1.html");
EXPECT_TRUE(ExecJs(shell, JsReplace("window.open($1)", destination),
EXECUTE_SCRIPT_NO_USER_GESTURE));
auto* popup = observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
EXPECT_EQ(popup->web_contents()->GetLastCommittedURL(), destination);
EXPECT_EQ(GetSiteInstance(popup)->GetBrowsingInstanceId(),
GetSiteInstance(shell)->GetBrowsingInstanceId());
EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition());
}
// Check opening a window with about:blank at the beginning, and then navigate
// it. It should stay in the same BrowsingInstance and StoragePartition.
{
ShellAddedObserver observer;
EXPECT_TRUE(ExecJs(shell, "newWindow = window.open()",
EXECUTE_SCRIPT_NO_USER_GESTURE));
auto* popup = observer.GetShell();
EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition());
auto destination = embedded_test_server()->GetURL("a.com", "/title1.html");
EXPECT_TRUE(ExecJs(shell,
JsReplace("newWindow.location.href = $1", destination),
EXECUTE_SCRIPT_NO_USER_GESTURE));
EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
EXPECT_EQ(popup->web_contents()->GetLastCommittedURL(), destination);
EXPECT_EQ(GetSiteInstance(popup)->GetBrowsingInstanceId(),
GetSiteInstance(shell)->GetBrowsingInstanceId());
EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition());
}
// Check navigation again.
ASSERT_TRUE(
NavigateToURL(shell, embedded_test_server()->GetURL("/title2.html")));
EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition());
// Check navigation that triggers a BrowsingInstance swap, and the storage
// partition config should also be preserved.
auto browsing_instance_id = GetSiteInstance(shell)->GetBrowsingInstanceId();
ASSERT_TRUE(NavigateToURL(
shell, embedded_test_server()->GetURL("c.com", "/title2.html")));
EXPECT_NE(GetSiteInstance(shell)->GetBrowsingInstanceId(),
browsing_instance_id);
EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(),
storage_partition_config);
EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition());
}
// Exercises the restored session history traversal code path which uses
// RESTORE navigation types, rather than HISTORY_{SAME|DIFFERENT}_DOCUMENT,
// which code might erroneously expect. See https://crbug.com/40068335.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
TraversingToRestoredEntryUsesRestoreType) {
ASSERT_TRUE(
web_contents()->GetController().GetActiveEntry()->IsInitialEntry());
const GURL url1(embedded_test_server()->GetURL("/title1.html"));
const GURL url2(embedded_test_server()->GetURL("/title2.html"));
const GURL url3(embedded_test_server()->GetURL("/title2.html#samedoc"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(NavigateToURL(shell(), url2));
EXPECT_TRUE(NavigateToURL(shell(), url3));
// Clone the tab and load the page.
std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
NavigationController& new_controller = new_tab_impl->GetController();
{
TestNavigationObserver clone_observer(new_tab.get());
new_controller.LoadIfNecessary();
clone_observer.Wait();
}
// Back to url2 which is a same document navigation but uses RESTORE.
{
NavigationHandleCommitObserver observer(new_tab.get(), url2);
ASSERT_TRUE(HistoryGoBack(new_tab_impl));
EXPECT_EQ(observer.navigation_type(),
blink::mojom::NavigationType::RESTORE);
}
// Back to url1 which is a cross document navigation but uses RESTORE.
{
NavigationHandleCommitObserver observer(new_tab.get(), url1);
ASSERT_TRUE(HistoryGoBack(new_tab_impl));
EXPECT_EQ(observer.navigation_type(),
blink::mojom::NavigationType::RESTORE);
}
}
class NavigationBrowserTestDeprecateUnloadOptOut
: public NavigationBrowserTest,
public ::testing::WithParamInterface<bool> {
void SetUpCommandLine(base::CommandLine* command_line) override {
NavigationBrowserTest::SetUpCommandLine(command_line);
if (IsOptOutEnabled()) {
scoped_feature_list_.InitWithFeatures(
{network::features::kDeprecateUnload,
blink::features::kDeprecateUnloadOptOut},
{});
} else {
scoped_feature_list_.InitWithFeatures(
{network::features::kDeprecateUnload},
{blink::features::kDeprecateUnloadOptOut});
}
}
protected:
bool IsOptOutEnabled() const { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
NavigationBrowserTestDeprecateUnloadOptOut,
::testing::Bool());
// Test that enabled/disabled kDeprecateUnloadOptOut has the desired effect.
IN_PROC_BROWSER_TEST_P(NavigationBrowserTestDeprecateUnloadOptOut,
DeprecateUnloadOptOutFlagRespected) {
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
// Unload will not run on Android if the page is cacheable.
web_contents()->GetController().GetBackForwardCache().DisableForTesting(
BackForwardCacheImpl::TEST_USES_UNLOAD_EVENT);
// Navigate to a page and install an unload handler with a side-effect.
ASSERT_TRUE(NavigateToURL(web_contents(), url_1));
ASSERT_TRUE(ExecJs(web_contents(), R"(
localStorage.setItem("unload", "not_dispatched");
addEventListener("unload", () => {
localStorage.setItem("unload", "dispatched");
})
)"));
// Navigate to a same-site page (to ensure that the unload handler's
// side-effect is reliably visible).
ASSERT_TRUE(NavigateToURL(web_contents(), url_2));
// Check for the side-effect.
ASSERT_EQ(EvalJs(web_contents(), "localStorage.getItem('unload')"),
IsOptOutEnabled() ? "dispatched" : "not_dispatched");
}
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FCPMetrics) {
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_1));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
NavigationController::LoadURLParams params(url_2);
params.transition_type = ui::PageTransitionFromInt(
ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
web_contents()->GetController().LoadURLWithParams(params);
WaitForHistogramRecordedInChildProcess(
"Navigation.FCPFrameSubmittedBeforeSurfaceEmbed");
}
// Tests that if the main frame has focus before a same-site navigation, it's
// kept after navigation.
// Regression test for crbug.com/360705823.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
FocusPreservedOnNavigation_MainFrame) {
GURL url_1(embedded_test_server()->GetURL("/page_with_iframe.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_1));
// Set focus on a button in the main frame.
ASSERT_TRUE(ExecJs(current_frame_host(), R"(
let button = document.createElement('button');
document.body.appendChild(button);
button.focus();
)"));
// The main document should have focus.
ASSERT_EQ(true, EvalJs(current_frame_host(), "document.hasFocus();"));
// After navigation, the main document should still have focus.
ASSERT_TRUE(NavigateToURL(shell(), url_2));
ASSERT_EQ(true, EvalJs(current_frame_host(), "document.hasFocus();"));
}
// Same as the above test, but the focus is on the iframe.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
FocusPreservedOnNavigation_Subframe) {
GURL url_1(embedded_test_server()->GetURL("/page_with_iframe.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_1));
// Set focus on a button in the iframe.
FrameTreeNode* child_ftn = current_frame_host()->child_at(0);
ASSERT_TRUE(ExecJs(child_ftn, R"(
let button = document.createElement('button');
document.body.appendChild(button);
button.focus();
)"));
// The iframe document should have focus.
ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();"));
// After navigation, the child document should still have focus.
ASSERT_TRUE(NavigateFrameToURL(child_ftn, url_2));
ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();"));
}
// When the navigation is cross-site, focus is not preserved.
IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
FocusNotPreservedOnNavigation_SubframeCrossSite) {
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "Test needs local -> remote swap";
}
GURL url_1(embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
ASSERT_TRUE(NavigateToURL(shell(), url_1));
// Set focus on a button in the iframe.
FrameTreeNode* child_ftn = current_frame_host()->child_at(0);
ASSERT_TRUE(ExecJs(child_ftn, R"(
let button = document.createElement('button');
document.body.appendChild(button);
button.focus();
)"));
// The iframe document should have focus.
ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();"));
// After navigation, the child document should no longer have focus.
ASSERT_TRUE(NavigateFrameToURL(child_ftn, url_2));
ASSERT_EQ(false, EvalJs(child_ftn, "document.hasFocus();"));
}
class NavigationWithPageSwapBrowserTest : public NavigationBrowserTest {
public:
NavigationWithPageSwapBrowserTest() {
feature_list_.InitAndEnableFeature(blink::features::kPageSwapEvent);
}
bool NavigateBack(WebContentsImpl* contents) {
auto result = EvalJs(contents, JsReplace(
R"(
(async () => {
let pageswapfired = new Promise((resolve) => {
onpageswap = (e) => {
activation = e.activation;
resolve(activation);
};
});
history.back();
let result = await pageswapfired;
return result != null;
})();
)"));
return result.ExtractBool();
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(NavigationWithPageSwapBrowserTest,
PageSwapForInitialEntry) {
ASSERT_TRUE(
web_contents()->GetController().GetActiveEntry()->IsInitialEntry());
// TODO(khushalsagar): Assert that pageswap is fired without activation. The
// test script is hitting an issue for the initial Document.
ASSERT_TRUE(NavigateToURL(web_contents(),
embedded_test_server()->GetURL("/title1.html")));
}
IN_PROC_BROWSER_TEST_F(NavigationWithPageSwapBrowserTest,
PageSwapWhenTraversingToRestoredEntry) {
ASSERT_TRUE(
web_contents()->GetController().GetActiveEntry()->IsInitialEntry());
const GURL url1(embedded_test_server()->GetURL("/title1.html"));
const GURL url2(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(NavigateToURL(shell(), url2));
// Clone the tab and load the page.
std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
NavigationController& new_controller = new_tab_impl->GetController();
{
TestNavigationObserver clone_observer(new_tab.get());
new_controller.LoadIfNecessary();
clone_observer.Wait();
}
ASSERT_TRUE(NavigateBack(new_tab_impl));
}
class NavigationBrowserTestPaintHoldingSubframe
: public NavigationBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
NavigationBrowserTestPaintHoldingSubframe() {
// Paint holding for in-process iframes is only enabled when there is a
// ViewTransition.
paint_holding_feature_.InitWithFeatures(
{blink::features::kPaintHoldingForIframes,
blink::features::kViewTransitionOnNavigationForIframes},
{});
const bool enable_render_document = GetParam();
if (enable_render_document) {
InitAndEnableRenderDocumentFeature(
&render_document_feature_,
GetRenderDocumentLevelName(RenderDocumentLevel::kSubframe));
} else {
InitAndEnableRenderDocumentFeature(
&render_document_feature_,
GetRenderDocumentLevelName(RenderDocumentLevel::kCrashedFrame));
}
auto* command_line = base::CommandLine::ForCurrentProcess();
// This test requires cross-process iframes.
command_line->AppendSwitch(switches::kSitePerProcess);
}
void SetUp() override {
EnablePixelOutput();
NavigationBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&NavigationBrowserTestPaintHoldingSubframe::HandleSlowStyleSheet,
base::Unretained(this)));
NavigationBrowserTest::SetUpOnMainThread();
}
protected:
void WaitForStylesheetRequest() {
if (start_response_) {
return;
}
base::RunLoop run_loop;
run_loop_ = &run_loop;
run_loop.Run();
run_loop_ = nullptr;
}
void FinishStylesheetRequest(RenderFrameHost* rfh) {
std::move(start_response_).Run();
std::move(finish_response_).Run();
// Ensure that the stylesheet response unblocks rendering for this Document.
ASSERT_TRUE(ExecJs(rfh, JsReplace(
R"(
(async () => {
let rafFired = new Promise((resolve) => {
requestAnimationFrame(resolve);
});
await rafFired;
})();
)")));
}
SkBitmap CopyView(RenderWidgetHostView* view) {
base::RunLoop run_loop;
run_loop_ = &run_loop;
constexpr gfx::Size kOutputSize(10, 10);
view->CopyFromSurface(
gfx::Rect(), kOutputSize,
base::BindOnce(&NavigationBrowserTestPaintHoldingSubframe::OnCopyDone,
base::Unretained(this)));
run_loop.Run();
run_loop_ = nullptr;
return bitmap_;
}
private:
class SlowHttpResponseNoCaching : public SlowHttpResponse {
public:
explicit SlowHttpResponseNoCaching(GotRequestCallback got_request)
: SlowHttpResponse(std::move(got_request)) {}
base::StringPairs ResponseHeaders() override {
auto response = SlowHttpResponse::ResponseHeaders();
// Disable response caching.
response.emplace_back("Cache-Control", "max-age=0");
return response;
}
};
std::unique_ptr<net::test_server::HttpResponse> HandleSlowStyleSheet(
const net::test_server::HttpRequest& request) {
if (request.relative_url != "/slow-response") {
return nullptr;
}
return std::make_unique<SlowHttpResponseNoCaching>(base::BindOnce(
&NavigationBrowserTestPaintHoldingSubframe::OnStylesheetRequest,
base::Unretained(this)));
}
void OnStylesheetRequest(base::OnceClosure start_response,
base::OnceClosure finish_response) {
start_response_ = std::move(start_response);
finish_response_ = std::move(finish_response);
if (run_loop_) {
run_loop_->Quit();
}
}
void OnCopyDone(const SkBitmap& bitmap) {
bitmap_ = bitmap;
run_loop_->Quit();
}
SkBitmap bitmap_;
raw_ptr<base::RunLoop> run_loop_;
base::OnceClosure start_response_;
base::OnceClosure finish_response_;
base::test::ScopedFeatureList paint_holding_feature_;
base::test::ScopedFeatureList render_document_feature_;
};
IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, Basic) {
auto* web_contents = shell()->web_contents();
GURL main_url(
embedded_test_server()->GetURL("/render-blocking-mainframe.html"));
ASSERT_TRUE(NavigateToURL(web_contents, main_url));
const std::string iframe_id = "iframe_id";
RenderFrameHostImpl* subframe_rfh = nullptr;
{
const std::string kCreateIFrameWithID = R"(
const iframe = document.createElement("iframe");
iframe.id = $1;
document.body.appendChild(iframe);
)";
ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(),
JsReplace(kCreateIFrameWithID, "iframe_id")));
GURL subframe_url(embedded_test_server()->GetURL(
"a.com", "/render-blocking-subframe.html"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// We should have a cross-process iframe which is a local root.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_TRUE(subframe_rfh->is_local_root());
FinishStylesheetRequest(subframe_rfh);
WaitForCopyableViewInWebContents(web_contents);
}
// The frame is displaying blue.
WaitForCopyableViewInFrame(subframe_rfh);
auto bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap);
{
GURL subframe_url(embedded_test_server()->GetURL(
"a.com", "/render-blocking-subframe.html?red"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// The subframe RFH could have changed.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_TRUE(subframe_rfh->is_local_root());
}
// The frame should continue to display blue from paint holding.
WaitForCopyableViewInWebContents(web_contents);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE);
// Respond to the stylesheet request which will resume rendering in the
// subframe.
FinishStylesheetRequest(subframe_rfh);
// Now the frame is displaying red.
WaitForCopyableViewInFrame(subframe_rfh);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap);
}
IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe,
BasicInProcessIframe) {
auto* web_contents = shell()->web_contents();
GURL main_url(
embedded_test_server()->GetURL("/render-blocking-mainframe.html"));
ASSERT_TRUE(NavigateToURL(web_contents, main_url));
const std::string iframe_id = "iframe_id";
RenderFrameHostImpl* subframe_rfh = nullptr;
{
const std::string kCreateIFrameWithID = R"(
const iframe = document.createElement("iframe");
iframe.id = $1;
document.body.appendChild(iframe);
)";
ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(),
JsReplace(kCreateIFrameWithID, "iframe_id")));
GURL subframe_url(
embedded_test_server()->GetURL("/render-blocking-subframe.html"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// We should have a same-process iframe which is not a local root.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_FALSE(subframe_rfh->is_local_root());
ASSERT_EQ(subframe_rfh->GetProcess(),
web_contents->GetPrimaryMainFrame()->GetProcess());
FinishStylesheetRequest(subframe_rfh);
WaitForCopyableViewInWebContents(web_contents);
}
{
const std::string kInjectVTOptIn = R"(
const style = document.createElement("style");
style.innerHTML = "@view-transition { navigation: auto; }"
document.head.appendChild(style);
)";
ASSERT_TRUE(ExecJs(subframe_rfh, kInjectVTOptIn));
}
// The frame is displaying blue.
WaitForCopyableViewInFrame(subframe_rfh);
auto bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap);
{
GURL subframe_url(
embedded_test_server()->GetURL("/render-blocking-subframe.html?red"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// The subframe RFH could have changed.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_FALSE(subframe_rfh->is_local_root());
ASSERT_EQ(subframe_rfh->GetProcess(),
web_contents->GetPrimaryMainFrame()->GetProcess());
}
// The frame should continue to display blue from paint holding.
WaitForCopyableViewInWebContents(web_contents);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE);
// Respond to the stylesheet request which will resume rendering in the
// subframe.
FinishStylesheetRequest(subframe_rfh);
// Now the frame is displaying red.
WaitForCopyableViewInFrame(subframe_rfh);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap);
}
IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, CrossOrigin) {
auto* web_contents = shell()->web_contents();
GURL main_url(
embedded_test_server()->GetURL("/render-blocking-mainframe.html"));
ASSERT_TRUE(NavigateToURL(web_contents, main_url));
const std::string iframe_id = "iframe_id";
RenderFrameHostImpl* subframe_rfh = nullptr;
{
const std::string kCreateIFrameWithID = R"(
const iframe = document.createElement("iframe");
iframe.id = $1;
document.body.appendChild(iframe);
)";
ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(),
JsReplace(kCreateIFrameWithID, "iframe_id")));
GURL subframe_url(embedded_test_server()->GetURL(
"a.com", "/render-blocking-subframe.html"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// We should have a cross-process iframe which is a local root.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_TRUE(subframe_rfh->is_local_root());
FinishStylesheetRequest(subframe_rfh);
WaitForCopyableViewInWebContents(web_contents);
}
// The frame is displaying blue.
WaitForCopyableViewInFrame(subframe_rfh);
auto bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap);
{
GURL subframe_url(embedded_test_server()->GetURL(
"b.com", "/render-blocking-subframe.html?red"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// The subframe RFH could have changed.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_TRUE(subframe_rfh->is_local_root());
}
// The frame is displaying white (from the main frame) because paint holding
// is disabled.
WaitForCopyableViewInWebContents(web_contents);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorWHITE);
// Respond to the stylesheet request which will resume rendering in the
// subframe.
FinishStylesheetRequest(subframe_rfh);
// Now the frame is displaying red.
WaitForCopyableViewInFrame(subframe_rfh);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap);
}
IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe,
CrashSubframe) {
auto* web_contents = shell()->web_contents();
GURL main_url(
embedded_test_server()->GetURL("/render-blocking-mainframe.html"));
ASSERT_TRUE(NavigateToURL(web_contents, main_url));
const std::string iframe_id = "iframe_id";
RenderFrameHostImpl* subframe_rfh = nullptr;
{
const std::string kCreateIFrameWithID = R"(
const iframe = document.createElement("iframe");
iframe.id = $1;
document.body.appendChild(iframe);
)";
ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(),
JsReplace(kCreateIFrameWithID, "iframe_id")));
GURL subframe_url(embedded_test_server()->GetURL(
"a.com", "/render-blocking-subframe.html"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// We should have a cross-process iframe which is a local root.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_TRUE(subframe_rfh->is_local_root());
FinishStylesheetRequest(subframe_rfh);
WaitForCopyableViewInWebContents(web_contents);
}
// The frame is displaying blue.
WaitForCopyableViewInFrame(subframe_rfh);
auto bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap);
// Crash the subframe.
{
auto* process = subframe_rfh->GetProcess();
content::ScopedAllowRendererCrashes allow_renderer_crashes(process);
RenderProcessHostWatcher watcher(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(content::RESULT_CODE_KILLED);
watcher.Wait();
}
{
GURL subframe_url(embedded_test_server()->GetURL(
"a.com", "/render-blocking-subframe.html?red"));
TestNavigationObserver load_observer(web_contents);
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url));
load_observer.WaitForNavigationFinished();
WaitForStylesheetRequest();
// The subframe RFH could have changed.
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
ASSERT_TRUE(subframe_rfh->is_local_root());
}
// The frame is displaying white (from the main frame) because paint holding
// is disabled.
WaitForCopyableViewInWebContents(web_contents);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorWHITE) << cc::GetPNGDataUrl(bitmap);
// Respond to the stylesheet request which will resume rendering in the
// subframe.
FinishStylesheetRequest(subframe_rfh);
// Now the frame is displaying red.
WaitForCopyableViewInFrame(subframe_rfh);
bitmap = CopyView(web_contents->GetRenderWidgetHostView());
EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap);
}
INSTANTIATE_TEST_SUITE_P(All,
NavigationBrowserTestPaintHoldingSubframe,
::testing::Bool());
RenderFrameHostImpl* GetMainFrameSpeculativeRFH(WebContentsImpl* web_contents) {
return web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
}
void VerifyDeferSpeculativeRFHActionUMA(const base::HistogramTester& tester,
DeferSpeculativeRFHAction action) {
tester.ExpectUniqueSample("Navigation.DeferSpeculativeRFHAction",
static_cast<int>(action), 1);
}
class DeferSpeculativeRFHCreationTest : public NavigationBrowserTest {
public:
DeferSpeculativeRFHCreationTest() {
feature_list_.InitAndEnableFeature(features::kDeferSpeculativeRFHCreation);
// Enable render document for all frames to ensure a speculative RFH
// will be created during navigation.
InitAndEnableRenderDocumentFeature(
&render_document_feature_,
GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames));
}
private:
base::test::ScopedFeatureList feature_list_;
base::test::ScopedFeatureList render_document_feature_;
};
class DeferSpeculativeRFHCreationRenderProcessTest
: public NavigationBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
DeferSpeculativeRFHCreationRenderProcessTest()
: warmup_spare_render_process_(GetParam()) {
std::map<std::string, std::string> parameters = {
{"warmup_spare_process", base::ToString(GetParam())},
};
defer_rfh_feature_list_.InitAndEnableFeatureWithParameters(
features::kDeferSpeculativeRFHCreation, parameters);
android_spare_rederer_feature_.InitAndEnableFeatureWithParameters(
features::kAndroidWarmUpSpareRendererWithTimeout,
base::FieldTrialParams{{"spare_renderer_memory_threshold", "0"}});
InitAndEnableRenderDocumentFeature(
&render_document_feature_,
GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames));
}
// A new renderer process will only be created for a cross-RFH navigation if
// it involves a SiteInstanceGroup change, which will happen if site isolation
// or BFCache is turned on
bool WillWarmupSpareRenderProcess() { return warmup_spare_render_process_; }
bool WillAllocateNewProcess() {
return AreAllSitesIsolatedForTesting() || IsBackForwardCacheEnabled();
}
private:
bool warmup_spare_render_process_;
base::test::ScopedFeatureList defer_rfh_feature_list_;
base::test::ScopedFeatureList android_spare_rederer_feature_;
base::test::ScopedFeatureList render_document_feature_;
};
// Verify the common flow for with DeferSpeculativeRFHCreation feature.
// The creation of the speculative RFH will be deferred until the network
// request is sent.
IN_PROC_BROWSER_TEST_P(DeferSpeculativeRFHCreationRenderProcessTest,
SpeculativeRFHCreationDeferred) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
RenderProcessHost* first_navigation_process =
main_frame()->render_manager()->current_frame_host()->GetProcess();
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
SpareRenderProcessHostManagerImpl::Get().CleanupSparesForTesting();
SpareRenderProcessHostStartedObserver spare_started_observer;
GURL url = embedded_test_server()->GetURL("b.com", "/title1.html");
TestNavigationManager nav_manager(web_contents, url);
base::HistogramTester histogram_tester;
// The speculative RFH shall not be created when the navigation request is
// created.
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url));
DeferSpeculativeRFHAction expected_action =
WillWarmupSpareRenderProcess()
? DeferSpeculativeRFHAction::kDeferredWithRenderProcessWarmUp
: DeferSpeculativeRFHAction::kDeferredWithoutRenderProcessWarmUp;
VerifyDeferSpeculativeRFHActionUMA(histogram_tester, expected_action);
NavigationRequest* navigation_request =
NavigationRequest::From(nav_manager.GetNavigationHandle());
RenderProcessHost* created_process = nullptr;
if (WillWarmupSpareRenderProcess()) {
created_process = spare_started_observer.WaitForSpareRenderProcessStarted();
}
ASSERT_EQ(!!created_process, WillWarmupSpareRenderProcess());
ASSERT_TRUE(navigation_request);
// The navigation manager pauses the navigation in the WillStartRequest
// throttle. The speculative RFH will be created after the throttle completes
// and the navigation request is sent.
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
// The loader will not be created until the WillStartRequest throttle check
// completed.
ASSERT_FALSE(navigation_request->HasLoader());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::NONE);
nav_manager.WaitForSpeculativeRenderFrameHostCreation();
// The speculative RFH shall be created after sending the request.
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
ASSERT_TRUE(navigation_request->HasLoader());
RenderFrameHostImplWrapper speculative_rfh(
GetMainFrameSpeculativeRFH(web_contents));
ASSERT_TRUE(speculative_rfh);
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
if (!WillAllocateNewProcess()) {
ASSERT_EQ(speculative_rfh->GetSiteInstance()->GetProcess(),
first_navigation_process);
} else if (WillWarmupSpareRenderProcess()) {
ASSERT_EQ(speculative_rfh->GetSiteInstance()->GetProcess(),
created_process);
}
// The speculative RFH shall become the primary RFH when the navigation is
// committed.
ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(main_frame()->render_manager()->current_frame_host(),
speculative_rfh.get());
}
INSTANTIATE_TEST_SUITE_P(All,
DeferSpeculativeRFHCreationRenderProcessTest,
::testing::Bool());
// Verify that navigating from a crashed page will create a speculative
// RFH at once.
IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest,
NavigationFromCrashedFrameNotDeferred) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Crash the frame.
{
auto* process = main_frame()
->GetRenderFrameHostManager()
.current_frame_host()
->GetProcess();
content::ScopedAllowRendererCrashes allow_renderer_crashes(process);
RenderProcessHostWatcher watcher(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(content::RESULT_CODE_KILLED);
watcher.Wait();
}
GURL url = embedded_test_server()->GetURL("a.com", "/title2.html");
TestNavigationManager nav_manager(web_contents, url);
// Navigation from a crashed frame shall immediately create a speculative RFH.
base::HistogramTester histogram_tester;
shell()->LoadURL(url);
VerifyDeferSpeculativeRFHActionUMA(histogram_tester,
DeferSpeculativeRFHAction::kNotDeferred);
NavigationRequest* navigation_request =
NavigationRequest::From(nav_manager.GetNavigationHandle());
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
ASSERT_FALSE(navigation_request->HasLoader());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::CURRENT);
ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
}
// Verify that the creation of the speculative RFH is not deferred for the
// web pages.
IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest,
CreationNotDeferredForWebUI) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
GURL url = GetWebUIURL(kChromeUIGpuHost);
TestNavigationManager nav_manager(web_contents, url);
// The speculative RFH shall be created when the navigation starts.
base::HistogramTester histogram_tester;
shell()->LoadURL(url);
VerifyDeferSpeculativeRFHActionUMA(histogram_tester,
DeferSpeculativeRFHAction::kNotDeferred);
ASSERT_TRUE(nav_manager.WaitForRequestStart());
NavigationRequest* navigation_request = main_frame()->navigation_request();
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
ASSERT_FALSE(navigation_request->HasLoader());
ASSERT_TRUE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
}
// Verify that the creation of the speculative RFH is not deferred for the
// pages without a URL loader.
IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest,
CreationNotDeferredWithoutURLLoader) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
GURL url("about:blank");
// The speculative RFH shall be created when the navigation starts.
base::HistogramTester histogram_tester;
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url));
VerifyDeferSpeculativeRFHActionUMA(histogram_tester,
DeferSpeculativeRFHAction::kNotDeferred);
ASSERT_TRUE(WaitForLoadStop(web_contents));
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
}
// Verify that the created speculative RFH after the network request will
// be correctly replaced if the redirection points to a different site.
IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest,
SpeculativeRFHWithRedirect) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html");
GURL url = embedded_test_server()->GetURL(
"c.com", "/server-redirect?" + redirect_url.spec());
TestNavigationManager nav_manager(web_contents, url);
// The speculative RFH shall not be created when the navigation request is
// created.
base::HistogramTester histogram_tester;
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url));
VerifyDeferSpeculativeRFHActionUMA(
histogram_tester,
DeferSpeculativeRFHAction::kDeferredWithoutRenderProcessWarmUp);
NavigationRequest* navigation_request =
NavigationRequest::From(nav_manager.GetNavigationHandle());
ASSERT_TRUE(navigation_request);
// The navigation manager pauses the navigation in the WillStartRequest
// throttle. The speculative RFH will be created after the throttle completes
// and the navigation request is sent.
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
// The loader will not be created until the WillStartRequest throttle check
// completed.
ASSERT_FALSE(navigation_request->HasLoader());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::NONE);
nav_manager.WaitForSpeculativeRenderFrameHostCreation();
// The speculative RFH shall be created after sending the request.
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
ASSERT_TRUE(navigation_request->HasLoader());
ASSERT_TRUE(GetMainFrameSpeculativeRFH(web_contents));
RenderFrameHostImplWrapper speculative_rfh(
GetMainFrameSpeculativeRFH(web_contents));
ASSERT_TRUE(speculative_rfh);
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
// After receiving the redirect, a new speculative RFH shall be created for
// the new site if site isolation is enabled.
ASSERT_TRUE(nav_manager.WaitForResponse());
if (AreAllSitesIsolatedForTesting()) {
ASSERT_TRUE(speculative_rfh.IsDestroyed());
}
RenderFrameHostImplWrapper new_speculative_rfh(
GetMainFrameSpeculativeRFH(web_contents));
ASSERT_TRUE(new_speculative_rfh);
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
// The speculative RFH shall become the primary RFH when the navigation is
// committed.
ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(main_frame()->render_manager()->current_frame_host(),
new_speculative_rfh.get());
}
// Test that if there is a navigation pending for commit, the deferred
// speculative RFH will not be created event after the request is sent. The new
// navigation will be queued until the pending navigation commits.
IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest,
NavigateWithPendingCommit) {
// TODO(crbug.com/349487596): Enable the test after fixing the unrepsonive
// renderer issue.
if (!AreAllSitesIsolatedForTesting() && !IsBackForwardCacheEnabled()) {
return;
}
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
NavigationLogger logger(web_contents);
// Create first navigation and pause before commit.
GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html");
TestNavigationManager nav_manager_b(web_contents, url_b);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_b));
nav_manager_b.WaitForSpeculativeRenderFrameHostCreation();
RenderFrameHostImplWrapper speculative_rfh_b(
GetMainFrameSpeculativeRFH(web_contents));
ASSERT_TRUE(speculative_rfh_b);
ASSERT_TRUE(nav_manager_b.WaitForResponse());
nav_manager_b.ResumeNavigation();
CommitNavigationPauser commit_pauser(speculative_rfh_b.get());
commit_pauser.WaitForCommitAndPause();
// Navigate to a new site, a new speculative RFH will not be created because
// of the pending navigation.
GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html");
TestNavigationManager nav_manager_c(web_contents, url_c);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_c));
NavigationRequest* navigation_request =
NavigationRequest::From(nav_manager_c.GetNavigationHandle());
ASSERT_TRUE(nav_manager_c.WaitForRequestStart());
// Normally, the new navigation will create a speculative RFH after the
// network request is sent, but since there is a pre-existing speculative RFH
// for a pending commit navigation, the new navigation won't create a
// speculative RFH at this point.
nav_manager_c.ResumeNavigation();
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
ASSERT_TRUE(navigation_request->HasLoader());
// Verify that the speculative RFH is not replaced by the new navigation.
ASSERT_EQ(speculative_rfh_b.get(), GetMainFrameSpeculativeRFH(web_contents));
ASSERT_FALSE(speculative_rfh_b.IsDestroyed());
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::NONE);
commit_pauser.ResumePausedCommit();
ASSERT_TRUE(nav_manager_b.WaitForNavigationFinished());
// Verify that a new speculative RFH will be created after the pending
// navigation is committed.
ASSERT_TRUE(nav_manager_c.WaitForResponse());
RenderFrameHostImplWrapper new_speculative_rfh(
GetMainFrameSpeculativeRFH(web_contents));
ASSERT_TRUE(new_speculative_rfh);
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_PROCESS_RESPONSE);
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
ASSERT_TRUE(nav_manager_c.WaitForNavigationFinished());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(main_frame()->render_manager()->current_frame_host(),
new_speculative_rfh.get());
// Check that all the navigations has been committed.
auto results = logger.results();
ASSERT_EQ(2u, results.size());
EXPECT_TRUE(results[0].committed);
EXPECT_EQ(url_b, results[0].url);
EXPECT_TRUE(results[1].committed);
EXPECT_EQ(url_c, results[1].url);
}
class DeferSpeculativeRFHCreationReuseRFHTest : public NavigationBrowserTest {
public:
DeferSpeculativeRFHCreationReuseRFHTest() {
feature_list_.InitAndEnableFeature(features::kDeferSpeculativeRFHCreation);
render_document_feature_.InitAndDisableFeature(features::kRenderDocument);
}
private:
base::test::ScopedFeatureList feature_list_;
base::test::ScopedFeatureList render_document_feature_;
};
// Verify that navigating with the same RFH will reuse the RFH at once.
IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationReuseRFHTest,
ReuseSameRFHNotDeferred) {
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
TestNavigationManager nav_manager(web_contents, url);
// Navigation from about:blank will reuse the render frame host.
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url));
NavigationRequest* navigation_request =
NavigationRequest::From(nav_manager.GetNavigationHandle());
ASSERT_EQ(navigation_request->state(),
NavigationRequest::NavigationState::WILL_START_REQUEST);
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_FALSE(navigation_request->HasLoader());
ASSERT_TRUE(nav_manager.WaitForResponse());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
ASSERT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::CURRENT);
ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents));
}
class VisualPropertiesSynchronization : public NavigationBrowserTest {
public:
VisualPropertiesSynchronization() {
// The deferral of the RFH prevents the potential race condition that this
// regression test is attempting to check.
feature_list_.InitAndDisableFeature(features::kDeferSpeculativeRFHCreation);
auto* command_line = base::CommandLine::ForCurrentProcess();
// This test requires cross-process iframes.
command_line->AppendSwitch(switches::kSitePerProcess);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Regression test for https://crbug.com/352093463.
// Verify that when a cross-origin subframe initiates a top-level navigation to
// a same-origin (with respect to itself) URL, that the visual properties
// are invalidated correctly.
IN_PROC_BROWSER_TEST_F(VisualPropertiesSynchronization,
RemoteToLocalTransition) {
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b_top_level(embedded_test_server()->GetURL("b.com", "/title1.html"));
GURL url_b_iframe(embedded_test_server()->GetURL("b.com", "/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
EXPECT_TRUE(NavigateToURL(shell(), url_a));
EXPECT_TRUE(ExecJs(shell(),
"let iframe = document.createElement('iframe');"
"iframe.id = 'iframe_id';"
"iframe.src = 'about:blank';"
"iframe.style = 'width: 0px; height: 0px;';"
"document.body.appendChild(iframe);"));
EXPECT_TRUE(WaitForLoadStop(web_contents));
// Start a navigation of the top-level document to b.com. Before we leave
// the original a.com, load an iframe to b.com which will be hosted in the
// same b.com process.
content::TestNavigationManager top_level_navigation(web_contents,
url_b_top_level);
EXPECT_TRUE(ExecJs(
shell(), JsReplace("window.location.replace($1);", url_b_top_level)));
// Don't proceed with the top-level navigation (wait while we complete our
// iframe nav to the same origin to commit).
EXPECT_TRUE(top_level_navigation.WaitForLoaderStart());
EXPECT_FALSE(top_level_navigation.was_committed());
// Navigate the iframe to a 'b.com' URL, making a remote frame within the
// a.com page.
FrameTreeNode* root =
FrameTreeNode::From(web_contents->GetPrimaryMainFrame());
CHECK(root->child_count() > 0u);
FrameTreeNode* iframe = root->child_at(0);
TestFrameNavigationObserver iframe_load_observer_first(
iframe->current_frame_host());
ASSERT_TRUE(
BeginNavigateIframeToURL(web_contents, "iframe_id", url_b_iframe));
iframe_load_observer_first.WaitForCommit();
EXPECT_EQ(url_b_iframe, iframe_load_observer_first.last_committed_url());
EXPECT_TRUE(iframe_load_observer_first.last_navigation_succeeded());
// Confirm the cross-process iframe process is not (yet) the main frame's
// process.
RenderFrameHostImpl* subframe_rfh = nullptr;
subframe_rfh = static_cast<RenderFrameHostImpl*>(
ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0));
ASSERT_TRUE(subframe_rfh);
RenderProcessHost* cross_origin_iframe_process = subframe_rfh->GetProcess();
ASSERT_NE(cross_origin_iframe_process,
web_contents->GetPrimaryMainFrame()->GetProcess());
// Allow the top-level navigation to proceed.
EXPECT_FALSE(top_level_navigation.was_committed());
EXPECT_TRUE(top_level_navigation.WaitForNavigationFinished());
EXPECT_TRUE(top_level_navigation.was_committed());
// The main frame should now be using the same 'b.com' renderer.
ASSERT_EQ(cross_origin_iframe_process,
web_contents->GetPrimaryMainFrame()->GetProcess());
// Verify that the browser side's VisualProperties' visible viewport size is
// non-zero.
root = FrameTreeNode::From(web_contents->GetPrimaryMainFrame());
auto* root_rwh = root->current_frame_host()->GetRenderWidgetHost();
std::optional<blink::VisualProperties> visual_properties =
root_rwh->LastComputedVisualProperties();
EXPECT_TRUE(visual_properties);
EXPECT_NE(gfx::Size(0, 0),
visual_properties->visible_viewport_size_device_px);
// Ensure a frame has been produced.
ASSERT_TRUE(
EvalJsAfterLifecycleUpdate(web_contents->GetPrimaryMainFrame(), "", "")
.error.empty());
// Verify the renderer received the correct size for the viewport.
EXPECT_GT(EvalJs(web_contents->GetPrimaryMainFrame(), "window.innerWidth;")
.ExtractDouble(),
0);
EXPECT_GT(EvalJs(web_contents->GetPrimaryMainFrame(), "window.innerHeight;")
.ExtractDouble(),
0);
}
#if BUILDFLAG(IS_ANDROID)
class AndroidPrewarmSpareRendererTest
: public NavigationBrowserTest,
public ::testing::WithParamInterface<std::tuple<std::string, bool>> {
public:
AndroidPrewarmSpareRendererTest() {
std::map<std::string, std::string> parameters = {
{"spare_renderer_creation_timing", std::get<0>(GetParam())},
{"spare_renderer_timeout_seconds",
std::get<1>(GetParam()) ? "10" : "-1"},
};
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/{{features::kAndroidWarmUpSpareRendererWithTimeout,
parameters}},
/*disabled_features=*/{{features::kSpareRendererForSitePerProcess}});
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Enable site per process so that the navigation will take
// the spare process.
command_line->AppendSwitch(switches::kSitePerProcess);
}
bool SpareRendererHasTimeout() { return std::get<1>(GetParam()); }
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
AndroidPrewarmSpareRendererTest,
testing::Combine(
testing::Values(
features::kAndroidSpareRendererCreationAfterLoading,
features::kAndroidSpareRendererCreationAfterFirstPaint,
features::kAndroidSpareRendererCreationDelayedDuringLoading),
testing::Bool()));
IN_PROC_BROWSER_TEST_P(AndroidPrewarmSpareRendererTest, ReuseSpareRenderer) {
auto& spare_manager = SpareRenderProcessHostManagerImpl::Get();
spare_manager.CleanupSparesForTesting();
SpareRenderProcessHostStartedObserver spare_started_observer;
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
RenderProcessHost* created_process =
spare_started_observer.WaitForSpareRenderProcessStarted();
ASSERT_TRUE(!!created_process);
ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title1.html")));
ASSERT_EQ(web_contents->GetSiteInstance()->GetProcess(), created_process);
}
IN_PROC_BROWSER_TEST_P(AndroidPrewarmSpareRendererTest, RendererTimeout) {
scoped_refptr<base::TestMockTimeTaskRunner> task_runner =
new base::TestMockTimeTaskRunner();
auto& spare_manager = SpareRenderProcessHostManagerImpl::Get();
spare_manager.SetDeferTimerTaskRunnerForTesting(task_runner);
const base::TimeDelta kTimeout = base::Seconds(10);
spare_manager.CleanupSparesForTesting();
SpareRenderProcessHostStartedObserver spare_started_observer;
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
RenderProcessHost* created_process =
spare_started_observer.WaitForSpareRenderProcessStarted();
ASSERT_TRUE(!!created_process);
ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process));
if (!SpareRendererHasTimeout()) {
// Warming up a spare renderer with a timeout shall not override
// a spare renderer without a timeout.
spare_manager.WarmupSpare(shell()->web_contents()->GetBrowserContext(),
kTimeout);
}
task_runner->FastForwardBy(kTimeout);
base::RunLoop().RunUntilIdle();
if (SpareRendererHasTimeout()) {
EXPECT_TRUE(spare_manager.GetSpares().empty());
} else {
ASSERT_THAT(spare_manager.GetSpares(),
testing::ElementsAre(created_process));
}
}
#endif // BUILDFLAG(IS_ANDROID)
class HstsUpgradeBrowserTest : public NavigationBrowserTest {
public:
HstsUpgradeBrowserTest() {
feature_list_.InitAndEnableFeature(
net::features::kHstsTopLevelNavigationsOnly);
}
void SetUpOnMainThread() override {
NavigationBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_https_test_server().Start());
}
content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
return fenced_frame_test_helper_;
}
private:
content::test::FencedFrameTestHelper fenced_frame_test_helper_;
base::test::ScopedFeatureList feature_list_;
};
// Tests that when HstsTopLevelNavigationsOnly is enabled only top-level
// navigations will be upgraded by HSTS.
IN_PROC_BROWSER_TEST_F(HstsUpgradeBrowserTest, UpgradeTopLevelOnly) {
// Url that loads a page with the HSTS url, http://b.com, as an iframe under
// an http://a.com main frame.
GURL hsts_url_in_iframe_http = embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)");
// The expected url of the HSTS url, http://b.com, iframe.
GURL url_of_hsts_frame_http = embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b()");
{
// Add hostname to the TransportSecurityState.
base::Time expiry = base::Time::Now() + base::Days(100);
bool include_subdomains = false;
auto* network_context = web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetNetworkContext();
base::RunLoop run_loop;
network_context->AddHSTS(url_of_hsts_frame_http.host(), expiry,
include_subdomains, run_loop.QuitClosure());
run_loop.Run();
}
// Navigate the main frame to the HSTS url, http://b.com.
// Note: Because the http and https embedded test servers run on different
// (non-default) ports the test will fail if we try to navigate to
// `url_of_hsts_frame_http` because HSTS will simply change the scheme to
// https, but the port will remain the http server's port. To work around this
// we can take an https url, `hsts_url_main_frame_https`, and change its
// scheme to http which will then be upgraded by HSTS back to https and will
// load correctly.
// Url of an https://b.com page.
GURL hsts_url_main_frame_https =
embedded_https_test_server().GetURL("b.com", "/title1.html");
GURL::Replacements scheme_replacement;
scheme_replacement.SetSchemeStr("http");
// The navigation should get upgraded to https://b.com.
EXPECT_TRUE(NavigateToURL(
web_contents(),
/*url=*/hsts_url_main_frame_https.ReplaceComponents(scheme_replacement),
/*expected_commit_url=*/hsts_url_main_frame_https));
// Now navigate to an http://a.com page that embeds an http://b.com iframe.
EXPECT_TRUE(NavigateToURL(web_contents(), hsts_url_in_iframe_http));
auto* sub_frame = main_frame()->child_at(0);
// The http://b.com iframe should not have been upgraded.
EXPECT_EQ(url_of_hsts_frame_http,
sub_frame->current_frame_host()->GetLastCommittedURL());
// Fenced Frames are treated as top-level frames in many cases, but not for
// HSTS upgrades. Requests for fenced frames should not be upgraded.
content::RenderFrameHost* fenced_frame =
fenced_frame_test_helper().CreateFencedFrame(
main_frame()->current_frame_host(), url_of_hsts_frame_http);
ASSERT_TRUE(fenced_frame);
EXPECT_EQ(url_of_hsts_frame_http, fenced_frame->GetLastCommittedURL());
}
} // namespace content