blob: 86e66c2b1e1fd69dc1767846529c3b52ddb80386 [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 <memory>
#include <string_view>
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/extensions/api/runtime/chrome_runtime_api_delegate.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/api/offscreen/offscreen_document_manager.h"
#include "extensions/browser/api/runtime/runtime_api.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/blocklist_extension_prefs.h"
#include "extensions/browser/blocklist_state.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_dialog_auto_confirm.h"
#include "extensions/browser/extension_function.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_registry.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/offscreen_document_host.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/script_executor.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_id.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/url_constants.h"
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/test/base/ui_test_utils.h"
#endif
#if BUILDFLAG(ENABLE_PLATFORM_APPS)
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#endif
namespace extensions {
using ContextType = extensions::browser_test_util::ContextType;
class RuntimeApiTest : public ExtensionApiTest,
public testing::WithParamInterface<ContextType> {
public:
RuntimeApiTest() : ExtensionApiTest(GetParam()) {}
~RuntimeApiTest() override = default;
RuntimeApiTest(const RuntimeApiTest&) = delete;
RuntimeApiTest& operator=(const RuntimeApiTest&) = delete;
std::string GetActiveUrl() {
return GetActiveWebContents()->GetLastCommittedURL().spec();
}
};
// Android only supports MV3 and later, therefor don't need to test for
// persistent background context.
#if !BUILDFLAG(IS_ANDROID)
INSTANTIATE_TEST_SUITE_P(PersistentBackground,
RuntimeApiTest,
::testing::Values(ContextType::kPersistentBackground));
#endif
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
RuntimeApiTest,
::testing::Values(ContextType::kServiceWorker));
// Tests the privileged components of chrome.runtime.
IN_PROC_BROWSER_TEST_P(RuntimeApiTest, ChromeRuntimePrivileged) {
ASSERT_TRUE(RunExtensionTest("runtime/privileged")) << message_;
}
// Tests the unprivileged components of chrome.runtime.
IN_PROC_BROWSER_TEST_P(RuntimeApiTest, ChromeRuntimeUnprivileged) {
ASSERT_TRUE(StartEmbeddedTestServer());
ASSERT_TRUE(
LoadExtension(test_data_dir_.AppendASCII("runtime/content_script")));
// The content script runs on this page.
ResultCatcher catcher;
ASSERT_TRUE(content::NavigateToURL(
GetActiveWebContents(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(catcher.GetNextResult()) << message_;
}
IN_PROC_BROWSER_TEST_P(RuntimeApiTest, ChromeRuntimeUninstallURL) {
// Auto-confirm the uninstall dialog.
ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT);
ExtensionTestMessageListener ready_listener("ready");
ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("runtime")
.AppendASCII("uninstall_url")
.AppendASCII("sets_uninstall_url")));
EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
ASSERT_TRUE(RunExtensionTest("runtime/uninstall_url")) << message_;
}
IN_PROC_BROWSER_TEST_P(RuntimeApiTest, GetPlatformInfo) {
ASSERT_TRUE(RunExtensionTest("runtime/get_platform_info")) << message_;
}
namespace {
#if BUILDFLAG(ENABLE_EXTENSIONS)
const char kUninstallUrl[] = "https://www.google.com/";
#endif
class RuntimeAPIUpdateTest : public ExtensionApiTest {
public:
RuntimeAPIUpdateTest() = default;
RuntimeAPIUpdateTest(const RuntimeAPIUpdateTest&) = delete;
RuntimeAPIUpdateTest& operator=(const RuntimeAPIUpdateTest&) = delete;
protected:
void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread();
EXPECT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
}
struct ExtensionCRXData {
std::string unpacked_relative_path;
base::FilePath crx_path;
explicit ExtensionCRXData(const std::string& unpacked_relative_path)
: unpacked_relative_path(unpacked_relative_path) {}
};
void SetUpCRX(const std::string& root_dir,
const std::string& pem_filename,
std::vector<ExtensionCRXData>* crx_data_list) {
const base::FilePath test_dir = test_data_dir_.AppendASCII(root_dir);
const base::FilePath pem_path = test_dir.AppendASCII(pem_filename);
for (ExtensionCRXData& crx_data : *crx_data_list) {
crx_data.crx_path = PackExtensionWithOptions(
test_dir.AppendASCII(crx_data.unpacked_relative_path),
scoped_temp_dir_.GetPath().AppendASCII(
crx_data.unpacked_relative_path + ".crx"),
pem_path, base::FilePath());
}
}
bool CrashEnabledExtension(const ExtensionId& extension_id) {
ExtensionHost* background_host =
ProcessManager::Get(profile())->GetBackgroundHostForExtension(
extension_id);
if (!background_host) {
return false;
}
content::CrashTab(background_host->host_contents());
return true;
}
private:
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
#if BUILDFLAG(ENABLE_EXTENSIONS)
// TODO(crbug.com/383366125): Enable this test for desktop Android once
// ChromeRuntimeAPIDelegate::OpenOptionsPage() is implemented.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeOpenOptionsPage) {
ASSERT_TRUE(RunExtensionTest("runtime/open_options_page"));
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeOpenOptionsPageError) {
ASSERT_TRUE(RunExtensionTest("runtime/open_options_page_error"));
}
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeGetPlatformInfo) {
base::Value::Dict dict =
api_test_utils::ToDict(api_test_utils::RunFunctionAndReturnSingleResult(
new RuntimeGetPlatformInfoFunction(), "[]", profile()));
EXPECT_TRUE(dict.contains("os"));
EXPECT_TRUE(dict.contains("arch"));
#if defined(ARCH_CPU_RISCV64)
// NaCl had never supported RISC-V, so nacl_arch is meaningless there.
EXPECT_FALSE(dict.contains("nacl_arch"));
#else
EXPECT_TRUE(dict.contains("nacl_arch"));
#endif
}
#if BUILDFLAG(ENABLE_PLATFORM_APPS)
// Tests chrome.runtime.getPackageDirectory with an app.
IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest,
ChromeRuntimeGetPackageDirectoryEntryApp) {
ASSERT_TRUE(RunExtensionTest("api_test/runtime/get_package_directory/app",
{.launch_as_platform_app = true}))
<< message_;
}
#endif // BUILDFLAG(ENABLE_PLATFORM_APPS)
// Tests chrome.runtime.getPackageDirectory with an MV2 extension.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest,
ChromeRuntimeGetPackageDirectoryEntryMV2Extension) {
ASSERT_TRUE(RunExtensionTest("runtime/get_package_directory/extension",
{.extension_url = "test/test.html"}))
<< message_;
}
// Tests chrome.runtime.getPackageDirectory with an MV3 extension. Note: we use
// an html page in this test as getPackageDirectory isn't exposed on service
// workers.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest,
ChromeRuntimeGetPackageDirectoryEntryMV3Extension) {
SetCustomArg("run_promise_test");
ASSERT_TRUE(RunExtensionTest("runtime/get_package_directory/extension",
{.extension_url = "test/test.html"},
{.load_as_manifest_version_3 = true}))
<< message_;
}
// Tests that an extension calling chrome.runtime.reload() repeatedly
// will eventually be terminated.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ExtensionTerminatedForRapidReloads) {
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
static constexpr char kManifest[] = R"(
{
"name": "reload",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2
})";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("background.js"),
"chrome.test.sendMessage('ready');");
// Use a packed extension, since this is the scenario we are interested in
// testing. Unpacked extensions are allowed more reloads within the allotted
// time, to avoid interfering with the developer work flow.
const Extension* extension = LoadExtension(dir.Pack());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
// The current limit for fast reload is 5, so the loop limit of 10
// be enough to trigger termination. If the extension manages to
// reload itself that often without being terminated, the test fails
// anyway.
for (int i = 0; i < RuntimeAPI::kFastReloadCount + 1; i++) {
ExtensionTestMessageListener ready_listener_reload("ready");
TestExtensionRegistryObserver unload_observer(registry, extension_id);
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(
extension_id, "chrome.runtime.reload();"));
unload_observer.WaitForExtensionUnloaded();
base::RunLoop().RunUntilIdle();
if (registry->terminated_extensions().GetByID(extension_id)) {
break;
} else {
EXPECT_TRUE(ready_listener_reload.WaitUntilSatisfied());
}
}
ASSERT_TRUE(registry->terminated_extensions().GetByID(extension_id));
}
// Tests chrome.runtime.reload
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeReload) {
static constexpr char kManifest[] = R"(
{
"name": "reload",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2
})";
static constexpr char kScript[] = R"(
chrome.test.sendMessage('ready', function(response) {
if (response == 'reload') {
chrome.runtime.reload();
} else if (response == 'done') {
chrome.test.notifyPass();
}
});
)";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("background.js"), kScript);
// This listener will respond to the initial load of the extension
// and tell the script to do the reload.
ExtensionTestMessageListener ready_listener_reload("ready",
ReplyBehavior::kWillReply);
const Extension* extension = LoadExtension(dir.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
EXPECT_TRUE(ready_listener_reload.WaitUntilSatisfied());
// This listener will respond to the ready message from the
// reloaded extension and tell the script to finish the test.
ExtensionTestMessageListener ready_listener_done("ready",
ReplyBehavior::kWillReply);
ResultCatcher reload_catcher;
ready_listener_reload.Reply("reload");
EXPECT_TRUE(ready_listener_done.WaitUntilSatisfied());
ready_listener_done.Reply("done");
EXPECT_TRUE(reload_catcher.GetNextResult());
}
// Tests sending messages from a webpage in the extension using
// chrome.runtime.sendMessage and responding to those from the extension's
// service worker in a chrome.runtime.onMessage listener.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeSendMessage) {
ASSERT_TRUE(
RunExtensionTest("runtime/send_message", {.extension_url = "test.html"}));
}
// Simple test for chrome.runtime.getBackgroundPage with a persistent background
// page.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeGetBackgroundPage) {
static constexpr char kManifest[] = R"(
{
"name": "getBackgroundPage",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2
})";
static constexpr char kBackground[] = "window.backgroundExists = true;";
static constexpr char kTestPage[] = R"(<script src="test.js"></script>)";
static constexpr char kTestJS[] = R"(
chrome.test.runTests([
function getBackgroundPage() {
chrome.runtime.getBackgroundPage((page) => {
chrome.test.assertTrue(page.backgroundExists);
chrome.test.succeed();
});
}
]);
)";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);
dir.WriteFile(FILE_PATH_LITERAL("test.html"), kTestPage);
dir.WriteFile(FILE_PATH_LITERAL("test.js"), kTestJS);
ASSERT_TRUE(RunExtensionTest(dir.UnpackedPath(),
{.extension_url = "test.html"},
/*load_options=*/{}));
}
// Simple test for chrome.runtime.getBackgroundPage with an MV3 service worker
// extension, which should return an error due to there being no background
// page.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeGetBackgroundPageMV3) {
static constexpr char kManifest[] = R"(
{
"name": "getBackgroundPage",
"version": "1.0",
"background": {
"service_worker": "worker.js"
},
"manifest_version": 3
})";
static constexpr char kWorker[] = "// We're just expecting an error";
static constexpr char kTestPage[] = R"(<script src="test.js"></script>)";
static constexpr char kTestJS[] = R"(
chrome.test.runTests([
function getBackgroundPage() {
chrome.runtime.getBackgroundPage((page) => {
chrome.test.assertEq(undefined, page);
chrome.test.assertLastError('You do not have a background page.');
chrome.test.succeed();
});
},
async function getBackGroundPagePromise() {
await chrome.test.assertPromiseRejects(
chrome.runtime.getBackgroundPage(),
'Error: You do not have a background page.');
chrome.test.succeed();
}
]);
)";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker);
dir.WriteFile(FILE_PATH_LITERAL("test.html"), kTestPage);
dir.WriteFile(FILE_PATH_LITERAL("test.js"), kTestJS);
ASSERT_TRUE(RunExtensionTest(
dir.UnpackedPath(), {.extension_url = "test.html"}, /*load_options=*/{}));
}
// Simple test for chrome.runtime.requestUpdateCheck using promises and
// callbacks. The actual behaviors and responses are more thoroughly tested in
// chrome_runtime_api_delegate_unittest.cc
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, RuntimeRequestUpdateCheck) {
static constexpr char kManifest[] = R"(
{
"name": "requestUpdateCheck",
"version": "1.0",
"background": {
"service_worker": "worker.js"
},
"manifest_version": 3
})";
static constexpr char kWorker[] = R"(
chrome.test.runTests([
// Note: when called with a callback, the callback will receive two
// parameters, but when called with a promise they will come back as
// parameters on a single object.
function noUpdateCallback() {
chrome.runtime.requestUpdateCheck((status, details) => {
chrome.test.assertNoLastError();
chrome.test.assertEq('no_update', status);
chrome.test.assertEq({version: ''}, details);
// Another call soon after will be throttled.
chrome.runtime.requestUpdateCheck((status, details) => {
chrome.test.assertNoLastError();
chrome.test.assertEq('throttled', status);
chrome.test.assertEq({version: ''}, details);
chrome.test.succeed();
});
});
},
async function noUpdate() {
// Advance the throttle clock so the requests in the previous test don't
// result in this getting a throttled response.
await chrome.test.sendMessage('Advance');
let result = await chrome.runtime.requestUpdateCheck();
chrome.test.assertEq({status:'no_update', version: ''}, result);
result = await chrome.runtime.requestUpdateCheck();
chrome.test.assertEq({status:'throttled', version: ''}, result);
chrome.test.succeed();
}
]);
)";
base::SimpleTestTickClock clock;
ChromeRuntimeAPIDelegate::set_tick_clock_for_tests(&clock);
ExtensionTestMessageListener message_listener("Advance",
ReplyBehavior::kWillReply);
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker);
// In the test environment we need this to be a packed extension.
base::FilePath crx_path = PackExtension(dir.UnpackedPath());
ASSERT_FALSE(crx_path.empty());
auto OnMessage = [&](const std::string& message) {
// Advance the clock past the point it will be throttled.
clock.Advance(base::Days(1));
message_listener.Reply("");
};
message_listener.SetOnSatisfied(base::BindLambdaForTesting(OnMessage));
ASSERT_TRUE(RunExtensionTest(crx_path, {}, {}));
}
// Tests that updating a terminated extension sends runtime.onInstalled event
// with correct previousVersion.
// Regression test for https://crbug.com/724563.
IN_PROC_BROWSER_TEST_F(RuntimeAPIUpdateTest,
TerminatedExtensionUpdateHasCorrectPreviousVersion) {
std::vector<ExtensionCRXData> data;
data.emplace_back("v1");
data.emplace_back("v2");
SetUpCRX("runtime/update_terminated_extension", "pem.pem", &data);
ExtensionId extension_id;
{
// Install version 1 of the extension.
ResultCatcher catcher;
const int expected_change = 1;
const Extension* extension_v1 =
InstallExtension(data[0].crx_path, expected_change);
extension_id = extension_v1->id();
ASSERT_TRUE(extension_v1);
EXPECT_TRUE(catcher.GetNextResult());
}
ASSERT_TRUE(CrashEnabledExtension(extension_id));
// The process-terminated notification may be received immediately before
// the task that will actually update the active-extensions count, so spin
// the message loop to ensure we are up-to-date.
base::RunLoop().RunUntilIdle();
{
// Update to version 2, expect runtime.onInstalled with
// previousVersion = '1'.
ResultCatcher catcher;
const int expected_change = 1;
const Extension* extension_v2 =
UpdateExtension(extension_id, data[1].crx_path, expected_change);
ASSERT_TRUE(extension_v2);
EXPECT_TRUE(catcher.GetNextResult());
}
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
// TODO(crbug.com/423725749): Port to desktop Android when a cross-platform
// browser abstraction is available.
// Tests that when the last active tab in the window belongs to the extension
// with an uninstall URL, uninstalling the extension does not close the current
// browser. Regression test for crbug.com/362452856
//
// TODO(crbug.com/415617543): Test is flaky on Linux ASan.
#if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER)
#define MAYBE_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab \
DISABLED_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab
#else
#define MAYBE_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab \
OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab
#endif
IN_PROC_BROWSER_TEST_P(
RuntimeApiTest,
MAYBE_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab) {
ExtensionTestMessageListener ready_listener("ready");
// Load an extension that has set an uninstall url.
scoped_refptr<const Extension> extension =
LoadExtension(test_data_dir_.AppendASCII("runtime")
.AppendASCII("uninstall_url")
.AppendASCII("sets_uninstall_url"));
EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
ASSERT_TRUE(extension.get());
extension_registrar()->AddExtension(extension.get());
ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension->id()));
TabStripModel* tabs = browser()->tab_strip_model();
ASSERT_EQ(1, tabs->count());
ASSERT_EQ("about:blank", GetActiveUrl());
// Navigate to an extension page.
const GURL extension_page_url = extension->GetResourceURL("page.html");
content::RenderFrameHost* new_host =
ui_test_utils::NavigateToURL(browser(), extension_page_url);
ASSERT_TRUE(new_host);
EXPECT_EQ(1, tabs->count());
EXPECT_EQ(extension_page_url.spec(), GetActiveUrl());
// Uninstall the extension and expect its uninstall url to open in a new tab.
extension_registrar()->UninstallExtension(
extension->id(), UNINSTALL_REASON_USER_INITIATED, nullptr);
content::WaitForLoadStop(tabs->GetActiveWebContents());
EXPECT_EQ(2, tabs->count());
// The current tab should be pointing to the uninstall url of the extension.
EXPECT_EQ(kUninstallUrl, GetActiveUrl());
// The tab at index 0 should now be overwritten with the default NTP.
EXPECT_EQ(chrome::kChromeUINewTabURL,
tabs->GetWebContentsAt(0)->GetLastCommittedURL().spec());
}
// Tests that when a blocklisted extension with a set uninstall url is
// uninstalled, its uninstall url does not open.
IN_PROC_BROWSER_TEST_P(RuntimeApiTest,
DoNotOpenUninstallUrlForBlocklistedExtensions) {
ExtensionTestMessageListener ready_listener("ready");
// Load an extension that has set an uninstall url.
scoped_refptr<const Extension> extension =
LoadExtension(test_data_dir_.AppendASCII("runtime")
.AppendASCII("uninstall_url")
.AppendASCII("sets_uninstall_url"));
EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
ASSERT_TRUE(extension.get());
extension_registrar()->AddExtension(extension.get());
ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension->id()));
// Uninstall the extension and expect its uninstall url to open.
extension_registrar()->UninstallExtension(
extension->id(), UNINSTALL_REASON_USER_INITIATED, nullptr);
TabStripModel* tabs = browser()->tab_strip_model();
EXPECT_EQ(2, tabs->count());
content::WaitForLoadStop(tabs->GetActiveWebContents());
// Verify the uninstall url
EXPECT_EQ(kUninstallUrl, GetActiveUrl());
// Close the tab pointing to the uninstall url.
tabs->CloseWebContentsAt(tabs->active_index(), 0);
EXPECT_EQ(1, tabs->count());
EXPECT_EQ("about:blank", GetActiveUrl());
// Load the same extension again, except blocklist it after installation.
ExtensionTestMessageListener ready_listener_reload("ready");
extension = LoadExtension(test_data_dir_.AppendASCII("runtime")
.AppendASCII("uninstall_url")
.AppendASCII("sets_uninstall_url"));
EXPECT_TRUE(ready_listener_reload.WaitUntilSatisfied());
extension_registrar()->AddExtension(extension.get());
ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension->id()));
// Blocklist extension.
blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension->id(), BitMapBlocklistState::BLOCKLISTED_MALWARE,
ExtensionPrefs::Get(profile()));
// Uninstalling a blocklisted extension should not open its uninstall url.
TestExtensionRegistryObserver observer(ExtensionRegistry::Get(profile()),
extension->id());
extension_registrar()->UninstallExtension(
extension->id(), UNINSTALL_REASON_USER_INITIATED, nullptr);
observer.WaitForExtensionUninstalled();
EXPECT_EQ(1, tabs->count());
EXPECT_TRUE(content::WaitForLoadStop(tabs->GetActiveWebContents()));
EXPECT_EQ(url::kAboutBlankURL, GetActiveUrl());
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
#if !BUILDFLAG(IS_ANDROID)
// Used for tests that only make sense with a background page. Unsupported on
// Android because it only supports service workers.
using BackgroundPageOnlyRuntimeApiTest = RuntimeApiTest;
INSTANTIATE_TEST_SUITE_P(All,
BackgroundPageOnlyRuntimeApiTest,
testing::Values(ContextType::kPersistentBackground));
// Regression test for https://crbug.com/1298195 - whether a tab opened
// from the background page (via `window.open(...)`) will be correctly
// marked as `mojom::ViewType::kTabContents`.
//
// This test is a BackgroundPageOnlyRuntimeApiTest, because service workers
// can call neither 1) window.open nor 2) chrome.extension.getViews.
IN_PROC_BROWSER_TEST_P(BackgroundPageOnlyRuntimeApiTest,
GetViewsOfWindowOpenedFromBackgroundPage) {
ASSERT_EQ(GetParam(), ContextType::kPersistentBackground);
static constexpr char kManifest[] = R"(
{
"name": "test",
"version": "1.0",
"background": {"scripts": ["background.js"]},
"manifest_version": 2
})";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
dir.WriteFile(FILE_PATH_LITERAL("index.htm"), "");
const Extension* extension = LoadExtension(dir.UnpackedPath());
ASSERT_TRUE(extension);
GURL new_tab_url = extension->GetResourceURL("index.htm");
{
content::TestNavigationObserver nav_observer(new_tab_url);
nav_observer.StartWatchingNewWebContents();
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(
extension->id(), R"(window.open('/index.htm', '');)"));
nav_observer.Wait();
}
{
ExtensionHost* host =
ProcessManager::Get(profile())->GetBackgroundHostForExtension(
extension->id());
ASSERT_TRUE(host);
content::DOMMessageQueue message_queue(host->host_contents());
static constexpr char kScript[] = R"(
const foundWindows = chrome.extension.getViews({type: 'tab'});
domAutomationController.send(foundWindows.length);
domAutomationController.send(foundWindows[0].location.href);
)";
ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(extension->id(), kScript));
std::string json;
ASSERT_TRUE(message_queue.WaitForMessage(&json));
ASSERT_EQ("1", json);
ASSERT_TRUE(message_queue.WaitForMessage(&json));
std::optional<base::Value> url =
base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS);
ASSERT_TRUE(url->is_string());
ASSERT_EQ(new_tab_url.spec(), url->GetString());
}
}
#endif // !BUILDFLAG(IS_ANDROID)
class RuntimeGetContextsApiTest : public ExtensionApiTest {
public:
RuntimeGetContextsApiTest() = default;
RuntimeGetContextsApiTest(const RuntimeGetContextsApiTest&) = delete;
RuntimeGetContextsApiTest& operator=(const RuntimeGetContextsApiTest&) =
delete;
~RuntimeGetContextsApiTest() override = default;
void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread();
SetUpExtension();
}
void TearDownOnMainThread() override {
extension_ = nullptr;
ExtensionApiTest::TearDownOnMainThread();
}
// Don't create a side panel context because desktop Android doesn't support
// that. It's tested separately below.
virtual void SetUpExtension() {
static constexpr char kManifest[] =
R"({
"name": "Get Contexts",
"version": "0.1",
"manifest_version": 3,
"permissions": ["offscreen"],
"devtools_page": "devtools.html",
"background": {
"service_worker": "background.js"
}
})";
test_dir_.WriteManifest(kManifest);
test_dir_.WriteFile(FILE_PATH_LITERAL("background.js"),
"// Intentionally blank");
test_dir_.WriteFile(FILE_PATH_LITERAL("page.html"),
"<html>Hello, world!</html>");
test_dir_.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
"<html>Hello, offscreen world!</html>");
test_dir_.WriteFile(FILE_PATH_LITERAL("devtools.html"),
R"(<html>
Hello, developer tools!
<script src="devtools.js"></script>
</html>)");
test_dir_.WriteFile(FILE_PATH_LITERAL("devtools.js"),
"chrome.test.sendMessage('devtools page opened');");
extension_ = LoadExtension(test_dir_.UnpackedPath());
ASSERT_TRUE(extension_);
}
// Runs `chrome.runtime.getContexts()` and returns the result as a
// base::Value.
base::Value GetContexts(std::string_view filter) {
static constexpr char kScriptTemplate[] =
R"((async () => {
chrome.test.sendScriptResult(
await chrome.runtime.getContexts(%s));
})();)";
std::string script = base::StringPrintf(kScriptTemplate, filter.data());
return BackgroundScriptExecutor::ExecuteScript(
profile(), extension_->id(), script,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
}
// Runs `chrome.runtime.getContexts()` and returns the result as a vector
// of strongly-typed `ExtensionContext`s. Expects the getContexts() call to
// return a valid value (i.e., not throw an error).
std::vector<api::runtime::ExtensionContext> GetContextStructs(
std::string_view filter) {
base::Value value = GetContexts(filter);
return ContextValueToContextStructs(value);
}
// Converts the given `value` into a vector of strongly-typed
// `ExtensionContext`s. Expects the value to properly convert.
std::vector<api::runtime::ExtensionContext> ContextValueToContextStructs(
const base::Value& value) {
if (!value.is_list()) {
ADD_FAILURE() << "Invalid return value: " << value;
return {};
}
std::vector<api::runtime::ExtensionContext> result;
result.reserve(value.GetList().size());
for (const auto& entry : value.GetList()) {
if (!entry.is_dict()) {
ADD_FAILURE() << "Invalid return value: " << value;
return {};
}
auto context = api::runtime::ExtensionContext::FromValue(entry.GetDict());
if (!context) {
ADD_FAILURE() << "Invalid return value: " << value;
return {};
}
result.push_back(std::move(*context));
}
return result;
}
// Returns a matcher that expects an ExtensionContext to be a valid
// background context (without testing details of the entry).
auto GetBackgroundMatcher() {
return testing::AllOf(
testing::Field(&api::runtime::ExtensionContext::context_type,
testing::Eq(api::runtime::ContextType::kBackground)),
testing::Field(&api::runtime::ExtensionContext::tab_id,
testing::Eq(-1)),
testing::Field(&api::runtime::ExtensionContext::window_id,
testing::Eq(-1)),
testing::Field(&api::runtime::ExtensionContext::frame_id,
testing::Eq(-1)));
}
// Returns a matcher that expects an ExtensionContext to correspond to a
// frame-based context type (without testing the details of the entry).
auto GetFrameMatcher(api::runtime::ContextType context_type,
const GURL& url) {
return testing::AllOf(
testing::Field(&api::runtime::ExtensionContext::context_type,
testing::Eq(context_type)),
testing::Field(&api::runtime::ExtensionContext::document_url,
testing::Eq(url.spec())));
}
const Extension& extension() const { return *extension_; }
protected:
raw_ptr<const Extension> extension_ = nullptr;
TestExtensionDir test_dir_;
};
// Tests retrieving the background service worker context using
// `chrome.runtime.getContexts()`.
// TODO(crbug.com/40901108): failed on "chromium/ci/Mac12 Tests"
#if BUILDFLAG(IS_MAC)
#define MAYBE_GetServiceWorkerContext DISABLED_GetServiceWorkerContext
#else
#define MAYBE_GetServiceWorkerContext GetServiceWorkerContext
#endif
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest,
MAYBE_GetServiceWorkerContext) {
// An empty dictionary filter should match all contexts (of which there is
// only one).
base::Value contexts = GetContexts("{}");
ProcessManager* process_manager = ProcessManager::Get(profile());
std::vector<WorkerId> workers =
process_manager->GetServiceWorkersForExtension(extension().id());
ASSERT_EQ(1u, workers.size());
base::Uuid expected_context_id =
ProcessManager::Get(profile())->GetContextIdForWorker(workers[0]);
EXPECT_TRUE(expected_context_id.is_valid());
// Note: fields of `documentId`, `documentUrl`, and `documentOrigin` are
// undefined (service worker contexts don't have an associated document).
// `tabId`, `frameId`, and `windowId` are -1 for consistency with other
// APIs.
static constexpr char kExpected[] =
R"([{
"contextType": "BACKGROUND",
"contextId": "%s",
"tabId": -1,
"windowId": -1,
"frameId": -1,
"incognito": false
}])";
std::string expected_contents = base::StringPrintf(
kExpected, expected_context_id.AsLowercaseString().c_str());
EXPECT_THAT(contexts, base::test::IsJson(expected_contents));
// Now, wait for the extension worker to terminate and verify that no
// no contexts are retrieved.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension().id());
// In order to be able to call the API, we need to open a new tab to an
// extension resource.
auto* web_contents = GetActiveWebContents();
const GURL extension_page_url = extension().GetResourceURL("page.html");
ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));
content::RenderFrameHost* new_host = web_contents->GetPrimaryMainFrame();
ASSERT_TRUE(new_host);
static constexpr char kScript[] =
R"((async () => {
let contexts =
await chrome.runtime.getContexts({contextTypes: ['BACKGROUND']});
return JSON.stringify(contexts);
})();)";
// No contexts should have been returned.
EXPECT_EQ("[]", content::EvalJs(new_host, kScript));
}
// Tests the filter matching behavior of `runtime.getContexts()`.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest, FilterMatching) {
// Currently, there is only one context: the background service worker. Also
// open a tab-based context.
auto* web_contents = GetActiveWebContents();
const GURL extension_page_url = extension().GetResourceURL("page.html");
ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));
content::RenderFrameHost* new_host = web_contents->GetPrimaryMainFrame();
ASSERT_TRUE(new_host);
int tab_id = ExtensionTabUtil::GetTabId(web_contents);
{
// Pass a filter matching everything. Both the tab context and worker
// context should be returned.
std::vector<api::runtime::ExtensionContext> contexts =
GetContextStructs(R"({})");
EXPECT_THAT(contexts, testing::UnorderedElementsAre(
GetBackgroundMatcher(),
GetFrameMatcher(api::runtime::ContextType::kTab,
extension_page_url)));
}
{
// Passing a filter to match background contexts should match the worker.
std::vector<api::runtime::ExtensionContext> contexts =
GetContextStructs(R"({"contextTypes": ["BACKGROUND"]})");
EXPECT_THAT(contexts, testing::ElementsAre(GetBackgroundMatcher()));
}
{
// Passing a filter to match a tab ID should match the corresponding
// page context.
std::string filter = base::StringPrintf(R"({"tabIds": [%d]})", tab_id);
std::vector<api::runtime::ExtensionContext> contexts =
GetContextStructs(filter);
EXPECT_THAT(contexts,
testing::ElementsAre(GetFrameMatcher(
api::runtime::ContextType::kTab, extension_page_url)));
}
{
// Try passing a filter for a context type with no corresponding matches. No
// contexts should be returned.
std::vector<api::runtime::ExtensionContext> contexts =
GetContextStructs(R"({"contextTypes": ["POPUP"]})");
EXPECT_THAT(contexts, testing::IsEmpty());
}
{
// Filter properties support an array of options; if the context matches an
// entry in the array, it matches the filter for that property. Thus,
// passing both "BACKGROUND" and "POPUP" should match the service worker
// context.
std::vector<api::runtime::ExtensionContext> contexts =
GetContextStructs(R"({"contextTypes": ["BACKGROUND", "POPUP"]})");
EXPECT_THAT(contexts, testing::ElementsAre(GetBackgroundMatcher()));
}
{
// All specified filter properties must match. Thus, if we look for a
// background context and also specify a tab ID, nothing should match
// (since the background context doesn't have an associated tab).
static constexpr char kFilter[] =
R"({
"contextTypes": ["BACKGROUND"],
"tabIds": [2]
})";
std::vector<api::runtime::ExtensionContext> contexts =
GetContextStructs(kFilter);
EXPECT_THAT(contexts, testing::IsEmpty());
}
}
// Tests retrieving tab contexts using `chrome.runtime.getContexts()`.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest, GetTabContext) {
// Open a new extension tab.
auto* web_contents = GetActiveWebContents();
const GURL frame_url = extension().GetResourceURL("page.html");
ASSERT_TRUE(NavigateToURL(web_contents, frame_url));
content::RenderFrameHost* new_host = web_contents->GetPrimaryMainFrame();
ASSERT_TRUE(new_host);
int expected_tab_id = ExtensionTabUtil::GetTabId(web_contents);
int expected_window_id = ExtensionTabUtil::GetWindowIdOfTab(web_contents);
int expected_frame_id = ExtensionApiFrameIdMap::GetFrameId(new_host);
std::string expected_context_id =
ExtensionApiFrameIdMap::GetContextId(new_host).AsLowercaseString();
std::string expected_document_id =
ExtensionApiFrameIdMap::GetDocumentId(new_host).ToString();
std::string expected_frame_url = frame_url.spec();
std::string expected_origin = extension().origin().Serialize();
// Query for tab-based contexts. There should only be one.
base::Value background_contexts = GetContexts(R"({"contextTypes": ["TAB"]})");
// Verify the properties of the returned context.
static constexpr char kExpectedTemplate[] =
R"([{
"contextType": "TAB",
"contextId": "%s",
"tabId": %d,
"windowId": %d,
"frameId": %d,
"documentId": "%s",
"documentUrl": "%s",
"documentOrigin": "%s",
"incognito": false
}])";
std::string expected = base::StringPrintf(
kExpectedTemplate, expected_context_id.c_str(), expected_tab_id,
expected_window_id, expected_frame_id, expected_document_id.c_str(),
expected_frame_url.c_str(), expected_origin.c_str());
EXPECT_THAT(background_contexts, base::test::IsJson(expected));
}
// Tests retrieving offscreen documents with `runtime.getContexts()`.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest, GetOffscreenDocumentContext) {
// Open a new offscreen document.
static constexpr char kOpenOffscreenDocumentScript[] =
R"((async () => {
await chrome.offscreen.createDocument(
{
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'testing'
});
chrome.test.sendScriptResult('done');
})();)";
base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
profile(), extension().id(), kOpenOffscreenDocumentScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
EXPECT_EQ("done", script_result);
OffscreenDocumentManager* offscreen_manager =
OffscreenDocumentManager::Get(profile());
const OffscreenDocumentHost* offscreen_document =
offscreen_manager->GetOffscreenDocumentForExtension(extension());
ASSERT_TRUE(offscreen_document);
content::RenderFrameHost* offscreen_frame_host =
offscreen_document->web_contents()->GetPrimaryMainFrame();
int expected_frame_id =
ExtensionApiFrameIdMap::GetFrameId(offscreen_frame_host);
std::string expected_context_id =
ExtensionApiFrameIdMap::GetContextId(offscreen_frame_host)
.AsLowercaseString();
std::string expected_document_id =
ExtensionApiFrameIdMap::GetDocumentId(offscreen_frame_host).ToString();
std::string expected_frame_url =
extension().GetResourceURL("offscreen.html").spec();
std::string expected_origin = extension().origin().Serialize();
// Query for offscreen document contexts. There should only be one.
base::Value background_contexts =
GetContexts(R"({"contextTypes": ["OFFSCREEN_DOCUMENT"]})");
// Verify the properties of the returned context.
static constexpr char kExpectedTemplate[] =
R"([{
"contextType": "OFFSCREEN_DOCUMENT",
"contextId": "%s",
"tabId": -1,
"windowId": -1,
"frameId": %d,
"documentId": "%s",
"documentUrl": "%s",
"documentOrigin": "%s",
"incognito": false
}])";
std::string expected =
base::StringPrintf(kExpectedTemplate, expected_context_id.c_str(),
expected_frame_id, expected_document_id.c_str(),
expected_frame_url.c_str(), expected_origin.c_str());
EXPECT_THAT(background_contexts, base::test::IsJson(expected));
}
#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/405218955): Support side panel on desktop Android.
class RuntimeGetContextsSidePanelTest : public RuntimeGetContextsApiTest {
public:
void SetUpExtension() override {
static constexpr char kManifest[] =
R"({
"name": "Get Contexts",
"version": "0.1",
"manifest_version": 3,
"permissions": ["sidePanel"],
"side_panel": {
"default_path": "side_panel.html"
},
"action": {},
"background": {
"service_worker": "background.js"
}
})";
test_dir_.WriteManifest(kManifest);
test_dir_.WriteFile(FILE_PATH_LITERAL("background.js"),
"// Intentionally blank");
test_dir_.WriteFile(FILE_PATH_LITERAL("side_panel.html"),
R"(<html>
Hello, side panel!
<script src="side_panel.js"></script>
</html>)");
test_dir_.WriteFile(FILE_PATH_LITERAL("side_panel.js"),
"chrome.test.sendMessage('panel opened');");
extension_ = LoadExtension(test_dir_.UnpackedPath());
ASSERT_TRUE(extension_);
}
};
// Tests retrieving a side panel context from the `runtime.getContexts()` API.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsSidePanelTest, GetSidePanelContext) {
// Set the side panel to open on toolbar action click. This makes it easier
// to trigger.
static constexpr char kSetUpSidePanelScript[] =
R"((async () => {
await chrome.sidePanel.setPanelBehavior(
{openPanelOnActionClick: true});
chrome.test.sendScriptResult('done');
})();)";
base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
profile(), extension().id(), kSetUpSidePanelScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
EXPECT_EQ("done", script_result);
// Click on the toolbar action and wait for the panel context to open.
ExtensionTestMessageListener panel_listener("panel opened");
ExtensionActionTestHelper::Create(browser())->Press(extension().id());
ASSERT_TRUE(panel_listener.WaitUntilSatisfied());
// Fetch the side panel host.
ExtensionHostRegistry* host_registry = ExtensionHostRegistry::Get(profile());
std::vector<ExtensionHost*> hosts =
host_registry->GetHostsForExtension(extension().id());
ASSERT_EQ(1u, hosts.size());
ExtensionHost* panel_host = hosts[0];
EXPECT_EQ(mojom::ViewType::kExtensionSidePanel,
panel_host->extension_host_type());
content::RenderFrameHost* panel_frame_host =
panel_host->web_contents()->GetPrimaryMainFrame();
// Verify the `runtime.getContexts()` API can retrieve the context and that
// the proper values are returned.
int expected_frame_id = ExtensionApiFrameIdMap::GetFrameId(panel_frame_host);
std::string expected_context_id =
ExtensionApiFrameIdMap::GetContextId(panel_frame_host)
.AsLowercaseString();
std::string expected_document_id =
ExtensionApiFrameIdMap::GetDocumentId(panel_frame_host).ToString();
std::string expected_frame_url =
extension().GetResourceURL("side_panel.html").spec();
std::string expected_origin = extension().origin().Serialize();
base::Value side_panel_contexts =
GetContexts(R"({"contextTypes": ["SIDE_PANEL"]})");
// Verify the properties of the returned context.
static constexpr char kExpectedTemplate[] =
R"([{
"contextType": "SIDE_PANEL",
"contextId": "%s",
"tabId": -1,
"windowId": -1,
"frameId": %d,
"documentId": "%s",
"documentUrl": "%s",
"documentOrigin": "%s",
"incognito": false
}])";
std::string expected =
base::StringPrintf(kExpectedTemplate, expected_context_id.c_str(),
expected_frame_id, expected_document_id.c_str(),
expected_frame_url.c_str(), expected_origin.c_str());
EXPECT_THAT(side_panel_contexts, base::test::IsJson(expected));
}
#endif // !BUILDFLAG(IS_ANDROID)
// Tests the behavior of `runtime.getContexts()` with a split-mode incognito
// extension. In split mode, the extension should only be able to access data
// about its own process's contexts.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest,
RetrievingIncognitoContexts_SplitMode) {
// Load up a split-mode extension.
static constexpr char kManifest[] =
R"({
"name": "Split mode extension",
"version": "0.1",
"manifest_version": 3,
"incognito": "split",
"background": {"service_worker": "background.js"}
})";
// Since we need to wait for the incognito profile's separate service worker
// to start, our bootstrapping code in LoadExtension() doesn't automatically
// handle it for us. Include a separate "ready" message.
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
"chrome.test.sendMessage('ready');");
test_dir.WriteFile(FILE_PATH_LITERAL("regular.html"), "<html>Regular</html>");
test_dir.WriteFile(FILE_PATH_LITERAL("incognito.html"),
"<html>Incognito</html>");
ExtensionTestMessageListener ready_listener("ready");
const Extension* extension =
LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
// Open a tab on-the-record to one of the extension's pages.
auto* web_contents = GetActiveWebContents();
GURL regular_url = extension->GetResourceURL("regular.html");
ASSERT_TRUE(NavigateToURL(web_contents, regular_url));
ASSERT_TRUE(web_contents->GetPrimaryMainFrame());
// Open up an incognito tab to another extension page, and wait for the
// incognito version of the extension to start up.
ready_listener.Reset();
GURL incognito_url = extension->GetResourceURL("incognito.html");
content::WebContents* incognito_web_contents =
PlatformOpenURLOffTheRecord(profile(), incognito_url);
ASSERT_TRUE(incognito_web_contents->GetPrimaryMainFrame());
ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
// A helper method to retrieve the contexts for the given `browser_context`.
auto run_get_contexts =
[extension](content::BrowserContext* browser_context) {
static constexpr char kScript[] =
R"((async () => {
chrome.test.sendScriptResult(
await chrome.runtime.getContexts({}));
})();)";
return BackgroundScriptExecutor::ExecuteScript(
browser_context, extension->id(), kScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
};
{
// Verify the on-the-record contexts. There should be a single background
// context and the on-the-record tab.
base::Value regular_results = run_get_contexts(profile());
std::vector<api::runtime::ExtensionContext> contexts =
ContextValueToContextStructs(regular_results);
EXPECT_THAT(contexts, testing::UnorderedElementsAre(
GetBackgroundMatcher(),
GetFrameMatcher(api::runtime::ContextType::kTab,
regular_url)));
}
{
// Now verify the incognito contexts. Here, too, there should be a single
// background context and tab, but it should be the incognito tab.
base::Value incognito_results =
run_get_contexts(incognito_web_contents->GetBrowserContext());
std::vector<api::runtime::ExtensionContext> contexts =
ContextValueToContextStructs(incognito_results);
EXPECT_THAT(contexts, testing::UnorderedElementsAre(
GetBackgroundMatcher(),
GetFrameMatcher(api::runtime::ContextType::kTab,
incognito_url)));
}
}
// Tests the behavior of `runtime.getContexts()` with a spanning-mode incognito
// extension.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest,
RetrievingIncognitoContexts_SpanningMode) {
ASSERT_TRUE(StartEmbeddedTestServer());
// Load up a spanning mode extension. See comment below for why we have
// a web accessible resource.
static constexpr char kManifest[] =
R"({
"name": "Split mode extension",
"version": "0.1",
"manifest_version": 3,
"incognito": "spanning",
"web_accessible_resources": [{
"resources": ["incognito.html"],
"matches": ["*://example.com/*"]
}],
"background": {"service_worker": "background.js"}
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
"// Intentionally blank");
test_dir.WriteFile(FILE_PATH_LITERAL("regular.html"), "<html>Regular</html>");
test_dir.WriteFile(FILE_PATH_LITERAL("incognito.html"),
"<html>Incognito</html>");
const Extension* extension =
LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
// Open an on-the-record tab to an extension page.
auto* web_contents = GetActiveWebContents();
GURL regular_url = extension->GetResourceURL("regular.html");
ASSERT_TRUE(NavigateToURL(web_contents, regular_url));
// Now, the tricky part. Spanning mode extensions aren't, typically, allowed
// to open contexts in an incognito profile (which means all contexts just
// open in the same profile). There's one exception to this: an embedded web-
// accessible iframe in an incognito tab. Make it so.
GURL incognito_url = extension->GetResourceURL("incognito.html");
auto* incognito_web_contents = PlatformOpenURLOffTheRecord(
profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
// Inject a script to add an iframe and navigate it to the extension's
// web-accessible resource.
content::RenderFrameHost* incognito_main_frame =
incognito_web_contents->GetPrimaryMainFrame();
static constexpr char kNavigateTemplate[] =
R"(let frame = document.createElement('iframe');
frame.src = '%s';
new Promise(resolve => {
frame.onload = () => { resolve('success'); };
frame.onerror = (e) => {
resolve('failure: ' + e.toString());
};
document.body.appendChild(frame);
});
)";
EXPECT_EQ("success",
content::EvalJs(incognito_main_frame,
base::StringPrintf(kNavigateTemplate,
incognito_url.spec().c_str())));
// Verify the frame loaded properly by checking both the URL and the content.
content::RenderFrameHost* incognito_extension_frame =
content::ChildFrameAt(incognito_main_frame, 0);
ASSERT_TRUE(incognito_extension_frame);
EXPECT_EQ(incognito_url, incognito_extension_frame->GetLastCommittedURL());
EXPECT_EQ("Incognito", content::EvalJs(incognito_extension_frame,
"document.body.textContent;"));
// A helper method to retrieve the contexts for the given `profile`.
auto run_get_contexts_in_profile = [extension](Profile* profile) {
static constexpr char kScript[] =
R"((async () => {
chrome.test.sendScriptResult(
await chrome.runtime.getContexts({}));
})();)";
return BackgroundScriptExecutor::ExecuteScript(
profile, extension->id(), kScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
};
{
// Verify the results for the on-the-record profile. Since the extension
// is in spanning mode, this is effectively the only instance of the
// extension. It should see the background context and the on-the-record
// tab, but not the embedded frame.
base::Value regular_results = run_get_contexts_in_profile(profile());
std::vector<api::runtime::ExtensionContext> contexts =
ContextValueToContextStructs(regular_results);
EXPECT_THAT(contexts, testing::UnorderedElementsAre(
GetBackgroundMatcher(),
GetFrameMatcher(api::runtime::ContextType::kTab,
regular_url)));
}
}
#if !BUILDFLAG(IS_ANDROID)
// This is a manifest V2 test meant to ensure test coverage for
// chrome.extension.getURL, which is deprecated and unavailable
// in MV3.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, GetExtensionURL) {
static constexpr char kManifest[] = R"(
{
"name": "chrome.extension.getURL",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2
})";
static constexpr char kScript[] = R"(
chrome.test.assertEq(
chrome.extension.getURL('foo.html'),
chrome.runtime.getURL('foo.html'));
chrome.test.notifyPass();
)";
ResultCatcher catcher;
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("background.js"), kScript);
const Extension* extension = LoadExtension(dir.UnpackedPath());
ASSERT_TRUE(extension);
EXPECT_TRUE(catcher.GetNextResult());
}
// Tests retrieving contexts when developer tools are opened.
// TODO(crbug.com/402538127): Improve devtools support on desktop Android.
class GetContextsWithDeveloperToolsOpened
: public RuntimeGetContextsApiTest,
public testing::WithParamInterface<bool> {
public:
GetContextsWithDeveloperToolsOpened() = default;
~GetContextsWithDeveloperToolsOpened() override = default;
GetContextsWithDeveloperToolsOpened(
const GetContextsWithDeveloperToolsOpened&) = delete;
GetContextsWithDeveloperToolsOpened& operator=(
const GetContextsWithDeveloperToolsOpened&) = delete;
};
// TODO(crbug.com/357845909): flaky on ChromeOS and Linux MSAN.
#if defined(MEMORY_SANITIZER) && (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS))
#define MAYBE_ReturnsDevToolsContext DISABLED_ReturnsDevToolsContext
#else
#define MAYBE_ReturnsDevToolsContext ReturnsDevToolsContext
#endif
IN_PROC_BROWSER_TEST_P(GetContextsWithDeveloperToolsOpened,
MAYBE_ReturnsDevToolsContext) {
const bool open_docked = GetParam();
// Open the developer tools and wait for the extension page to be loaded.
ExtensionTestMessageListener listener("devtools page opened");
content::WebContents* inspected_web_contents = GetActiveWebContents();
DevToolsWindow* devtools_window =
DevToolsWindowTesting::OpenDevToolsWindowSync(inspected_web_contents,
open_docked);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Assert the docked state of developer tools.
content::WebContents* devtools_web_contents =
DevToolsWindowTesting::Get(devtools_window)->main_web_contents();
bool is_docked = devtools_web_contents->GetTopLevelNativeWindow() ==
browser()->window()->GetNativeWindow();
ASSERT_EQ(open_docked, is_docked);
// Extract the extension host from the devtools web contents.
GURL expected_frame_url = extension().GetResourceURL("devtools.html");
auto is_extension_frame =
[expected_frame_url](content::RenderFrameHost* rfh) {
return rfh->GetLastCommittedURL() == expected_frame_url;
};
content::RenderFrameHost* extension_host = content::FrameMatchingPredicate(
devtools_web_contents->GetPrimaryPage(),
base::BindLambdaForTesting(is_extension_frame));
// Setup the expected values for the context. Only one tab-based context
// should be returned by chrome.runtime.getContexts().
int expected_tab_id = -1;
int expected_window_id = ExtensionTabUtil::GetWindowIdOfTab(
is_docked ? inspected_web_contents : devtools_web_contents);
int expected_frame_id = -1;
std::string expected_context_id =
ExtensionApiFrameIdMap::GetContextId(extension_host).AsLowercaseString();
std::string expected_document_id =
ExtensionApiFrameIdMap::GetDocumentId(extension_host).ToString();
std::string expected_origin = extension().origin().Serialize();
static constexpr char kExpectedTemplate[] =
R"([{
"contextType": "DEVELOPER_TOOLS",
"contextId": "%s",
"tabId": %d,
"windowId": %d,
"frameId": %d,
"documentId": "%s",
"documentUrl": "%s",
"documentOrigin": "%s",
"incognito": false
}])";
std::string expected_contexts = base::StringPrintf(
kExpectedTemplate, expected_context_id.c_str(), expected_tab_id,
expected_window_id, expected_frame_id, expected_document_id.c_str(),
expected_frame_url.spec().c_str(), expected_origin.c_str());
// Verify the result of chrome.runtime.getContexts().
base::Value contexts =
GetContexts(R"({"contextTypes": ["DEVELOPER_TOOLS"]})");
EXPECT_THAT(contexts, base::test::IsJson(expected_contexts));
}
// Test for undocked developer tools.
INSTANTIATE_TEST_SUITE_P(UndockedDevTools,
GetContextsWithDeveloperToolsOpened,
::testing::Values(false) /* open_docked */);
// Test for docked developer tools. This is also a regression test for
// crbug.com/355625882.
INSTANTIATE_TEST_SUITE_P(DockedDevTools,
GetContextsWithDeveloperToolsOpened,
::testing::Values(true) /* open_docked */);
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace extensions