blob: 1356bb9808b45cee98b68df8513f9c88e7d665c5 [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 "content/browser/renderer_host/frame_tree.h"
#include "base/command_line.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/dedicated_worker_service.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/origin_util.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/render_document_feature.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom.h"
#include "url/url_constants.h"
namespace content {
namespace {
EvalJsResult GetOriginFromRenderer(FrameTreeNode* node) {
return EvalJs(node, "self.origin");
}
// Expect that frame_name, id and src match the node's values.
void ExpectAttributesEq(FrameTreeNode* node,
const std::string& frame_name,
const std::optional<std::string> id,
const std::optional<std::string> src) {
EXPECT_EQ(frame_name, node->frame_name());
EXPECT_EQ(id, node->html_id());
EXPECT_EQ(src, node->html_src());
}
} // namespace
class FrameTreeBrowserTest : public ContentBrowserTest {
public:
FrameTreeBrowserTest() = default;
FrameTreeBrowserTest(const FrameTreeBrowserTest&) = delete;
FrameTreeBrowserTest& operator=(const FrameTreeBrowserTest&) = delete;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
// Ensures FrameTree correctly reflects page structure during navigations.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeShape) {
GURL base_url = embedded_test_server()->GetURL("A.com", "/site_isolation/");
// Load doc without iframes. Verify FrameTree just has root.
// Frame tree:
// Site-A Root
EXPECT_TRUE(NavigateToURL(shell(), base_url.Resolve("blank.html")));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(0U, root->child_count());
// Add 2 same-site frames. Verify 3 nodes in tree with proper names.
// Frame tree:
// Site-A Root -- Site-A frame1
// \-- Site-A frame2
LoadStopObserver observer1(shell()->web_contents());
EXPECT_TRUE(NavigateToURL(shell(), base_url.Resolve("frames-X-X.html")));
observer1.Wait();
ASSERT_EQ(2U, root->child_count());
EXPECT_EQ(0U, root->child_at(0)->child_count());
EXPECT_EQ(0U, root->child_at(1)->child_count());
}
// TODO(ajwong): Talk with nasko and merge this functionality with
// FrameTreeShape.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeShape2) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
// Check that the root node is properly created.
ASSERT_EQ(3UL, root->child_count());
ExpectAttributesEq(root, std::string(), std::nullopt, std::nullopt);
ASSERT_EQ(2UL, root->child_at(0)->child_count());
ExpectAttributesEq(root->child_at(0), "1-1-name", "1-1-id", "1-1.html");
// Verify the deepest node exists and has the right name.
ASSERT_EQ(2UL, root->child_at(2)->child_count());
EXPECT_EQ(1UL, root->child_at(2)->child_at(1)->child_count());
EXPECT_EQ(0UL, root->child_at(2)->child_at(1)->child_at(0)->child_count());
ExpectAttributesEq(root->child_at(2)->child_at(1)->child_at(0), "3-1-name",
"3-1-id", "3-1.html");
// Navigate to about:blank, which should leave only the root node of the frame
// tree in the browser process.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
root = wc->GetPrimaryFrameTree().root();
EXPECT_EQ(0UL, root->child_count());
ExpectAttributesEq(root, std::string(), std::nullopt, std::nullopt);
}
// Frame attributes of iframe elements are correctly tracked in FrameTree.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeAttributesUpdate) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
// Check that the root node is properly created.
ASSERT_EQ(3UL, root->child_count());
ExpectAttributesEq(root, std::string(), std::nullopt, std::nullopt);
ASSERT_EQ(2UL, root->child_at(0)->child_count());
ExpectAttributesEq(root->child_at(0), "1-1-name", "1-1-id", "1-1.html");
// Change id, name and src of the iframe.
EXPECT_TRUE(ExecJs(root->current_frame_host(), R"(
let iframe = document.getElementById('1-1-id');
iframe.id = '1-1-updated-id';
iframe.name = '1-1-updated-name';
iframe.src = '1-1-updated.html';
)"));
// |html_name()| gets updated whenever the name attribute gets updated.
EXPECT_EQ("1-1-updated-name", root->child_at(0)->html_name());
ExpectAttributesEq(root->child_at(0), "1-1-name", "1-1-updated-id",
"1-1-updated.html");
}
// Ensures that frames' name attributes and their updates are tracked in
// |html_name()| and window.name and its updates are tracked in |frame_name()|.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameNameVSWindowName) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
// Check that the root node is properly created.
ASSERT_EQ(3UL, root->child_count());
EXPECT_EQ(std::nullopt, root->html_name());
EXPECT_EQ(std::string(), root->frame_name());
ASSERT_EQ(2UL, root->child_at(0)->child_count());
EXPECT_EQ("1-1-name", root->child_at(0)->html_name());
EXPECT_EQ("1-1-name", root->child_at(0)->frame_name());
// Change the name attribute of the iframe.
EXPECT_TRUE(ExecJs(root->current_frame_host(), R"(
let iframe = document.getElementById('1-1-id');
iframe.name = '1-1-updated-name';
)"));
// |html_name()| gets updated whenever the name attribute gets updated.
EXPECT_EQ("1-1-updated-name", root->child_at(0)->html_name());
// |frame_name()| stays the same.
EXPECT_EQ("1-1-name", root->child_at(0)->frame_name());
// Change the window.name of the iframe.
EXPECT_TRUE(ExecJs(root->current_frame_host(), R"(
let iframe = document.getElementById('1-1-id');
iframe.contentWindow.name = '1-1-updated-name-2';
)"));
// |html_name()| stays the same.
EXPECT_EQ("1-1-updated-name", root->child_at(0)->html_name());
// |frame_name()| gets updated.
EXPECT_EQ("1-1-updated-name-2", root->child_at(0)->frame_name());
}
// Ensures that long attributes are cut down to the max length.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, LongAttributesCutDown) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
// Check that the root node is properly created.
ASSERT_EQ(3UL, root->child_count());
ASSERT_EQ(2UL, root->child_at(0)->child_count());
EXPECT_EQ("1-1-name", root->child_at(0)->html_name());
// Change the name attribute of the iframe.
EXPECT_TRUE(ExecJs(root->current_frame_host(), R"(
let iframe = document.getElementById('1-1-id');
iframe.id += 'a'.repeat(1200);
iframe.name += 'b'.repeat(1200);
iframe.src += 'c'.repeat(1200) + '.html';
)"));
// Long attribute is cut down to the maximum length.
EXPECT_EQ(1024UL, root->child_at(0)->html_id()->size());
EXPECT_EQ(1024UL, root->child_at(0)->html_name()->size());
EXPECT_EQ(1024UL, root->child_at(0)->html_src()->size());
}
// Insert a frame into the frame tree and ensure that the inserted frame's
// attributes are correctly captured.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, InsertFrameInTree) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
// Check that the root node is properly created.
ASSERT_EQ(3UL, root->child_count());
ExpectAttributesEq(root, std::string(), std::nullopt, std::nullopt);
ASSERT_EQ(2UL, root->child_at(0)->child_count());
ExpectAttributesEq(root->child_at(0), "1-1-name", "1-1-id", "1-1.html");
// Insert a child iframe.
EXPECT_TRUE(ExecJs(root->current_frame_host(), R"(
let new_iframe = document.createElement('iframe');
new_iframe.id = '1-1-child-id';
new_iframe.src = '1-1-child.html';
new_iframe.name = '1-1-child-name';
document.body.appendChild(new_iframe);
)"));
// Check that the new iframe is inserted and their attributes are correct.
ASSERT_EQ(4UL, root->child_count());
ExpectAttributesEq(root->child_at(3), "1-1-child-name", "1-1-child-id",
"1-1-child.html");
}
// Test that we can navigate away if the previous renderer doesn't clean up its
// child frames.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeAfterCrash) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
// Ensure the view and frame are live.
RenderFrameHostImpl* rfh1 = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderViewHostImpl* rvh = rfh1->render_view_host();
EXPECT_TRUE(rvh->IsRenderViewLive());
EXPECT_TRUE(rfh1->IsRenderFrameLive());
// Crash the renderer so that it doesn't send any FrameDetached messages.
RenderProcessHostWatcher crash_observer(
shell()->web_contents(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
ASSERT_TRUE(
shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->Shutdown(
0));
crash_observer.Wait();
// The frame tree should be cleared.
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
EXPECT_EQ(0UL, root->child_count());
// Ensure the view and frame aren't live anymore.
EXPECT_FALSE(rvh->IsRenderViewLive());
EXPECT_FALSE(rfh1->IsRenderFrameLive());
// Navigate to a new URL.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(0UL, root->child_count());
EXPECT_EQ(url, root->current_url());
RenderFrameHostImpl* rfh2 = root->current_frame_host();
// Ensure the view and frame are live again.
EXPECT_TRUE(rvh->IsRenderViewLive());
EXPECT_TRUE(rfh2->IsRenderFrameLive());
}
// Tests the frame discard impl, both with and without post-discard process
// shutdown.
class FrameTreeBrowserWithDiscardTest
: public FrameTreeBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
FrameTreeBrowserWithDiscardTest() {
scoped_feature_list_.InitAndEnableFeature(features::kWebContentsDiscard);
}
void DiscardFrameTree(FrameTree& frame_tree) {
RenderProcessHostImpl* root_rph = static_cast<RenderProcessHostImpl*>(
frame_tree.root()->current_frame_host()->GetProcess());
if (KeepAliveDiscardedProcess()) {
// Increment the worker ref count of the renderer process to keep it
// alive post discard, simulating the situation where the process may be
// shared by multiple frames.
root_rph->IncrementWorkerRefCount();
}
frame_tree.Discard();
if (!KeepAliveDiscardedProcess()) {
// If not keeping the process alive wait for it to successfully exit.
RenderProcessHostWatcher exit_observer(
root_rph, content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
exit_observer.Wait();
}
}
bool KeepAliveDiscardedProcess() const { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_P(FrameTreeBrowserWithDiscardTest, DiscardFrameTree) {
base::HistogramTester uma_recorder;
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
FrameTreeNode* root = frame_tree.root();
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
EXPECT_FALSE(wc->GetController().NeedsReload());
EXPECT_EQ(3UL, root->child_count());
// Ensure the view and frame are live.
RenderFrameHostImplWrapper initial_rfh(wc->GetPrimaryMainFrame());
RenderViewHostImpl* initial_rvh = initial_rfh->render_view_host();
EXPECT_TRUE(initial_rvh->IsRenderViewLive());
EXPECT_TRUE(initial_rfh->IsRenderFrameLive());
const auto get_child_isn = [&](RenderFrameHostImpl* rfh, int child_pos) {
return rfh->child_at(child_pos)
->current_frame_host()
->last_committed_frame_entry()
->item_sequence_number();
};
const int initial_nav_entry_id =
wc->GetController().GetLastCommittedEntry()->GetUniqueID();
const int64_t initial_isn =
initial_rfh->last_committed_frame_entry()->item_sequence_number();
const int64_t initial_isn_child1 = get_child_isn(initial_rfh.get(), 0);
const int64_t initial_isn_child2 = get_child_isn(initial_rfh.get(), 1);
const int64_t initial_isn_child3 = get_child_isn(initial_rfh.get(), 2);
// Discard the frame tree, wait until all child frames have been cleared away.
EXPECT_FALSE(root->was_discarded());
uma_recorder.ExpectUniqueSample("Discarding.DiscardFrameTree", true, 0);
DiscardFrameTree(frame_tree);
ASSERT_TRUE(
base::test::RunUntil([&]() { return 0u == root->child_count(); }));
// The root rfh and rvh should remain unchanged however the child frames
// should have been cleared.
EXPECT_EQ(initial_nav_entry_id,
wc->GetController().GetLastCommittedEntry()->GetUniqueID());
EXPECT_EQ(initial_isn,
initial_rfh->last_committed_frame_entry()->item_sequence_number());
EXPECT_TRUE(root->was_discarded());
EXPECT_TRUE(wc->GetController().NeedsReload());
EXPECT_EQ(initial_rfh.get(), wc->GetPrimaryMainFrame());
EXPECT_EQ(initial_rvh, wc->GetPrimaryMainFrame()->render_view_host());
EXPECT_EQ(0u, root->child_count());
uma_recorder.ExpectUniqueSample("Discarding.DiscardFrameTree", true, 1);
if (KeepAliveDiscardedProcess()) {
EXPECT_TRUE(initial_rvh->IsRenderViewLive());
EXPECT_TRUE(initial_rfh->IsRenderFrameLive());
} else {
// After the document has been discarded the render process should have been
// cleared away.
EXPECT_FALSE(initial_rvh->IsRenderViewLive());
EXPECT_FALSE(initial_rfh->IsRenderFrameLive());
}
// Reload the frame tree. Child frames should be reloaded and the root rfh and
// rvh should have changed.
wc->GetController().LoadIfNecessary();
EXPECT_TRUE(WaitForLoadStop(wc));
RenderFrameHostImplWrapper final_rfh(wc->GetPrimaryMainFrame());
RenderViewHostImpl* final_rvh = final_rfh->render_view_host();
EXPECT_EQ(initial_nav_entry_id,
wc->GetController().GetLastCommittedEntry()->GetUniqueID());
EXPECT_EQ(initial_isn,
final_rfh->last_committed_frame_entry()->item_sequence_number());
EXPECT_EQ(initial_isn_child1, get_child_isn(final_rfh.get(), 0));
EXPECT_EQ(initial_isn_child2, get_child_isn(final_rfh.get(), 1));
EXPECT_EQ(initial_isn_child3, get_child_isn(final_rfh.get(), 2));
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
EXPECT_EQ(3u, root->child_count());
EXPECT_NE(initial_rfh.get(), final_rfh.get());
EXPECT_TRUE(final_rvh->IsRenderViewLive());
EXPECT_TRUE(final_rfh->IsRenderFrameLive());
if (KeepAliveDiscardedProcess()) {
EXPECT_NE(initial_rvh, final_rvh);
} else {
// TODO(crbug.com/40228869): It should be the case that a new RVH is created
// when reloading from a discarded state. This expectation for when the
// render process is shutdown should be merged with the one above once
// support for terminated processes is landed and all main-frame navigations
// use speculative RenderViewHosts.
EXPECT_EQ(initial_rvh, final_rvh);
}
}
// Regression test for crbug.com/361658816. Ensures that same-document
// navigations triggered in the document's unload handler are handled without
// crashing.
IN_PROC_BROWSER_TEST_P(FrameTreeBrowserWithDiscardTest,
DiscardHandlesSameDocumentNavigationsDuringUnload) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
FrameTreeNode* root = frame_tree.root();
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
// Setup a same-document navigation in the unload handler.
ASSERT_TRUE(ExecJs(shell(), R"(
addEventListener('unload', () => {
history.pushState({}, '', 'title1.html');
});
)"));
// Discard the rfh, the frame and its children should be cleared successfully.
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
EXPECT_EQ(3UL, root->child_count());
DiscardFrameTree(frame_tree);
EXPECT_TRUE(root->was_discarded());
EXPECT_TRUE(wc->GetController().NeedsReload());
ASSERT_TRUE(
base::test::RunUntil([&]() { return 0u == root->child_count(); }));
}
IN_PROC_BROWSER_TEST_P(FrameTreeBrowserWithDiscardTest,
DiscardClearsServiceWorkers) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
FrameTreeNode* root = frame_tree.root();
// Load a new page, register a service worker and wait for it to become ready.
EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
"/register_service_worker.html")));
EXPECT_EQ("DONE", EvalJs(shell(), "register('/fetch_event_passthrough.js')"));
RenderFrameHostImplWrapper rfh(wc->GetPrimaryMainFrame());
EXPECT_EQ(1u, rfh->service_worker_clients_for_testing().size());
// Discard the frame tree.
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
DiscardFrameTree(frame_tree);
EXPECT_TRUE(root->was_discarded());
EXPECT_TRUE(wc->GetController().NeedsReload());
// Assert the service worker has been de-registered post discard.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return rfh->service_worker_clients_for_testing().size() == 0; }));
}
// Runs pending navigation discard browsertests with RenderDocument enabled for
// all frames to ensure a speculative RFH is created during navigation.
class FrameTreeDiscardPendingNavigationTest
: public FrameTreeBrowserWithDiscardTest {
public:
FrameTreeDiscardPendingNavigationTest() {
InitAndEnableRenderDocumentFeature(
&feature_list_,
GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames));
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_P(FrameTreeDiscardPendingNavigationTest,
DiscardHandlesNavigationWaitingResponse) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
FrameTreeNode* root = frame_tree.root();
const GURL original_url =
embedded_test_server()->GetURL("/frame_tree/top.html");
EXPECT_TRUE(NavigateToURL(shell(), original_url));
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
EXPECT_EQ(3UL, root->child_count());
// Queue a navigation.
const GURL new_url = embedded_test_server()->GetURL("/frame_tree/1-1.html");
TestNavigationManager manager(shell()->web_contents(), new_url);
shell()->LoadURL(new_url);
// Get to the point where the frame is waiting for the response.
EXPECT_TRUE(manager.WaitForRequestStart());
manager.ResumeNavigation();
// Discard while waiting for a response for the previous navigation.
DiscardFrameTree(frame_tree);
EXPECT_TRUE(WaitForLoadStop(wc));
EXPECT_TRUE(root->was_discarded());
EXPECT_TRUE(wc->GetController().NeedsReload());
// Assert the pending navigation finished.
ASSERT_TRUE(manager.WaitForNavigationFinished());
// Wait for the discarded document to be replaced and clear its children.
ASSERT_TRUE(
base::test::RunUntil([&]() { return 0u == root->child_count(); }));
EXPECT_EQ(original_url, wc->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_P(FrameTreeDiscardPendingNavigationTest,
DiscardHandlesNavigationPendingCommit) {
class ReadyToCommitWaiter : public content::WebContentsObserver {
public:
explicit ReadyToCommitWaiter(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
void Wait() { run_loop_.Run(); }
void ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) override {
run_loop_.Quit();
}
private:
base::RunLoop run_loop_;
};
base::HistogramTester uma_recorder;
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
FrameTreeNode* root = frame_tree.root();
const GURL original_url =
embedded_test_server()->GetURL("/frame_tree/top.html");
EXPECT_TRUE(NavigateToURL(shell(), original_url));
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
EXPECT_EQ(3UL, root->child_count());
RenderFrameHostImplWrapper initial_rfh(wc->GetPrimaryMainFrame());
EXPECT_TRUE(initial_rfh->IsRenderFrameLive());
// Queue a navigation and wait until the browser is ready to commit.
ReadyToCommitWaiter ready_to_commit_waiter(wc);
const GURL new_url = embedded_test_server()->GetURL("/title1.html");
shell()->LoadURL(new_url);
ready_to_commit_waiter.Wait();
// Discard while ready to commit the previous navigation.
uma_recorder.ExpectUniqueSample("Discarding.DiscardFrameTree", false, 0);
frame_tree.Discard();
EXPECT_TRUE(WaitForLoadStop(wc));
// The pending navigation will commit to a new rfh and the tab will settle to
// an undiscarded state.
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
uma_recorder.ExpectUniqueSample("Discarding.DiscardFrameTree", false, 1);
RenderFrameHostImplWrapper final_rfh(wc->GetPrimaryMainFrame());
EXPECT_NE(initial_rfh.get(), final_rfh.get());
EXPECT_EQ(new_url, root->current_url());
}
// Asserts that a process pinned with a keep-alive ref hosting only discarded
// frames is successfully shutdown after the keep-alive timeout.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
DiscardedFrameRendererShutdownAfterKeepAliveTimeout) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
// Ensure the view, frame and process are reported alive.
RenderFrameHostImpl* rfh =
static_cast<RenderFrameHostImpl*>(wc->GetPrimaryMainFrame());
RenderViewHostImpl* rvh = rfh->render_view_host();
RenderProcessHostImpl* rph =
static_cast<RenderProcessHostImpl*>(rfh->GetProcess());
EXPECT_TRUE(rvh->IsRenderViewLive());
EXPECT_TRUE(rfh->IsRenderFrameLive());
EXPECT_TRUE(rph->IsInitializedAndNotDead());
// Set a worker on the renderer process.
rph->IncrementWorkerRefCount();
// Discard the frame tree. The process should remain alive.
frame_tree.Discard();
EXPECT_TRUE(rvh->IsRenderViewLive());
EXPECT_TRUE(rfh->IsRenderFrameLive());
EXPECT_TRUE(rph->IsInitializedAndNotDead());
// Simulate a keep-alive timeout, the process should be promptly shutdown.
rfh->SimulateDiscardShutdownKeepAliveTimeoutForTesting();
RenderProcessHostWatcher process_exit_observer(
wc, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process_exit_observer.Wait();
// Ensure the process has been successfully shutdown.
EXPECT_FALSE(rvh->IsRenderViewLive());
EXPECT_FALSE(rfh->IsRenderFrameLive());
EXPECT_FALSE(rph->IsInitializedAndNotDead());
}
class DedicatedWorkerObserver : public DedicatedWorkerService::Observer {
public:
explicit DedicatedWorkerObserver(DedicatedWorkerService* worker_service) {
scoped_context_observation_.Observe(worker_service);
}
DedicatedWorkerObserver(const DedicatedWorkerObserver&) = delete;
DedicatedWorkerObserver& operator=(const DedicatedWorkerObserver&) = delete;
~DedicatedWorkerObserver() override = default;
void WaitForCreated() {
if (!is_created_) {
run_loop_.emplace();
run_loop_->Run();
run_loop_.reset();
}
}
void WaitForDestroyed() {
if (!is_destroyed_) {
run_loop_.emplace();
run_loop_->Run();
run_loop_.reset();
}
}
// DedicatedWorkerService::Observer:
void OnBeforeWorkerDestroyed(const blink::DedicatedWorkerToken& worker_token,
DedicatedWorkerCreator creator) override {
is_destroyed_ = true;
if (run_loop_.has_value()) {
run_loop_->Quit();
}
}
void OnWorkerCreated(const blink::DedicatedWorkerToken& worker_token,
int worker_process_id,
const url::Origin& security_origin,
DedicatedWorkerCreator creator) override {
is_created_ = true;
if (run_loop_.has_value()) {
run_loop_->Quit();
}
}
void OnFinalResponseURLDetermined(
const blink::DedicatedWorkerToken& worker_token,
const GURL& url) override {}
private:
bool is_created_ = false;
bool is_destroyed_ = false;
std::optional<base::RunLoop> run_loop_;
base::ScopedObservation<DedicatedWorkerService,
DedicatedWorkerService::Observer>
scoped_context_observation_{this};
};
class DedicatedWorkerFrameTreeBrowserTest
: public FrameTreeBrowserWithDiscardTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
FrameTreeBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(blink::switches::kJavaScriptFlags,
"--expose-gc");
}
};
IN_PROC_BROWSER_TEST_P(DedicatedWorkerFrameTreeBrowserTest,
DiscardClearsDedicatedWorkers) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
FrameTreeNode* root = frame_tree.root();
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
// Navigate to a page and register a dedicated worker.
DedicatedWorkerObserver worker_observer(root->current_frame_host()
->GetStoragePartition()
->GetDedicatedWorkerService());
EXPECT_TRUE(EvalJs(shell(), "const worker = new Worker('/workers/empty.js');")
.error.empty());
worker_observer.WaitForCreated();
// Discard the rfh, the associated worker should be cleared.
EXPECT_FALSE(root->was_discarded());
EXPECT_FALSE(wc->GetController().NeedsReload());
DiscardFrameTree(frame_tree);
EXPECT_TRUE(root->was_discarded());
EXPECT_TRUE(wc->GetController().NeedsReload());
if (KeepAliveDiscardedProcess()) {
// Trigger GC to cleanup the worker in the renderer if persisted.
EXPECT_TRUE(EvalJs(shell(), "window.gc();").error.empty());
}
worker_observer.WaitForDestroyed();
}
// TODO(347770670): Consider restricting script access to discarded documents
// from related documents.
IN_PROC_BROWSER_TEST_P(FrameTreeBrowserWithDiscardTest,
DiscardedFrameAllowsScriptAccess) {
if (!KeepAliveDiscardedProcess()) {
GTEST_SKIP() << "Not applicable when destroying process post discard.";
}
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
const GURL main_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Open a popup using window.open.
const GURL popup_url(embedded_test_server()->GetURL("/title2.html"));
Shell* new_shell = OpenPopup(shell(), popup_url, "foo");
EXPECT_TRUE(new_shell);
// Assert the opened window is able to script its opener.
EXPECT_EQ("foo", EvalJs(new_shell, "window.name;"));
EXPECT_TRUE(EvalJs(new_shell, "window.opener.name = 'bar';").error.empty());
EXPECT_EQ("bar", EvalJs(shell(), "window.name;"));
frame_tree.Discard();
EXPECT_TRUE(frame_tree.root()->was_discarded());
// After a discard operation the opened window should should still be able to
// script its opener.
EXPECT_TRUE(EvalJs(new_shell, "window.opener.name = 'bar2';").error.empty());
EXPECT_EQ("bar2", EvalJs(shell(), "window.name;"));
// After a reload the opened window should still be able to script its opener.
wc->GetController().LoadIfNecessary();
EXPECT_TRUE(WaitForLoadStop(wc));
EXPECT_TRUE(EvalJs(new_shell, "window.opener.name = 'bar3';").error.empty());
EXPECT_EQ("bar3", EvalJs(shell(), "window.name;"));
}
class FrameTreeBrowserTestWithBFCache : public FrameTreeBrowserTest {
public:
FrameTreeBrowserTestWithBFCache() {
feature_list_.InitWithFeaturesAndParameters(
{{features::kBackForwardCache, {}},
{kBackForwardCacheNoTimeEviction, {}}},
// Allow BackForwardCache for all devices regardless of their memory.
{features::kBackForwardCacheMemoryControls});
EXPECT_TRUE(IsBackForwardCacheEnabled());
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTestWithBFCache,
FrameTreeBrowserWithDiscardTest) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
BackForwardCacheImpl& back_forward_cache =
wc->GetController().GetBackForwardCache();
// The BFCache should start empty.
EXPECT_TRUE(back_forward_cache.GetEntries().empty());
const GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
const GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
// Navigate to url1.
EXPECT_TRUE(NavigateToURL(shell(), url1));
RenderFrameHostImplWrapper rfh_a(wc->GetPrimaryMainFrame());
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a.get());
// Navigate to url2, the frame hosting url1 should be moved to the BFCache.
EXPECT_TRUE(NavigateToURL(shell(), url2));
EXPECT_FALSE(delete_observer_rfh_a.deleted());
EXPECT_TRUE(rfh_a->IsInBackForwardCache());
EXPECT_EQ(1u, back_forward_cache.GetEntries().size());
// Discard the frame tree, the BFCache should have been cleared.
frame_tree.Discard();
EXPECT_TRUE(frame_tree.root()->was_discarded());
back_forward_cache.PostTaskToDestroyEvictedFrames();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return back_forward_cache.GetEntries().empty(); }));
EXPECT_TRUE(delete_observer_rfh_a.deleted());
}
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTestWithBFCache,
DiscardedFrameDoesNotEnterBFCache) {
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTree& frame_tree = wc->GetPrimaryFrameTree();
BackForwardCacheImpl& back_forward_cache =
wc->GetController().GetBackForwardCache();
// The BFCache should start empty.
EXPECT_TRUE(back_forward_cache.GetEntries().empty());
const GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
const GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
// Navigate to url1.
EXPECT_TRUE(NavigateToURL(shell(), url1));
RenderFrameHostImplWrapper rfh_a(wc->GetPrimaryMainFrame());
RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a.get());
// Discard the frame tree.
frame_tree.Discard();
EXPECT_TRUE(frame_tree.root()->was_discarded());
EXPECT_FALSE(delete_observer_rfh_a.deleted());
// Navigate to url2, the frame hosting url1 should not be moved to the
// BFCache.
EXPECT_TRUE(NavigateToURL(shell(), url2));
EXPECT_FALSE(frame_tree.root()->was_discarded());
EXPECT_TRUE(back_forward_cache.GetEntries().empty());
ASSERT_TRUE(
base::test::RunUntil([&]() { return delete_observer_rfh_a.deleted(); }));
}
// Test that we can navigate away if the previous renderer doesn't clean up its
// child frames.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateWithLeftoverFrames) {
GURL base_url = embedded_test_server()->GetURL("A.com", "/site_isolation/");
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
// Hang the renderer so that it doesn't send any FrameDetached messages.
// (This navigation will never complete, so don't wait for it.)
shell()->LoadURL(GURL(blink::kChromeUIHangURL));
// Check that the frame tree still has children.
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
ASSERT_EQ(3UL, root->child_count());
// Navigate to a new URL. We use LoadURL because NavigateToURL will try to
// wait for the previous navigation to stop.
TestNavigationObserver tab_observer(wc, 1);
shell()->LoadURL(base_url.Resolve("blank.html"));
tab_observer.Wait();
// The frame tree should now be cleared.
EXPECT_EQ(0UL, root->child_count());
}
// Ensure that IsRenderFrameLive is true for main frames and same-site iframes.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, IsRenderFrameLive) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// The root and subframe should each have a live RenderFrame.
EXPECT_TRUE(
root->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
// Load a same-site page into iframe and it should still be live.
GURL http_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), http_url));
EXPECT_TRUE(
root->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
}
// Ensure that origins are correctly set on navigations.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, OriginSetOnNavigation) {
GURL about_blank(url::kAboutBlankURL);
GURL main_url(
embedded_test_server()->GetURL("a.com", "/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContents* contents = shell()->web_contents();
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents)->GetPrimaryFrameTree().root();
// Extra '/' is added because the replicated origin is serialized in RFC 6454
// format, which dictates no trailing '/', whereas GURL::GetOrigin does put a
// '/' at the end.
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->current_origin().Serialize() + '/');
EXPECT_EQ(
main_url.DeprecatedGetOriginAsURL().spec(),
root->current_frame_host()->GetLastCommittedOrigin().Serialize() + '/');
// The iframe is inititially same-origin.
EXPECT_TRUE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
root->child_at(0)->current_frame_host()->GetLastCommittedOrigin()));
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(),
GetOriginFromRenderer(root->child_at(0)));
// Navigate the iframe cross-origin.
GURL frame_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
EXPECT_EQ(frame_url, root->child_at(0)->current_url());
EXPECT_EQ(frame_url.DeprecatedGetOriginAsURL().spec(),
root->child_at(0)->current_origin().Serialize() + '/');
EXPECT_FALSE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
root->child_at(0)->current_frame_host()->GetLastCommittedOrigin()));
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(),
GetOriginFromRenderer(root->child_at(0)));
// Parent-initiated about:blank navigation should inherit the parent's a.com
// origin.
NavigateIframeToURL(contents, "1-1-id", about_blank);
EXPECT_EQ(about_blank, root->child_at(0)->current_url());
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->child_at(0)->current_origin().Serialize() + '/');
EXPECT_EQ(root->current_frame_host()->GetLastCommittedOrigin().Serialize(),
root->child_at(0)
->current_frame_host()
->GetLastCommittedOrigin()
.Serialize());
EXPECT_TRUE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
root->child_at(0)->current_frame_host()->GetLastCommittedOrigin()));
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(),
GetOriginFromRenderer(root->child_at(0)));
GURL data_url("data:text/html,foo");
EXPECT_TRUE(NavigateToURL(shell(), data_url));
// Navigating to a data URL should set a unique origin. This is represented
// as "null" per RFC 6454.
EXPECT_EQ("null", root->current_origin().Serialize());
EXPECT_TRUE(
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
EXPECT_EQ("null", GetOriginFromRenderer(root));
// Re-navigating to a normal URL should update the origin.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->current_origin().Serialize() + '/');
EXPECT_EQ(
main_url.DeprecatedGetOriginAsURL().spec(),
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().Serialize() +
'/');
EXPECT_FALSE(
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
}
// Tests a cross-origin navigation to a blob URL. The main frame initiates this
// navigation on its grandchild. It should wind up in the main frame's process.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateGrandchildToBlob) {
WebContents* contents = shell()->web_contents();
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents)->GetPrimaryFrameTree().root();
// First, snapshot the FrameTree for a normal A(B(A)) case where all frames
// are served over http. The blob test should result in the same structure.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(a))")));
std::string reference_tree = DepictFrameTree(*root);
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// The root node will initiate the navigation; its grandchild node will be the
// target of the navigation.
FrameTreeNode* target = root->child_at(0)->child_at(0);
RenderFrameDeletedObserver deleted_observer(target->current_frame_host());
std::string html =
"<html><body><div>This is blob content.</div>"
"<script>"
"window.parent.parent.postMessage('HI', self.origin);"
"</script></body></html>";
std::string script = JsReplace(
"new Promise((resolve) => {"
" window.addEventListener('message', resolve, false);"
" var blob = new Blob([$1], {type: 'text/html'});"
" var blob_url = URL.createObjectURL(blob);"
" frames[0][0].location.href = blob_url;"
"}).then((event) => {"
" document.body.appendChild(document.createTextNode(event.data));"
" return event.source.location.href;"
"});",
html);
std::string blob_url_string = EvalJs(root, script).ExtractString();
// Wait for the RenderFrame to go away, if this will be cross-process.
if (AreAllSitesIsolatedForTesting())
deleted_observer.WaitUntilDeleted();
EXPECT_EQ(GURL(blob_url_string), target->current_url());
EXPECT_EQ(url::kBlobScheme, target->current_url().scheme());
EXPECT_FALSE(target->current_origin().opaque());
EXPECT_EQ("a.com", target->current_origin().host());
EXPECT_EQ(url::kHttpScheme, target->current_origin().scheme());
EXPECT_EQ("This is blob content.",
EvalJs(target, "document.body.children[0].innerHTML"));
EXPECT_EQ(reference_tree, DepictFrameTree(*root));
}
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateChildToAboutBlank) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The leaf node (c.com) will be navigated. Its parent node (b.com) will
// initiate the navigation.
FrameTreeNode* target =
contents->GetPrimaryFrameTree().root()->child_at(0)->child_at(0);
RenderFrameHost* initiator_rfh = target->parent();
// Give the target a name.
EXPECT_TRUE(ExecJs(target, "window.name = 'target';"));
// Use window.open(about:blank), then poll the document for access.
EvalJsResult about_blank_origin = EvalJs(
initiator_rfh,
"new Promise(resolve => {"
" var didNavigate = false;"
" var intervalID = setInterval(function() {"
" if (!didNavigate) {"
" didNavigate = true;"
" window.open('about:blank', 'target');"
" }"
" // Poll the document until it doesn't throw a SecurityError.\n"
" try {"
" frames[0].document.write('Hi from ' + document.domain);"
" } catch (e) { return; }"
" clearInterval(intervalID);"
" resolve(frames[0].self.origin);"
" }, 16);"
"});");
EXPECT_EQ(target->current_origin(), about_blank_origin);
EXPECT_EQ(GURL(url::kAboutBlankURL), target->current_url());
EXPECT_EQ(url::kAboutScheme, target->current_url().scheme());
EXPECT_FALSE(target->current_origin().opaque());
EXPECT_EQ("b.com", target->current_origin().host());
EXPECT_EQ(url::kHttpScheme, target->current_origin().scheme());
EXPECT_EQ("Hi from b.com", EvalJs(target, "document.body.innerHTML"));
}
// Nested iframes, three origins: A(B(C)). Frame A navigates C to about:blank
// (via window.open). This should wind up in A's origin per the spec. Test fails
// because of http://crbug.com/564292
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
DISABLED_NavigateGrandchildToAboutBlank) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The leaf node (c.com) will be navigated. Its grandparent node (a.com) will
// initiate the navigation.
FrameTreeNode* target =
contents->GetPrimaryFrameTree().root()->child_at(0)->child_at(0);
RenderFrameHost* initiator_rfh = target->parent()->GetParent();
// Give the target a name.
EXPECT_TRUE(ExecJs(target, "window.name = 'target';"));
// Use window.open(about:blank), then poll the document for access.
EvalJsResult about_blank_origin =
EvalJs(initiator_rfh,
"new Promise((resolve) => {"
" var didNavigate = false;"
" var intervalID = setInterval(() => {"
" if (!didNavigate) {"
" didNavigate = true;"
" window.open('about:blank', 'target');"
" }"
" // May raise a SecurityError, that's expected.\n"
" try {"
" frames[0][0].document.write('Hi from ' + document.domain);"
" } catch (e) { return; }"
" clearInterval(intervalID);"
" resolve(frames[0][0].self.origin);"
" }, 16);"
"});");
EXPECT_EQ(target->current_origin(), about_blank_origin);
EXPECT_EQ(GURL(url::kAboutBlankURL), target->current_url());
EXPECT_EQ(url::kAboutScheme, target->current_url().scheme());
EXPECT_FALSE(target->current_origin().opaque());
EXPECT_EQ("a.com", target->current_origin().host());
EXPECT_EQ(url::kHttpScheme, target->current_origin().scheme());
EXPECT_EQ("Hi from a.com", EvalJs(target, "document.body.innerHTML"));
}
// Tests a cross-origin navigation to a data: URL. The main frame initiates this
// navigation on its grandchild. It should wind up in the main frame's process
// and have precursor origin of the main frame origin.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateGrandchildToDataUrl) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The leaf node (c.com) will be navigated. Its grandparent node (a.com) will
// initiate the navigation.
FrameTreeNode* target =
contents->GetPrimaryFrameTree().root()->child_at(0)->child_at(0);
RenderFrameHostImpl* initiator_rfh = target->parent()->GetParent();
// Give the target a name.
EXPECT_TRUE(ExecJs(target, "window.name = 'target';"));
// Navigate the target frame through the initiator frame.
{
TestFrameNavigationObserver observer(target);
EXPECT_TRUE(ExecJs(initiator_rfh,
"window.open('data:text/html,content', 'target');"));
observer.Wait();
}
url::Origin original_target_origin =
target->current_frame_host()->GetLastCommittedOrigin();
EXPECT_TRUE(original_target_origin.opaque());
EXPECT_EQ(original_target_origin.GetTupleOrPrecursorTupleIfOpaque(),
url::SchemeHostPort(main_url));
// Navigate the grandchild frame again cross-process to foo.com, then
// go back in session history. The frame should commit a new opaque origin,
// but it will still have the same precursor origin (the main frame origin).
{
TestFrameNavigationObserver observer(target);
EXPECT_TRUE(ExecJs(target, JsReplace("window.location = $1",
embedded_test_server()->GetURL(
"foo.com", "/title2.html"))));
observer.Wait();
}
EXPECT_NE(original_target_origin,
target->current_frame_host()->GetLastCommittedOrigin());
{
TestFrameNavigationObserver observer(target);
contents->GetController().GoBack();
observer.Wait();
}
url::Origin target_origin =
target->current_frame_host()->GetLastCommittedOrigin();
EXPECT_NE(target_origin, original_target_origin);
EXPECT_TRUE(target_origin.opaque());
EXPECT_EQ(target_origin.GetTupleOrPrecursorTupleIfOpaque(),
original_target_origin.GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(target_origin.GetTupleOrPrecursorTupleIfOpaque(),
url::SchemeHostPort(main_url));
}
// Ensures that iframe with srcdoc is always put in the same origin as its
// parent frame.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, ChildFrameWithSrcdoc) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = contents->GetPrimaryFrameTree().root();
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
std::string frame_origin = EvalJs(child, "self.origin;").ExtractString();
EXPECT_TRUE(
child->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
url::Origin::Create(GURL(frame_origin))));
EXPECT_FALSE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
url::Origin::Create(GURL(frame_origin))));
// Create a new iframe with srcdoc and add it to the main frame. It should
// be created in the same SiteInstance as the parent.
{
std::string script(
"var f = document.createElement('iframe');"
"f.srcdoc = 'some content';"
"document.body.appendChild(f)");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, script));
EXPECT_EQ(2U, root->child_count());
observer.Wait();
EXPECT_TRUE(root->child_at(1)->current_url().IsAboutSrcdoc());
EvalJsResult js_result = EvalJs(root->child_at(1), "self.origin");
EXPECT_EQ(root->current_frame_host()
->GetLastCommittedURL()
.DeprecatedGetOriginAsURL(),
GURL(js_result.ExtractString()));
EXPECT_NE(child->current_frame_host()
->GetLastCommittedURL()
.DeprecatedGetOriginAsURL(),
GURL(js_result.ExtractString()));
}
// Set srcdoc on the existing cross-site frame. It should navigate the frame
// back to the origin of the parent.
{
std::string script(
"var f = document.getElementById('child-0');"
"f.srcdoc = 'some content';");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, script));
observer.Wait();
EXPECT_TRUE(child->current_url().IsAboutSrcdoc());
EXPECT_EQ(root->current_frame_host()->GetLastCommittedOrigin().Serialize(),
EvalJs(child, "self.origin"));
}
}
// Ensure that sandbox flags are correctly set in the main frame when set by
// Content-Security-Policy header.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, SandboxFlagsSetForMainFrame) {
GURL main_url(embedded_test_server()->GetURL("/csp_sandboxed_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Verify that sandbox flags are set properly for the root FrameTreeNode and
// RenderFrameHost. Root frame is sandboxed with "allow-scripts".
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->active_sandbox_flags());
EXPECT_EQ(root->active_sandbox_flags(),
root->current_frame_host()->active_sandbox_flags());
// Verify that child frames inherit sandbox flags from the root. First frame
// has no explicitly set flags of its own, and should inherit those from the
// root. Second frame is completely sandboxed.
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->child_at(0)->active_sandbox_flags());
EXPECT_EQ(root->child_at(0)->active_sandbox_flags(),
root->child_at(0)->current_frame_host()->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(1)->active_sandbox_flags());
EXPECT_EQ(root->child_at(1)->active_sandbox_flags(),
root->child_at(1)->current_frame_host()->active_sandbox_flags());
// Navigating the main frame to a different URL should clear sandbox flags.
GURL unsandboxed_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root, unsandboxed_url));
// Verify that sandbox flags are cleared properly for the root FrameTreeNode
// and RenderFrameHost.
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->current_frame_host()->active_sandbox_flags());
}
// Ensure that sandbox flags are correctly set when child frames are created.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, SandboxFlagsSetForChildFrames) {
GURL main_url(embedded_test_server()->GetURL("/sandboxed_frames.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Verify that sandbox flags are set properly for all FrameTreeNodes.
// First frame is completely sandboxed; second frame uses "allow-scripts",
// which resets both SandboxFlags::Scripts and
// SandboxFlags::AutomaticFeatures bits per blink::parseSandboxPolicy(), and
// third frame has "allow-scripts allow-same-origin".
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kOrigin,
root->child_at(2)->effective_frame_policy().sandbox_flags);
// Sandboxed frames should set a unique origin unless they have the
// "allow-same-origin" directive.
EXPECT_EQ("null", root->child_at(0)->current_origin().Serialize());
EXPECT_EQ("null", root->child_at(1)->current_origin().Serialize());
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->child_at(2)->current_origin().Serialize() + "/");
// Navigating to a different URL should not clear sandbox flags.
GURL frame_url(embedded_test_server()->GetURL("/title1.html"));
NavigateFrameToURL(root->child_at(0), frame_url);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(0)->effective_frame_policy().sandbox_flags);
}
// Ensure that sandbox flags are correctly set in the child frames when set by
// Content-Security-Policy header, and in combination with the sandbox iframe
// attribute.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
SandboxFlagsSetByCSPForChildFrames) {
GURL main_url(embedded_test_server()->GetURL("/sandboxed_frames_csp.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Verify that sandbox flags are set properly for all FrameTreeNodes.
// First frame has no iframe sandbox flags, but the framed document is served
// with a CSP header which sets "allow-scripts", "allow-popups" and
// "allow-pointer-lock".
// Second frame is sandboxed with "allow-scripts", "allow-pointer-lock" and
// "allow-orientation-lock", and the framed document is also served with a CSP
// header which uses "allow-popups" and "allow-pointer-lock". The resulting
// sandbox for the frame should only have "allow-pointer-lock".
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->active_sandbox_flags());
EXPECT_EQ(root->active_sandbox_flags(),
root->current_frame_host()->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(
network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kPopups &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kTopNavigationToCustomProtocols,
root->child_at(0)->active_sandbox_flags());
EXPECT_EQ(root->child_at(0)->active_sandbox_flags(),
root->child_at(0)->current_frame_host()->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kOrientationLock,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock,
root->child_at(1)->active_sandbox_flags());
EXPECT_EQ(root->child_at(1)->active_sandbox_flags(),
root->child_at(1)->current_frame_host()->active_sandbox_flags());
// Navigating to a different URL *should* clear CSP-set sandbox flags, but
// should retain those flags set by the frame owner.
GURL frame_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->child_at(0)->active_sandbox_flags());
EXPECT_EQ(root->child_at(0)->active_sandbox_flags(),
root->child_at(0)->current_frame_host()->active_sandbox_flags());
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), frame_url));
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kOrientationLock,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kOrientationLock,
root->child_at(1)->active_sandbox_flags());
EXPECT_EQ(root->child_at(1)->active_sandbox_flags(),
root->child_at(1)->current_frame_host()->active_sandbox_flags());
}
// Ensure that a popup opened from a subframe sets its opener to the subframe's
// FrameTreeNode, and that the opener is cleared if the subframe is destroyed.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, SubframeOpenerSetForNewWindow) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Open a new window from a subframe.
ShellAddedObserver new_shell_observer;
GURL popup_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(
ExecJs(root->child_at(0), JsReplace("window.open($1);", popup_url)));
Shell* new_shell = new_shell_observer.GetShell();
WebContents* new_contents = new_shell->web_contents();
EXPECT_TRUE(WaitForLoadStop(new_contents));
// Check that the new window's opener points to the correct subframe on
// original window.
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_contents)->GetPrimaryFrameTree().root();
EXPECT_EQ(root->child_at(0), popup_root->opener());
// Close the original window. This should clear the new window's opener.
shell()->Close();
EXPECT_EQ(nullptr, popup_root->opener());
}
// Tests that the user activation bits get cleared when a same-site document is
// installed in the frame.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
ClearUserActivationForNewDocument) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Set the user activation bits.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
// Install a new same-site document to check the clearing of user activation
// bits.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
}
class CrossProcessFrameTreeBrowserTest : public ContentBrowserTest {
public:
CrossProcessFrameTreeBrowserTest() = default;
CrossProcessFrameTreeBrowserTest(const CrossProcessFrameTreeBrowserTest&) =
delete;
CrossProcessFrameTreeBrowserTest& operator=(
const CrossProcessFrameTreeBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
// Ensure that we can complete a cross-process subframe navigation.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
CreateCrossProcessSubframeProxies) {
GURL main_url(embedded_test_server()->GetURL("/site_per_process_main.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// There should not be a proxy for the root's own SiteInstance.
SiteInstanceImpl* root_instance =
root->current_frame_host()->GetSiteInstance();
EXPECT_FALSE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(root_instance->group()));
// Load same-site page into iframe.
GURL http_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), http_url));
// Load cross-site page into iframe.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
// Ensure that we have created a new process for the subframe.
ASSERT_EQ(2U, root->child_count());
FrameTreeNode* child = root->child_at(0);
SiteInstanceImpl* child_instance =
child->current_frame_host()->GetSiteInstance();
RenderViewHost* rvh = child->current_frame_host()->render_view_host();
RenderProcessHost* rph = child->current_frame_host()->GetProcess();
EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost(),
rvh);
EXPECT_NE(shell()->web_contents()->GetSiteInstance(), child_instance);
EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), rph);
// Ensure that the root node has a proxy for the child node's SiteInstance.
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(child_instance->group()));
// Also ensure that the child has a proxy for the root node's SiteInstance.
EXPECT_TRUE(child->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(root_instance->group()));
// The nodes should not have proxies for their own SiteInstance.
EXPECT_FALSE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(root_instance->group()));
EXPECT_FALSE(child->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(child_instance->group()));
// Ensure that the RenderViews and RenderFrames are all live.
EXPECT_TRUE(
root->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(
child->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
}
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
OriginSetOnNavigations) {
GURL main_url(embedded_test_server()->GetURL("/site_per_process_main.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(root->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
// First frame is an about:blank frame. Check that its origin is correctly
// inherited from the parent.
EXPECT_EQ(root->child_at(0)->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
// Second frame loads a same-site page. Its origin should also be the same
// as the parent.
EXPECT_EQ(root->child_at(1)->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
// Load cross-site page into the first frame.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize() + '/',
cross_site_url.DeprecatedGetOriginAsURL().spec());
// The root's origin shouldn't have changed.
EXPECT_EQ(root->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
{
GURL data_url("data:text/html,foo");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace("window.location = $1", data_url)));
observer.Wait();
}
// Navigating to a data URL should set a unique origin. This is represented
// as "null" per RFC 6454. A frame navigating itself to a data: URL does not
// require a process transfer, but should retain the original origin
// as its precursor.
EXPECT_EQ(root->child_at(1)->current_origin().Serialize(), "null");
EXPECT_TRUE(root->child_at(1)->current_origin().opaque());
ASSERT_EQ(
url::SchemeHostPort(main_url),
root->child_at(1)->current_origin().GetTupleOrPrecursorTupleIfOpaque())
<< "Expected the precursor origin to be preserved; should be the "
"initiator of a data: navigation.";
// Adding an <iframe sandbox srcdoc=> frame should result in a unique origin
// that is different-origin from its data: URL parent.
{
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(0U, root->child_at(1)->child_count());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace(
R"(
var iframe = document.createElement('iframe');
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.srcdoc = $1;
document.body.appendChild(iframe);
)",
"<html><body>This sandboxed doc should "
"be different-origin.</body></html>")));
observer.Wait();
ASSERT_EQ(1U, root->child_at(1)->child_count());
}
url::Origin root_origin = root->current_origin();
url::Origin child_1 = root->child_at(1)->current_origin();
url::Origin child_1_0 = root->child_at(1)->child_at(0)->current_origin();
EXPECT_FALSE(root_origin.opaque());
EXPECT_TRUE(child_1.opaque());
EXPECT_TRUE(child_1_0.opaque());
EXPECT_NE(child_1, child_1_0);
EXPECT_EQ(url::SchemeHostPort(main_url),
root_origin.GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(url::SchemeHostPort(main_url),
child_1.GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(url::SchemeHostPort(main_url),
child_1_0.GetTupleOrPrecursorTupleIfOpaque());
{
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(1U, root->child_at(1)->child_count());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace(
R"(
var iframe = document.createElement('iframe');
iframe.srcdoc = $1;
document.body.appendChild(iframe);
)",
"<html><body>This srcdoc document should "
"be same-origin.</body></html>")));
observer.Wait();
ASSERT_EQ(2U, root->child_at(1)->child_count());
}
EXPECT_EQ(root_origin, root->current_origin());
EXPECT_EQ(child_1, root->child_at(1)->current_origin());
EXPECT_EQ(child_1_0, root->child_at(1)->child_at(0)->current_origin());
url::Origin child_1_1 = root->child_at(1)->child_at(1)->current_origin();
EXPECT_EQ(child_1, child_1_1);
EXPECT_NE(child_1_0, child_1_1);
{
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(2U, root->child_at(1)->child_count());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace(
R"(
var iframe = document.createElement('iframe');
iframe.src = 'data:text/html;base64,' + btoa($1);
document.body.appendChild(iframe);
)",
"<html><body>This data: doc should be "
"different-origin.</body></html>")));
observer.Wait();
ASSERT_EQ(3U, root->child_at(1)->child_count());
}
EXPECT_EQ(root_origin, root->current_origin());
EXPECT_EQ(child_1, root->child_at(1)->current_origin());
EXPECT_EQ(child_1_0, root->child_at(1)->child_at(0)->current_origin());
EXPECT_EQ(child_1_1, root->child_at(1)->child_at(1)->current_origin());
url::Origin child_1_2 = root->child_at(1)->child_at(2)->current_origin();
EXPECT_NE(child_1, child_1_2);
EXPECT_NE(child_1_0, child_1_2);
EXPECT_NE(child_1_1, child_1_2);
EXPECT_EQ(url::SchemeHostPort(main_url),
child_1_2.GetTupleOrPrecursorTupleIfOpaque());
// If the parent navigates its child to a data URL, it should transfer
// to the parent's process, and the precursor origin should track the
// parent's origin.
{
GURL data_url("data:text/html,foo2");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, JsReplace("frames[0].location = $1", data_url)));
observer.Wait();
EXPECT_EQ(data_url, root->child_at(0)->current_url());
}
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(), "null");
EXPECT_TRUE(root->child_at(0)->current_origin().opaque());
EXPECT_EQ(
url::SchemeHostPort(main_url),
root->child_at(0)->current_origin().GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(root->current_frame_host()->GetProcess(),
root->child_at(0)->current_frame_host()->GetProcess());
}
// Test to verify that a blob: URL that is created by a unique opaque origin
// will correctly set the origin_to_commit on a session history navigation.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
OriginForBlobUrlsFromUniqueOpaqueOrigin) {
// Start off with a navigation to data: URL in the main frame. It should
// result in a unique opaque origin without any precursor information.
GURL data_url("data:text/html,foo<iframe id='child' src='" +
embedded_test_server()->GetURL("/title1.html").spec() +
"'></iframe>");
EXPECT_TRUE(NavigateToURL(shell(), data_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(root->current_origin().opaque());
EXPECT_FALSE(
root->current_origin().GetTupleOrPrecursorTupleIfOpaque().IsValid());
EXPECT_EQ(1UL, root->child_count());
FrameTreeNode* child = root->child_at(0);
// Create a blob: URL and navigate the child frame to it.
std::string html = "<html><body>This is blob content.</body></html>";
std::string script = JsReplace(
"var blob = new Blob([$1], {type: 'text/html'});"
"var blob_url = URL.createObjectURL(blob);"
"document.getElementById('child').src = blob_url;"
"blob_url;",
html);
GURL blob_url;
{
TestFrameNavigationObserver observer(child);
blob_url = GURL(EvalJs(root, script).ExtractString());
observer.Wait();
EXPECT_EQ(blob_url, child->current_frame_host()->GetLastCommittedURL());
}
// We expect the frame to have committed in an opaque origin which contains
// the same precursor information - none.
url::Origin blob_origin = child->current_origin();
EXPECT_TRUE(blob_origin.opaque());
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
blob_origin.GetTupleOrPrecursorTupleIfOpaque());
EXPECT_FALSE(
child->current_origin().GetTupleOrPrecursorTupleIfOpaque().IsValid());
// Navigate the frame away to any web URL.
{
GURL url(embedded_test_server()->GetURL("/title2.html"));
TestFrameNavigationObserver observer(child);
EXPECT_TRUE(ExecJs(child, JsReplace("window.location = $1", url)));
observer.Wait();
EXPECT_EQ(url, child->current_frame_host()->GetLastCommittedURL());
}
EXPECT_FALSE(child->current_origin().opaque());
EXPECT_TRUE(shell()->web_contents()->GetController().CanGoBack());
EXPECT_EQ(3, shell()->web_contents()->GetController().GetEntryCount());
EXPECT_EQ(
2, shell()->web_contents()->GetController().GetLastCommittedEntryIndex());
// Verify the blob URL still exists in the main frame, which keeps it alive
// allowing a session history navigation back to succeed.
EXPECT_EQ(blob_url, GURL(EvalJs(root, "blob_url;").ExtractString()));
// Now navigate back in session history. It should successfully go back to
// the blob: URL. The child frame won't be reusing the exact same origin it
// used before, but it will commit a new opaque origin which will still have
// no precursor information.
{
TestFrameNavigationObserver observer(child);
shell()->web_contents()->GetController().GoBack();
observer.Wait();
}
EXPECT_EQ(blob_url, child->current_frame_host()->GetLastCommittedURL());
EXPECT_TRUE(child->current_origin().opaque());
EXPECT_NE(blob_origin, child->current_origin());
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
child->current_origin().GetTupleOrPrecursorTupleIfOpaque());
EXPECT_FALSE(
child->current_origin().GetTupleOrPrecursorTupleIfOpaque().IsValid());
}
// Test to verify that about:blank iframe, which is a child of a sandboxed
// iframe is not considered same origin, but precursor information is preserved
// in its origin.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
AboutBlankSubframeInSandboxedFrame) {
// Start off by navigating to a page with sandboxed iframe, which allows
// script execution.
GURL main_url(
embedded_test_server()->GetURL("/sandboxed_main_frame_script.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1UL, root->child_count());
FrameTreeNode* child = root->child_at(0);
// Navigate the frame to data: URL to cause it to have an opaque origin that
// is derived from the |main_url| origin.
GURL data_url("data:text/html,<html><body>foo</body></html>");
{
TestFrameNavigationObserver observer(child);
EXPECT_TRUE(ExecJs(root, JsReplace("frames[0].location = $1", data_url)));
observer.Wait();
EXPECT_EQ(data_url, child->current_frame_host()->GetLastCommittedURL());
}
// Add an about:blank iframe to the data: frame, which should not inherit the
// origin, but should preserve the precursor information.
{
EXPECT_TRUE(ExecJs(child,
"var f = document.createElement('iframe');"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1UL, child->child_count());
FrameTreeNode* grandchild = child->child_at(0);
EXPECT_TRUE(grandchild->current_origin().opaque());
EXPECT_EQ(GURL(url::kAboutBlankURL),
grandchild->current_frame_host()->GetLastCommittedURL());
// The origin of the data: document should have precursor information matching
// the main frame origin.
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
child->current_origin().GetTupleOrPrecursorTupleIfOpaque());
// The same should hold also for the about:blank subframe of the data: frame.
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
grandchild->current_origin().GetTupleOrPrecursorTupleIfOpaque());
// The about:blank document should not be able to access its parent, as they
// are considered cross origin due to the sandbox flags on the parent.
EXPECT_FALSE(ExecJs(grandchild, "window.parent.foo = 'bar';"));
EXPECT_NE(child->current_origin(), grandchild->current_origin());
}
// Ensure that a popup opened from a sandboxed main frame inherits sandbox flags
// from its opener.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
SandboxFlagsSetForNewWindow) {
GURL main_url(
embedded_test_server()->GetURL("/sandboxed_main_frame_script.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Open a new window from the main frame.
GURL popup_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
Shell* new_shell = OpenPopup(root->current_frame_host(), popup_url, "");
EXPECT_TRUE(new_shell);
WebContents* new_contents = new_shell->web_contents();
// Check that the new window's sandbox flags correctly reflect the opener's
// flags. Main frame sets allow-popups, allow-pointer-lock and allow-scripts.
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_contents)->GetPrimaryFrameTree().root();
network::mojom::WebSandboxFlags main_frame_sandbox_flags =
root->current_frame_host()->active_sandbox_flags();
EXPECT_EQ(
network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kPopups &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kTopNavigationToCustomProtocols,
main_frame_sandbox_flags);
EXPECT_EQ(main_frame_sandbox_flags,
popup_root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(main_frame_sandbox_flags, popup_root->active_sandbox_flags());
EXPECT_EQ(main_frame_sandbox_flags,
popup_root->current_frame_host()->active_sandbox_flags());
}
// Tests that the user activation bits get cleared when a cross-site document is
// installed in the frame.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
ClearUserActivationForNewDocument) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Set the user activation bits.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
// Install a new cross-site document to check the clearing of user activation
// bits.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
}
class BrowserContextGroupSwapFrameTreeBrowserTest : public ContentBrowserTest {
public:
BrowserContextGroupSwapFrameTreeBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
net::test_server::RegisterDefaultHandlers(&https_server_);
ASSERT_TRUE(https_server_.Start());
}
net::EmbeddedTestServer* https_server() { return &https_server_; }
public:
net::EmbeddedTestServer https_server_;
};
// Force a race between when the RenderViewHostImpl's main frame is running
// the unload handlers and when a new navigation occurs that tries to
// reuse a RenderViewHostImpl.
IN_PROC_BROWSER_TEST_F(BrowserContextGroupSwapFrameTreeBrowserTest,
NavigateAndGoBack) {
GURL main_url(https_server()->GetURL("a.test", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
web_contents->GetPrimaryMainFrame()->DoNotDeleteForTesting();
DisableBFCacheForRFHForTesting(
web_contents->GetPrimaryFrameTree().root()->current_frame_host());
// Load a page with COOP set to force the browsing context group swap
// and clears out old proxies.
GURL new_main_url(https_server()->GetURL(
"b.test", "/set-header?Cross-Origin-Opener-Policy: same-origin"));
EXPECT_TRUE(NavigateToURL(shell(), new_main_url));
TestNavigationObserver back_load_observer(web_contents);
web_contents->GetController().GoBack();
back_load_observer.Wait();
}
// FrameTreeBrowserTest variant where we isolate http://*.is, Iceland's top
// level domain. This is an analogue to isolating extensions, which we can use
// inside content_browsertests, where extensions don't exist. Iceland, like an
// extension process, is a special place with magical powers; we want to protect
// it from outsiders.
class IsolateIcelandFrameTreeBrowserTest : public ContentBrowserTest {
public:
IsolateIcelandFrameTreeBrowserTest() = default;
IsolateIcelandFrameTreeBrowserTest(
const IsolateIcelandFrameTreeBrowserTest&) = delete;
IsolateIcelandFrameTreeBrowserTest& operator=(
const IsolateIcelandFrameTreeBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
// Blink suppresses navigations to blob URLs of origins different from the
// frame initiating the navigation. We disable those checks for this test,
// to test what happens in a compromise scenario.
command_line->AppendSwitch(switches::kDisableWebSecurity);
// ProcessSwitchForIsolatedBlob test below requires that one of URLs used in
// the test (blob:http://b.is/) belongs to an isolated origin.
command_line->AppendSwitchASCII(switches::kIsolateOrigins, "http://b.is/");
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
// Regression test for https://crbug.com/644966
IN_PROC_BROWSER_TEST_F(IsolateIcelandFrameTreeBrowserTest,
ProcessSwitchForIsolatedBlob) {
// Set up an iframe.
WebContents* contents = shell()->web_contents();
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents)->GetPrimaryFrameTree().root();
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// The navigation targets an invalid blob url; that's intentional to trigger
// an error response. The response should commit in a process dedicated to
// http://b.is or error pages, depending on policy.
EXPECT_EQ(
"done",
EvalJs(
root,
"new Promise((resolve) => {"
" var iframe_element = document.getElementsByTagName('iframe')[0];"
" iframe_element.onload = () => resolve('done');"
" iframe_element.src = 'blob:http://b.is/';"
"});"));
EXPECT_TRUE(WaitForLoadStop(contents));
// Make sure we did a process transfer back to "b.is".
const std::string kExpectedSiteURL =
AreStrictSiteInstancesEnabled()
? "http://a.com/"
: SiteInstanceImpl::GetDefaultSiteURL().spec();
const std::string kExpectedSubframeSiteURL =
SiteIsolationPolicy::IsErrorPageIsolationEnabled(/*in_main_frame*/ false)
? "chrome-error://chromewebdata/"
: "http://b.is/";
EXPECT_EQ(base::StringPrintf(" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
"Where A = %s\n"
" B = %s",
kExpectedSiteURL.c_str(),
kExpectedSubframeSiteURL.c_str()),
DepictFrameTree(*root));
}
class FrameTreeCredentiallessIframeBrowserTest : public FrameTreeBrowserTest {
public:
FrameTreeCredentiallessIframeBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kEnableBlinkTestFeatures);
}
};
// Tests the mojo propagation of the 'credentialless' attribute to the browser.
IN_PROC_BROWSER_TEST_F(FrameTreeCredentiallessIframeBrowserTest,
AttributeIsPropagatedToBrowser) {
GURL main_url(embedded_test_server()->GetURL("/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Not setting the attribute => the iframe is not credentialless.
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('iframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
EXPECT_FALSE(root->child_at(0)->Credentialless());
EXPECT_EQ(false, EvalJs(root->child_at(0)->current_frame_host(),
"window.credentialless"));
// Setting the attribute on the iframe element makes the iframe
// credentialless.
EXPECT_TRUE(ExecJs(root,
"var d = document.createElement('div');"
"d.innerHTML = '<iframe credentialless></iframe>';"
"document.body.appendChild(d);"));
EXPECT_EQ(2U, root->child_count());
EXPECT_TRUE(root->child_at(1)->Credentialless());
EXPECT_EQ(true, EvalJs(root->child_at(1)->current_frame_host(),
"window.credentialless"));
// Setting the attribute via javascript works.
EXPECT_TRUE(ExecJs(root,
"var g = document.createElement('iframe');"
"g.credentialless = true;"
"document.body.appendChild(g);"));
EXPECT_EQ(3U, root->child_count());
EXPECT_TRUE(root->child_at(2)->Credentialless());
EXPECT_EQ(true, EvalJs(root->child_at(2)->current_frame_host(),
"window.credentialless"));
EXPECT_TRUE(ExecJs(root, "g.credentialless = false;"));
EXPECT_FALSE(root->child_at(2)->Credentialless());
EXPECT_EQ(true, EvalJs(root->child_at(2)->current_frame_host(),
"window.credentialless"));
EXPECT_TRUE(ExecJs(root, "g.credentialless = true;"));
EXPECT_TRUE(root->child_at(2)->Credentialless());
EXPECT_EQ(true, EvalJs(root->child_at(2)->current_frame_host(),
"window.credentialless"));
}
class FrameTreeLastSuccessfulOriginBrowserTest : public FrameTreeBrowserTest {
public:
FrameTreeLastSuccessfulOriginBrowserTest() = default;
};
IN_PROC_BROWSER_TEST_F(FrameTreeLastSuccessfulOriginBrowserTest,
SuccessfulNavigation) {
GURL main_url(embedded_test_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// After a successful navigation, the "last committed origin" and the
// "last successfully committed origin" should be the same.
EXPECT_FALSE(root->current_origin().opaque());
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque().host(),
"a.test");
EXPECT_EQ(root->last_successful_origin(), root->current_origin());
}
IN_PROC_BROWSER_TEST_F(FrameTreeLastSuccessfulOriginBrowserTest,
FailedNavigationAfterSuccessfulNavigation) {
// First, perform a successful navigation, so that the root FrameTreeNode has
// a non-opaque `last_successful_origin()`.
GURL main_url(embedded_test_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(root->last_successful_origin(), root->current_origin());
url::Origin initial_successful_origin = root->last_successful_origin();
// Now, navigate to a malformed URL to force an error page.
TestFrameNavigationObserver navigation_observer(root);
EXPECT_TRUE(ExecJs(root, R"(location.href = 'https://hello';)"));
navigation_observer.Wait();
EXPECT_FALSE(navigation_observer.last_navigation_succeeded());
EXPECT_TRUE(root->current_frame_host()->IsErrorDocument());
// The new error document should have an opaque origin, but the frame's
// `last_successful_origin()` should remain the same as the initial origin
// from the first successful navigation.
EXPECT_TRUE(root->current_origin().opaque());
EXPECT_NE(root->last_successful_origin(), root->current_origin());
EXPECT_EQ(root->last_successful_origin(), initial_successful_origin);
}
IN_PROC_BROWSER_TEST_F(FrameTreeLastSuccessfulOriginBrowserTest,
CorrectStateForNewMainFrame) {
// Don't navigate the root frame to anything. It should have an empty URL with
// an opaque origin.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(root->current_frame_host()->GetLastCommittedURL().is_empty());
EXPECT_TRUE(root->current_origin().opaque());
EXPECT_EQ(root->last_successful_origin(), root->current_origin());
}
IN_PROC_BROWSER_TEST_F(FrameTreeLastSuccessfulOriginBrowserTest,
CorrectStateForNewSubframe) {
GURL main_url(embedded_test_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"let frame = document.createElement('iframe');"
"document.body.appendChild(frame);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* new_frame = root->child_at(0);
// Our new subframe hasn't been navigated yet, so its current URL is
// about:blank. It has also inherited the origin of its creator, and the
// last successful origin should be the same.
EXPECT_EQ(new_frame->current_url(), url::kAboutBlankURL);
EXPECT_EQ(new_frame->current_origin().host(), "a.test");
EXPECT_EQ(new_frame->last_successful_origin(), new_frame->current_origin());
}
INSTANTIATE_TEST_SUITE_P(
,
FrameTreeBrowserWithDiscardTest,
::testing::Values(false, true),
[](const ::testing::TestParamInfo<
FrameTreeBrowserWithDiscardTest::ParamType>& info) {
return info.param ? "KeepAlive" : "NoKeepAlive";
});
INSTANTIATE_TEST_SUITE_P(
,
FrameTreeDiscardPendingNavigationTest,
::testing::Values(false, true),
[](const ::testing::TestParamInfo<
FrameTreeDiscardPendingNavigationTest::ParamType>& info) {
return info.param ? "KeepAlive" : "NoKeepAlive";
});
INSTANTIATE_TEST_SUITE_P(
,
DedicatedWorkerFrameTreeBrowserTest,
::testing::Values(false, true),
[](const ::testing::TestParamInfo<
DedicatedWorkerFrameTreeBrowserTest::ParamType>& info) {
return info.param ? "KeepAlive" : "NoKeepAlive";
});
} // namespace content