blob: 38e87dd1d9e7a2112e2396741639f08712d410c2 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/back_forward_cache/back_forward_cache_disable.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/messaging/message_service.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "net/dns/mock_host_resolver.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
#include "third_party/blink/public/mojom/navigation/renderer_eviction_reason.mojom-shared.h"
namespace extensions {
using ContextType = ExtensionBrowserTest::ContextType;
struct TestParams {
bool enable_disconnect_message_port_on_bfcache;
ContextType context_type;
};
class ExtensionBackForwardCacheBrowserTest
: public ExtensionBrowserTest,
public ::testing::WithParamInterface<TestParams> {
public:
ExtensionBackForwardCacheBrowserTest()
: ExtensionBrowserTest(GetParam().context_type) {
auto enabled_features =
content::GetDefaultEnabledBackForwardCacheFeaturesForTesting(
{{features::kBackForwardCache, {}}});
auto disabled_features =
content::GetDefaultDisabledBackForwardCacheFeaturesForTesting();
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
enabled_features.push_back(
{features::kDisconnectExtensionMessagePortWhenPageEntersBFCache, {}});
} else {
disabled_features.push_back(
features::kDisconnectExtensionMessagePortWhenPageEntersBFCache);
}
feature_list_.InitWithFeaturesAndParameters(enabled_features,
disabled_features);
}
bool IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled() {
return GetParam().enable_disconnect_message_port_on_bfcache;
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ExtensionBrowserTest::SetUpOnMainThread();
}
content::RenderFrameHost* current_main_frame_host() {
return web_contents()->GetPrimaryMainFrame();
}
void RunChromeRuntimeConnectTest() {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
content::TitleWatcher title_watcher(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
const int kMessagingBucket =
(static_cast<int>(content::BackForwardCache::DisabledSource::kEmbedder)
<< 16) +
static_cast<int>(back_forward_cache::DisabledReasonId::
kExtensionSentMessageToCachedFrame);
std::string action = base::StringPrintf(
R"HTML(
var p = chrome.runtime.connect('%s');
p.onMessage.addListener((m) => {document.title = m; });
)HTML",
extension->id().c_str());
EXPECT_TRUE(ExecJs(render_frame_host_a.get(), action));
// 2) Wait for the message port to be connected.
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Expect that a channel is open.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
EXPECT_EQ(0, histogram_tester_.GetBucketCount(
"BackForwardCache.HistoryNavigationOutcome."
"DisabledForRenderFrameHostReason2",
kMessagingBucket));
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// Expect that `render_frame_host_a` is cached.
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// The channel should remain open if
// DisconnectExtensionMessagePortWhenPageEntersBFCache is disabled.
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
} else {
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// Send a message to the port.
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(
extension->id(), "port.postMessage('bye');"));
// `render_frame_host_a` should be destroyed now, and the channel should
// be closed.
ASSERT_TRUE(render_frame_host_a.WaitUntilRenderFrameDeleted());
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
}
// 4) Go back to A.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
web_contents->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents));
int expected_count = 0;
if (!IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
// If `DisconnectExtensionMessagePortWhenPageEntersBFCache` is
// disabled, validate that the not restored reason is
// `kExtensionSentMessageToCachedFrame` due to a message being sent to an
// inactive frame.
expected_count = 1;
}
EXPECT_EQ(expected_count, histogram_tester_.GetBucketCount(
"BackForwardCache.HistoryNavigationOutcome."
"DisabledForRenderFrameHostReason2",
kMessagingBucket));
}
void ExpectTitleChangeSuccess(const Extension& extension, const char* title) {
const std::string script = base::StringPrintf(R"(
chrome.tabs.executeScript({
code: "document.title='%s'"
});
)",
title);
ExecuteScriptInBackgroundPageNoWait(extension.id(), script);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
std::u16string title16(base::UTF8ToUTF16(title));
content::TitleWatcher title_watcher(web_contents, title16);
EXPECT_EQ(title16, title_watcher.WaitAndGetTitle());
}
void ExpectTitleChangeFail(const Extension& extension) {
static constexpr char kScript[] =
R"(
chrome.tabs.executeScript({code: "document.title='fail'"},
() => {
if (chrome.runtime.lastError) {
chrome.test.sendScriptResult(
chrome.runtime.lastError.message);
} else {
chrome.test.sendScriptResult("Unexpected success");
}
});
)";
EXPECT_EQ(manifest_errors::kCannotAccessPage,
ExecuteScriptInBackgroundPage(extension.id(), kScript));
std::u16string title;
ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &title));
EXPECT_NE(u"fail", title);
}
content::WebContents* web_contents() const {
return browser()->tab_strip_model()->GetActiveWebContents();
}
protected:
base::HistogramTester histogram_tester_;
private:
base::test::ScopedFeatureList feature_list_;
};
// These tests use chrome.tabs.executeScript, so the SW versions of the tests
// must still be run with MV2. See crbug.com/332328868.
INSTANTIATE_TEST_SUITE_P(EventPageAndFalse,
ExtensionBackForwardCacheBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = false,
.context_type = ContextType::kEventPage}));
INSTANTIATE_TEST_SUITE_P(ServiceWorkerAndFalse,
ExtensionBackForwardCacheBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = false,
.context_type = ContextType::kServiceWorkerMV2}));
INSTANTIATE_TEST_SUITE_P(EventPageAndTrue,
ExtensionBackForwardCacheBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = true,
.context_type = ContextType::kEventPage}));
INSTANTIATE_TEST_SUITE_P(ServiceWorkerAndTrue,
ExtensionBackForwardCacheBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = true,
.context_type = ContextType::kServiceWorkerMV2}));
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest, ScriptAllowed) {
ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script")));
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Navigate to B.
content::RenderFrameHostWrapper render_frame_host_b(
ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_NE(render_frame_host_a.get(), render_frame_host_b.get());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
}
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest, CSSAllowed) {
ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_css")));
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Navigate to B.
content::RenderFrameHostWrapper render_frame_host_b(
ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_NE(render_frame_host_a.get(), render_frame_host_b.get());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
}
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
UnloadExtensionFlushCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// Load the extension so we can unload it later.
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_css"));
ASSERT_TRUE(extension);
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Navigate to B.
content::RenderFrameHostWrapper render_frame_host_b(
ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_NE(render_frame_host_a.get(), render_frame_host_b.get());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// Now unload the extension after something is in the cache.
UnloadExtension(extension->id());
// Expect that `render_frame_host_a` is destroyed as it should be cleared from
// the cache.
EXPECT_TRUE(render_frame_host_a.WaitUntilRenderFrameDeleted());
}
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
LoadExtensionFlushCache) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Navigate to B.
content::RenderFrameHostWrapper render_frame_host_b(
ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_NE(render_frame_host_a.get(), render_frame_host_b.get());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// Now load the extension after something is in the cache.
ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_css")));
// Expect that `render_frame_host_a` is destroyed as it should be cleared from
// the cache.
EXPECT_TRUE(render_frame_host_a.WaitUntilRenderFrameDeleted());
}
// Test if the chrome.runtime.connect API is called, the page is prevented from
// entering bfcache.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeRuntimeConnectUsage) {
RunChromeRuntimeConnectTest();
}
// Test that we correctly clear the bfcache disable reasons on a same-origin
// cross document navigation for a document with an active channel, allowing
// the frame to be bfcached subsequently.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeRuntimeConnectUsageInIframeWithIframeNavigation) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a = embedded_test_server()->GetURL("a.com", "/iframe.html");
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper primary_render_frame_host(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
content::TitleWatcher title_watcher(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
content::RenderFrameHost* child =
ChildFrameAt(primary_render_frame_host.get(), 0);
std::string action = base::StringPrintf(
R"HTML(
var p = chrome.runtime.connect('%s');
p.onMessage.addListener((m) => {window.top.document.title = m; });
)HTML",
extension->id().c_str());
ASSERT_TRUE(ExecJs(child, action));
// 2) Wait for the message port to be connected.
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Expect that a channel is open.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate the iframe.
GURL iframe_url = embedded_test_server()->GetURL("a.com", "/title2.html");
EXPECT_TRUE(NavigateToURLFromRenderer(child, iframe_url));
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
// 4) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 5) Expect that A is in the back forward cache.
EXPECT_FALSE(primary_render_frame_host.IsDestroyed());
EXPECT_EQ(primary_render_frame_host->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
}
// Test that the page can enter BFCache with an active channel created from the
// iframe.
IN_PROC_BROWSER_TEST_P(
ExtensionBackForwardCacheBrowserTest,
ChromeRuntimeConnectUsageInIframeWithoutIframeNavigation) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a = embedded_test_server()->GetURL("a.com", "/iframe.html");
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper primary_render_frame_host(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
content::TitleWatcher title_watcher(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
content::RenderFrameHost* child =
ChildFrameAt(primary_render_frame_host.get(), 0);
std::string action = base::StringPrintf(
R"JS(
var p = chrome.runtime.connect('%s');
p.onMessage.addListener((m) => {window.top.document.title = m; });
)JS",
extension->id().c_str());
ASSERT_TRUE(ExecJs(child, action));
// 2) Wait for the message port to be connected.
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Expect that a channel is open.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate to B, and the channel is still open if
// `DisconnectExtensionMessagePortWhenPageEntersBFCache` is not enabled.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
} else {
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
}
// 4) Expect that A is in the back forward cache.
EXPECT_FALSE(primary_render_frame_host.IsDestroyed());
EXPECT_EQ(primary_render_frame_host->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
}
// Test that the page can enter BFCache with an active channel that's created
// from the extension background with two receivers from different frames.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeTabsConnectWithMultipleReceivers) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script_all_frames"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/iframe.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper primary_render_frame_host(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Create channel from the extension background.
static constexpr char kScript[] =
R"JS(
var p;
var countConnected = 0;
chrome.tabs.query({}, (t) => {
p = chrome.tabs.connect(t[0].id);
p.onMessage.addListener(
(m) => {
if (m == 'connected') {
countConnected++;
if (countConnected == 2) {
chrome.test.sendScriptResult('connected twice');
}
}
});
});
)JS";
// The background should receives two "connected" messages from different
// frames.
EXPECT_EQ("connected twice",
ExecuteScriptInBackgroundPage(extension->id(), kScript));
// Even though there are two ports from the receiver end, there is still one
// channel.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 4) Expect that A is in the back forward cache.
EXPECT_EQ(primary_render_frame_host->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
} else {
// When `DisconnectExtensionMessagePortWhenPageEntersBFCache` is not
// enabled, the channel should still be active.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
}
}
// Test if the chrome.runtime.sendMessage API is called, the page is allowed
// to enter the bfcache.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeRuntimeSendMessageUsage) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"sent";
content::TitleWatcher title_watcher(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
static constexpr char kAction[] =
R"HTML(
chrome.runtime.sendMessage('%s', 'some message',
() => { document.title = 'sent'});
)HTML";
EXPECT_TRUE(ExecJs(render_frame_host_a.get(),
base::StringPrintf(kAction, extension->id().c_str())));
// 2) Wait until the sendMessage has completed.
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Expect that no channel is open.
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 4) Expect that A is in the back forward cache.
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// 5) Ensure that the runtime.onConnect listener in the restored page still
// works.
static constexpr char kScript[] =
R"HTML(
var p;
chrome.tabs.query({}, (t) => {
p = chrome.tabs.connect(t[0].id);
p.onMessage.addListener(
(m) => {chrome.test.sendScriptResult(m)}
);
});
)HTML";
EXPECT_EQ("connected",
ExecuteScriptInBackgroundPage(extension->id(), kScript));
}
// Test if the chrome.runtime.connect is called then disconnected, the page is
// allowed to enter the bfcache.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeRuntimeConnectDisconnect) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
auto title_watcher = std::make_unique<content::TitleWatcher>(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
std::string action = base::StringPrintf(
R"HTML(
var p = chrome.runtime.connect('%s');
p.onMessage.addListener((m) => {document.title = m; });
)HTML",
extension->id().c_str());
EXPECT_TRUE(ExecJs(render_frame_host_a.get(), action));
// 2) Wait for the message port to be connected.
EXPECT_EQ(expected_title, title_watcher->WaitAndGetTitle());
expected_title = u"disconnect";
title_watcher = std::make_unique<content::TitleWatcher>(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
EXPECT_TRUE(ExecJs(render_frame_host_a.get(),
R"HTML(
p.onDisconnect.addListener((m) => {document.title = 'disconnect';});
p.postMessage('disconnect');
)HTML"));
EXPECT_EQ(expected_title, title_watcher->WaitAndGetTitle());
// Expect that the channel is closed.
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 4) Expect that A is in the back forward cache.
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
}
// Test if the chrome.tabs.connect is called and then the page is navigated,
// the page is allowed to enter the bfcache, but if the extension tries to send
// it a message the page will be evicted.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeTabsConnect) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
static constexpr char kScript[] =
R"HTML(
chrome.tabs.query({}, (t) => {
p = chrome.tabs.connect(t[0].id);
// Save a "global" reference to the port so it can be used by the test
// later.
port = p;
p.onMessage.addListener(
(m) => {chrome.test.sendScriptResult(m)}
);
});
)HTML";
EXPECT_EQ("connected",
ExecuteScriptInBackgroundPage(extension->id(), kScript));
// Expect that a channel is open.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// Expect that `render_frame_host_a` is cached, and the channel is still open
// only if `DisconnectExtensionMessagePortWhenPageEntersBFCache` is disabled.
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
} else {
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// Send a message to the port.
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(
extension->id(), "port.postMessage('bye');"));
// Expect that `render_frame_host_a` is destroyed, since the message should
// cause it to be evicted, and that the channel is closed.
EXPECT_TRUE(render_frame_host_a.WaitUntilRenderFrameDeleted());
EXPECT_EQ(0u, MessageService::Get(profile())->GetChannelCountForTest());
}
}
// Test that after caching and restoring a page, long-lived ports still work.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeTabsConnectChannelWorksAfterRestore) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title_connected = u"connected";
content::TitleWatcher title_watcher_connected(
browser()->tab_strip_model()->GetActiveWebContents(),
expected_title_connected);
EXPECT_EQ(MessageService::Get(profile())->GetChannelCountForTest(), 0u);
std::string action = base::StringPrintf(
R"HTML(
var p = chrome.runtime.connect('%s');
p.onMessage.addListener((m) => {
document.title = m;
});
)HTML",
extension->id().c_str());
ASSERT_TRUE(ExecJs(render_frame_host_a.get(), action));
// 2) Wait for the message port to be connected.
EXPECT_EQ(expected_title_connected,
title_watcher_connected.WaitAndGetTitle());
EXPECT_EQ(MessageService::Get(profile())->GetChannelCountForTest(), 1u);
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
EXPECT_EQ(MessageService::Get(profile())->GetChannelCountForTest(), 0u);
} else {
EXPECT_EQ(MessageService::Get(profile())->GetChannelCountForTest(), 1u);
}
// Expect that `render_frame_host_a` is cached.
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// 4) Navigate back to A.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
web_contents->GetController().GoBack();
ASSERT_TRUE(WaitForLoadStop(web_contents));
// Verify that `render_frame_host_a` is the active frame again.
EXPECT_TRUE(render_frame_host_a->GetLifecycleState() ==
content::RenderFrameHost::LifecycleState::kActive);
// 5) Post a message to the frame. Note that we shouldn't do this when
// `DisconnectExtensionMessagePortWhenPageEntersBFCache` is enabled, because
// the port has already been closed.
if (!IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(
extension->id(), "port.postMessage('restored');"));
// Verify that the message was received properly.
content::TitleWatcher title_watcher_restored(
browser()->tab_strip_model()->GetActiveWebContents(), u"restored");
EXPECT_EQ(u"restored", title_watcher_restored.WaitAndGetTitle());
}
}
// Test if the chrome.tabs.connect is called then disconnected, the page is
// allowed to enter the bfcache.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ChromeTabsConnectDisconnect) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
static constexpr char kScript[] =
R"HTML(
var p;
chrome.tabs.query({}, (t) => {
p = chrome.tabs.connect(t[0].id);
p.onMessage.addListener(
(m) => {chrome.test.sendScriptResult(m)}
);
});
)HTML";
EXPECT_EQ("connected",
ExecuteScriptInBackgroundPage(extension->id(), kScript));
static constexpr char kDisconnectScript[] =
R"HTML(
p.postMessage('disconnect');
p.onDisconnect.addListener(() => {
chrome.test.sendScriptResult('disconnect')
});
)HTML";
EXPECT_EQ("disconnect",
ExecuteScriptInBackgroundPage(extension->id(), kDisconnectScript));
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 4) Expect that A is in the back forward cache.
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
}
// Test that the extension background receives `disconnect` event if the
// channel is closed after the page enters BFCache when
// `DisconnectExtensionMessagePortWhenPageEntersBFCache` is enabled.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ExtensionBackgroundOnDisconnectEvent) {
const Extension* extension = LoadExtension(
test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script_with_background_disconnect_listener"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper rfh(
ui_test_utils::NavigateToURL(browser(), url_a));
std::u16string expected_title = u"connected";
content::TitleWatcher title_watcher(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
std::string connectScript = base::StringPrintf(
R"JS(
var p = chrome.runtime.connect('%s');
p.onMessage.addListener((m) => {document.title = m; });
)JS",
extension->id().c_str());
ASSERT_TRUE(ExecJs(rfh.get(), connectScript));
// 2) Wait for the message port to be connected.
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Expect that a channel is open.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
// 3) Navigate to B, and the channel is still open when the
// `DisconnectExtensionMessagePortWhenPageEntersBFCache` is disabled, and
// closed when it's enabled.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
EXPECT_EQ(
IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled() ? 0u : 1u,
MessageService::Get(profile())->GetChannelCountForTest());
// 4) Expect that A is in the back forward cache.
ASSERT_FALSE(rfh.IsDestroyed());
EXPECT_EQ(rfh->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// 5) Expect that the `disconnect` event is dispatched to the
// background if
// `IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled` is enabled.
constexpr char kCheckDisconnectCountScript[] =
R"JS(chrome.test.sendScriptResult(disconnectCount))JS";
EXPECT_EQ(
IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled() ? 1 : 0,
ExecuteScriptInBackgroundPage(extension->id(),
kCheckDisconnectCountScript));
}
// Tests sending a message to all frames does not send it to back-forward
// cached frames.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
MessageSentToAllFramesDoesNotSendToBackForwardCache) {
const Extension* extension = extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("background_page"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title2.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Navigate to B.
content::RenderFrameHostWrapper render_frame_host_b(
ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
ASSERT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
std::u16string expected_title = u"foo";
auto title_watcher = std::make_unique<content::TitleWatcher>(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
static constexpr char kScript[] =
R"HTML(
chrome.tabs.executeScript({allFrames: true, code: "document.title='foo'"})
)HTML";
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(extension->id(), kScript));
EXPECT_EQ(expected_title, title_watcher->WaitAndGetTitle());
// `render_frame_host_a` should still be in the cache.
ASSERT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// Expect the original title when going back to A.
expected_title = u"Title Of Awesomeness";
title_watcher = std::make_unique<content::TitleWatcher>(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
// Go back to A.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
web_contents->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(expected_title, title_watcher->WaitAndGetTitle());
// `render_frame_host_b` should still be in the cache.
ASSERT_FALSE(render_frame_host_b.IsDestroyed());
EXPECT_EQ(render_frame_host_b->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// Now go forward to B, and expect that it is what was set before it
// went into the back forward cache.
expected_title = u"foo";
title_watcher = std::make_unique<content::TitleWatcher>(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
web_contents->GetController().GoForward();
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(expected_title, title_watcher->WaitAndGetTitle());
}
// Tests sending a message to specific frame that is in the back forward cache
// fails.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
MessageSentToCachedIdFails) {
const Extension* extension = extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("background_page"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/iframe_blank.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
content::RenderFrameHostWrapper iframe(
ChildFrameAt(render_frame_host_a.get(), 0));
ASSERT_TRUE(iframe.get());
// Cache the iframe's frame tree node id to send it a message later.
int iframe_frame_tree_node_id = iframe->GetFrameTreeNodeId();
// 2) Navigate to B.
content::RenderFrameHostWrapper render_frame_host_b(
ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_NE(render_frame_host_a.get(), render_frame_host_b.get());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
std::u16string expected_title = u"foo";
auto title_watcher = std::make_unique<content::TitleWatcher>(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
static constexpr char kScript[] =
R"HTML(
chrome.tabs.executeScript({frameId: %d,
code: "document.title='foo'",
matchAboutBlank: true
}, (e) => {
chrome.test.sendScriptResult(chrome.runtime.lastError ? 'false'
: 'true')});
)HTML";
EXPECT_EQ("false",
ExecuteScriptInBackgroundPage(
extension->id(),
base::StringPrintf(kScript, iframe_frame_tree_node_id)));
// Go back to A.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
web_contents->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents));
// Re-execute the script.
EXPECT_EQ("true",
ExecuteScriptInBackgroundPage(
extension->id(),
base::StringPrintf(kScript, iframe_frame_tree_node_id)));
}
// TODO(crbug.com/1317431): WebSQL does not work on Fuchsia.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_StorageCallbackEvicts DISABLED_StorageCallbackEvicts
#else
#define MAYBE_StorageCallbackEvicts StorageCallbackEvicts
#endif
// Test that running extensions message dispatching via a ScriptContext::ForEach
// for back forward cached pages causes eviction of that RenderFrameHost.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
MAYBE_StorageCallbackEvicts) {
const Extension* extension = extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script_storage"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
// 1) Navigate to A and wait until the extension's content script has
// executed.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Navigate to B. Ensure that |render_frame_host_a| is in back/forward
// cache.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// Validate that the eviction due to JavaScript execution has not happened.
EXPECT_EQ(0, histogram_tester_.GetBucketCount(
"BackForwardCache.Eviction.Renderer",
blink::mojom::RendererEvictionReason::kJavaScriptExecution));
// 3) Navigate back to A and make sure that the callback is called after
// restore.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// Check that the page was cached.
ASSERT_EQ(render_frame_host_a.get(), web_contents()->GetPrimaryMainFrame());
// Wait for the content script to run.
content::DOMMessageQueue dom_message_queue(web_contents());
std::string dom_message;
ASSERT_TRUE(dom_message_queue.WaitForMessage(&dom_message));
ASSERT_EQ("\"event handler ran\"", dom_message);
// Verify that the callback was called.
EXPECT_EQ("called", EvalJs(render_frame_host_a.get(),
"document.getElementById('callback').value;"));
}
// Test that ensures the origin restriction declared on the extension
// manifest.json is properly respected even when BFCache is involved.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest, TabsOrigin) {
scoped_refptr<const Extension> extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("correct_origin"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
ExpectTitleChangeSuccess(*extension, "first nav");
// 2) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
ExpectTitleChangeFail(*extension);
// 3) Go back to A.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
web_contents->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents));
std::u16string title;
ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &title));
ASSERT_EQ(title, u"first nav");
ExpectTitleChangeSuccess(*extension, "restore nav");
}
// Test that ensures the content scripts only execute once on a back/forward
// cached page.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ContentScriptsRunOnlyOnce) {
ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script_stages")));
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
std::u16string expected_title = u"document_idle";
content::TitleWatcher title_watcher(
browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Verify that the content scripts have been run (the 'stage' element
// is created by the content script running at 'document_start" and
// populated whenever the content script run at 'document_start',
// 'document_end', or 'document_idle').
EXPECT_EQ("document_start/document_end/document_idle/page_show/",
EvalJs(render_frame_host_a.get(),
"document.getElementById('stage').value;"));
// 2) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// 3) Go back to A.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
web_contents->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents));
// Verify that the content scripts have not run again and that the
// 'stage' element has the appended a page_hide/page_show to its list.
EXPECT_EQ(
"document_start/document_end/document_idle/page_show/page_hide/"
"page_show/",
EvalJs(render_frame_host_a.get(),
"document.getElementById('stage').value;"));
}
// Test that an activeTab permission temporarily granted to an extension for a
// page does not revive when the BFCache entry is restored.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheBrowserTest,
ActiveTabPermissionRevoked) {
scoped_refptr<const Extension> extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("active_tab"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// Grant the activeTab permission.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ExtensionActionRunner::GetForWebContents(web_contents)
->RunAction(extension.get(), /* grant_tab_permissions=*/true);
ExpectTitleChangeSuccess(*extension, "changed_title");
// 2) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// Ensure that `render_frame_host_a` is in the cache.
EXPECT_FALSE(render_frame_host_a.IsDestroyed());
EXPECT_EQ(render_frame_host_a->GetLifecycleState(),
content::RenderFrameHost::LifecycleState::kInBackForwardCache);
// Extension should no longer be able to change title, since the permission
// should be revoked with a cross-site navigation.
ExpectTitleChangeFail(*extension);
// 3) Go back to A.
web_contents->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents));
// Extension should no longer be able to change title, since the permission
// should not revive with BFCache navigation to a.com.
ExpectTitleChangeFail(*extension);
}
// This subclass adds some necessary setup for testing the BFCache metrics
// reported by the extensions.
class ExtensionBackForwardCacheMetricsBrowserTest
: public ExtensionBackForwardCacheBrowserTest {
public:
void SetUpOnMainThread() override {
ExtensionBackForwardCacheBrowserTest::SetUpOnMainThread();
test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
// Enable extension sync, otherwise the new source url entry will be
// dropped.
test_ukm_recorder_->SetIsWebstoreExtensionCallback(
base::BindRepeating([](base::StringPiece) { return true; }));
}
protected:
ukm::TestUkmRecorder* test_ukm_recorder() { return test_ukm_recorder_.get(); }
private:
std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
};
INSTANTIATE_TEST_SUITE_P(EventPageAndFalse,
ExtensionBackForwardCacheMetricsBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = false,
.context_type = ContextType::kEventPage}));
INSTANTIATE_TEST_SUITE_P(ServiceWorkerAndFalse,
ExtensionBackForwardCacheMetricsBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = false,
.context_type = ContextType::kServiceWorker}));
INSTANTIATE_TEST_SUITE_P(EventPageAndTrue,
ExtensionBackForwardCacheMetricsBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = true,
.context_type = ContextType::kEventPage}));
INSTANTIATE_TEST_SUITE_P(ServiceWorkerAndTrue,
ExtensionBackForwardCacheMetricsBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = true,
.context_type = ContextType::kServiceWorker}));
namespace {
// Convert the given source and reason into metric value that is used for metric
// testing. This follows the implementation of
// `content::BackForwardCacheMetrics::MetricValue`.
// See the comments from `content::BackForwardCache::DisabledSource` also.
constexpr int ToBackForwardCacheDisabledReasonMetricValue(
content::BackForwardCache::DisabledSource source,
back_forward_cache::DisabledReasonId reason) {
return (static_cast<int>(source) << 16) + static_cast<int>(reason);
}
} // namespace
// Test when `DisconnectExtensionMessagePortWhenPageEntersBFCache` is disabled,
// if the extension sends message to a cached document, the document is not
// allowed to enter the back/forward cache, and the
// `BackForwardCacheDisabledForRenderFrameHostReason` metric will be recorded
// for the document URL and the extension URL.
// It also tests the case when the same extension triggers the disabling twice
// in different navigations, the metrics should be recorded under different
// source ids.
IN_PROC_BROWSER_TEST_P(
ExtensionBackForwardCacheMetricsBrowserTest,
BFCacheMetricsRecordedIfExtensionSendsMessageToCachedFrame) {
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script_message_on_pagehide"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
// 1) Navigate to A.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_a));
content::RenderFrameHostWrapper render_frame_host_a(
current_main_frame_host());
// 2) Wait for the extension to be successfully loaded.
const char16_t kTitleModified[] = u"modified";
ASSERT_EQ(
kTitleModified,
content::TitleWatcher(web_contents(), kTitleModified).WaitAndGetTitle());
// 3) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 4) If `kDisconnectExtensionMessagePortWhenPageEntersBFCache` is not
// enabled, wait for A to be deleted since back/forward cache will be disabled
// because the loaded extension is attempting to send messages to the cached
// page A.
if (!IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
ASSERT_TRUE(render_frame_host_a.WaitUntilRenderFrameDeleted());
}
// 5) Go back to A.
web_contents()->GetController().GoBack();
ASSERT_TRUE(WaitForLoadStop(web_contents()));
// Expect that metrics are recorded properly in `test_ukm_recorder()`.
constexpr int kExtensionSentMessageToCachedFrame =
ToBackForwardCacheDisabledReasonMetricValue(
content::BackForwardCache::DisabledSource::kEmbedder,
back_forward_cache::DisabledReasonId::
kExtensionSentMessageToCachedFrame);
auto entries = test_ukm_recorder()->GetEntriesByName(
ukm::builders::BackForwardCacheDisabledForRenderFrameHostReason::
kEntryName);
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
// If `DisconnectExtensionMessagePortWhenPageEntersBFCache` is enabled, the
// page will be restored from BFCache.
ASSERT_EQ(0u, entries.size());
} else {
// There should be two entries, one for the document URL and one for the
// extension URL.
ASSERT_EQ(2u, entries.size());
std::vector<GURL> entry_urls;
for (const ukm::mojom::UkmEntry* const entry : entries) {
auto* src = test_ukm_recorder()->GetSourceForSourceId(entry->source_id);
EXPECT_TRUE(src)
<< "The recorded UKM source id should have a source URL registered.";
entry_urls.push_back(src->url());
test_ukm_recorder()->ExpectEntryMetric(
entry,
ukm::builders::BackForwardCacheDisabledForRenderFrameHostReason::
kReason2Name,
kExtensionSentMessageToCachedFrame);
}
EXPECT_THAT(entry_urls,
testing::UnorderedElementsAre(url_a, extension->url()))
<< "UKM metrics should be recorded under the document URL and the "
"extension URL.";
}
// 6) Now we are in A, wait for the extension to be successfully loaded.
content::RenderFrameHostWrapper render_frame_host_a2(
current_main_frame_host());
ASSERT_EQ(
kTitleModified,
content::TitleWatcher(web_contents(), kTitleModified).WaitAndGetTitle());
// 7) Navigate to B.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b));
// 8) If `DisconnectExtensionMessagePortWhenPageEntersBFCache` is not
// enabled, wait for A to be deleted since back/forward cache will be disabled
// because the loaded extension is attempting to send messages to the cached
// page A.
if (!IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
ASSERT_TRUE(render_frame_host_a2.WaitUntilRenderFrameDeleted());
}
// 9) Go back to A.
web_contents()->GetController().GoBack();
ASSERT_TRUE(WaitForLoadStop(web_contents()));
// Expect that metrics are recorded properly in `test_ukm_recorder()`, and
// with a different source id compared to the first time.
entries = test_ukm_recorder()->GetEntriesByName(
ukm::builders::BackForwardCacheDisabledForRenderFrameHostReason::
kEntryName);
if (IsDisconnectExtensionMessagePortWhenPageEntersBFCacheEnabled()) {
// If `DisconnectExtensionMessagePortWhenPageEntersBFCache` is enabled, the
// page will be restored from BFCache.
ASSERT_EQ(0u, entries.size());
} else {
// There should be two more new entries, one for the document URL and one
// for the extension URL.
EXPECT_EQ(2u + 2u, entries.size())
<< "Another 2 UKM metrics with different source ID should be recorded "
"from the second navigation";
std::vector<GURL> entry_urls;
for (const ukm::mojom::UkmEntry* const entry : entries) {
auto* src = test_ukm_recorder()->GetSourceForSourceId(entry->source_id);
ASSERT_TRUE(src)
<< "The recorded UKM source id should have a source URL registered.";
entry_urls.push_back(src->url());
test_ukm_recorder()->ExpectEntryMetric(
entry,
ukm::builders::BackForwardCacheDisabledForRenderFrameHostReason::
kReason2Name,
kExtensionSentMessageToCachedFrame);
}
EXPECT_THAT(entry_urls,
testing::UnorderedElementsAre(url_a, url_a, extension->url(),
extension->url()))
<< "UKM metrics should be recorded under the document URL and the "
"extension URL, and they are recorded twice each with different UKM "
"source id.";
}
}
class ExtensionBackForwardCacheWithPrerenderBrowserTest
: public ExtensionBackForwardCacheBrowserTest {
public:
ExtensionBackForwardCacheWithPrerenderBrowserTest()
: prerender_helper_(base::BindRepeating(
&ExtensionBackForwardCacheBrowserTest::web_contents,
base::Unretained(this))) {}
void SetUp() override {
prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
ExtensionBackForwardCacheBrowserTest::SetUp();
}
content::test::PrerenderTestHelper& prerender_helper() {
return prerender_helper_;
}
private:
content::test::PrerenderTestHelper prerender_helper_;
};
INSTANTIATE_TEST_SUITE_P(EventPageAndFalse,
ExtensionBackForwardCacheWithPrerenderBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = false,
.context_type = ContextType::kEventPage}));
INSTANTIATE_TEST_SUITE_P(ServiceWorkerAndFalse,
ExtensionBackForwardCacheWithPrerenderBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = false,
.context_type = ContextType::kServiceWorker}));
INSTANTIATE_TEST_SUITE_P(EventPageAndTrue,
ExtensionBackForwardCacheWithPrerenderBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = true,
.context_type = ContextType::kEventPage}));
INSTANTIATE_TEST_SUITE_P(ServiceWorkerAndTrue,
ExtensionBackForwardCacheWithPrerenderBrowserTest,
::testing::Values(TestParams{
.enable_disconnect_message_port_on_bfcache = true,
.context_type = ContextType::kServiceWorker}));
// Test the extension message port created during prerendering won't be closed
// after the prerendered page is activated.
IN_PROC_BROWSER_TEST_P(ExtensionBackForwardCacheWithPrerenderBrowserTest,
PortIsStillOpenAfterPrerenderAndActivate) {
// This extension will automatically create a port from the content script.
// It's only registers on title2.html, the prerendered page from this test.
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("back_forward_cache")
.AppendASCII("content_script_auto_connect"));
ASSERT_TRUE(extension);
ASSERT_TRUE(embedded_test_server()->Start());
base::HistogramTester histogram_tester;
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
// 1) Navigate to A.
content::RenderFrameHostWrapper render_frame_host_a(
ui_test_utils::NavigateToURL(browser(), url_a));
// 2) Start a prerender.
GURL prerender_url = embedded_test_server()->GetURL("a.com", "/title2.html");
prerender_helper().AddPrerender(prerender_url);
// 3) Activate.
content::TestActivationManager activation_manager(web_contents(),
prerender_url);
ASSERT_TRUE(
content::ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("location = $1", prerender_url)));
activation_manager.WaitForNavigationFinished();
EXPECT_TRUE(activation_manager.was_activated());
histogram_tester.ExpectUniqueSample(
"Prerender.Experimental.PrerenderHostFinalStatus.SpeculationRule",
/* PrerenderFinalStatus::kActivated */ 0, 1);
// The channel associated to the prerendered page should be open.
EXPECT_EQ(1u, MessageService::Get(profile())->GetChannelCountForTest());
}
} // namespace extensions