blob: 04b6f7ec3e6e8a48fb74058920fa3f42599f59cb [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/test/base/android/android_browser_test.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/declarative_net_request/rules_monitor_service.h"
#include "extensions/browser/api/declarative_net_request/test_utils.h"
#include "extensions/browser/embedder_user_script_loader.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/user_script_manager.h"
#include "extensions/common/extension.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_handlers/content_scripts_handler.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_content_script_load_waiter.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
namespace extensions {
// Smoke tests for the experimental desktop Android build, which supports
// extensions. See https://crbug.com/356905053
class DesktopAndroidExtensionsBrowserTest : public AndroidBrowserTest {
public:
DesktopAndroidExtensionsBrowserTest() = default;
~DesktopAndroidExtensionsBrowserTest() override = default;
content::WebContents* GetActiveWebContents() {
return chrome_test_utils::GetActiveWebContents(this);
}
void SetUpOnMainThread() override {
AndroidBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
// Returns the main `BrowserContext`.
content::BrowserContext* GetBrowserContext() {
return GetActiveWebContents()->GetBrowserContext();
}
// Attempts to parse and load an extension from the given `file_path` and add
// it to the extensions system (which will also activate the extension).
// Returns the extension on success; on failure, returns null.
scoped_refptr<const Extension> LoadExtensionFromDirectory(
const base::FilePath& file_path) {
ChromeTestExtensionLoader loader(GetBrowserContext());
return loader.LoadExtension(file_path);
}
};
// The following is a simple test exercising a basic navigation and script
// injection. This doesn't exercise any extensions logic, but ensures Chrome
// successfully starts and can navigate the web.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest, SanityCheck) {
ASSERT_EQ(TabModelList::models().size(), 1u);
EXPECT_TRUE(content::NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("example.com", "/title1.html")));
EXPECT_EQ("This page has no title.",
content::EvalJs(GetActiveWebContents(), "document.body.innerText"));
}
// Tests the ability to parse and validate a simple extension.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
ParseAndValidateASimpleExtension) {
static constexpr char kManifest[] =
R"({
"name": "Test Extension",
"version": "0.1",
"manifest_version": 3
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
// Validate the fields in the extension.
EXPECT_EQ("Test Extension", extension->name());
EXPECT_EQ("0.1", extension->version().GetString());
EXPECT_EQ(3, extension->manifest_version());
}
// Tests the adding an extension to the registry and navigating to a
// corresponding page in the extension, verifying the expected content, and
// leveraging the chrome.test API to pass a result. The latter verifies the
// core extension bindings system and API handling works, including
// exercising custom bindings.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
NavigateToExtensionPage) {
static constexpr char kManifest[] =
R"({
"name": "Test Extension",
"version": "0.1",
"manifest_version": 3
})";
static constexpr char kPageHtml[] =
R"(<html>
Hello, world
<script src="page.js"></script>
</html>)";
static constexpr char kPageJs[] =
R"(chrome.test.runTests([
function sanityCheck() {
chrome.test.assertEq(2, 1 + 1);
chrome.test.succeed();
}
]);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs);
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
GURL extension_page = extension->GetResourceURL("page.html");
ResultCatcher result_catcher;
EXPECT_TRUE(content::NavigateToURL(GetActiveWebContents(), extension_page));
EXPECT_EQ(extension_page, GetActiveWebContents()->GetLastCommittedURL());
EXPECT_EQ("Hello, world",
content::EvalJs(GetActiveWebContents(), "document.body.innerText"));
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
// Test service worker-based extensions properly load and have the service
// worker initialize and run.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
ServiceWorkerBasedExtension) {
static constexpr char kManifest[] =
R"({
"name": "Test Extension",
"version": "0.1",
"manifest_version": 3,
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackgroundJs[] =
R"(chrome.test.runTests([
function sanityCheck() {
chrome.test.assertEq(2, 1 + 1);
chrome.test.succeed();
}
]);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
ResultCatcher result_catcher;
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
// Tests the declarative net request API in extension service workers.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
DeclarativeNetRequestSupport) {
// Load a simple extension that redirects from example.com -> google.com.
static constexpr char kManifest[] =
R"({
"name": "My Test Extension",
"manifest_version": 3,
"version": "0.1",
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset",
"enabled": true,
"path": "rules.json"
}]
},
"permissions": ["declarativeNetRequest"],
"host_permissions": ["*://example.com/*"]
})";
// One rule, which is a redirect from example.com to
// http://google.com:<portid>/title2.html.
static constexpr char kRulesJson[] =
R"([{
"id": 1,
"priority": 1,
"action": {
"type": "redirect",
"redirect": { "url": "http://google.com:%d/title2.html" }
},
"condition": {
"urlFilter": "example.com",
"resourceTypes": ["main_frame"]
}
}])";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(
FILE_PATH_LITERAL("rules.json"),
base::StringPrintf(kRulesJson, embedded_test_server()->port()));
declarative_net_request::RulesetManagerObserver ruleset_manager_observer(
declarative_net_request::RulesMonitorService::Get(GetBrowserContext())
->ruleset_manager());
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ruleset_manager_observer.WaitForExtensionsWithRulesetsCount(1);
GURL url = embedded_test_server()->GetURL("example.com", "/title1.html");
GURL expected_url =
embedded_test_server()->GetURL("google.com", "/title2.html");
EXPECT_TRUE(
content::NavigateToURL(GetActiveWebContents(), url, expected_url));
EXPECT_EQ(expected_url, GetActiveWebContents()->GetLastCommittedURL());
EXPECT_EQ("This page has a title.",
content::EvalJs(GetActiveWebContents(), "document.body.innerText"));
}
// Verifies content scripts are properly injected.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
ContentScriptInjection) {
// An extension that injects a simple script on match.test sites.
static constexpr char kManifest[] =
R"({
"name": "Content Script Extension",
"manifest_version": 3,
"version": "0.1",
"content_scripts": [{
"matches": ["*://match.test/*"],
"js": ["content_script.js"],
"run_at": "document_idle"
}]
})";
// The script just appends a span to indicate it injected.
static constexpr char kContentScriptJs[] =
R"(let span = document.createElement('span');
span.textContent = 'content script injected';
document.body.appendChild(span);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScriptJs);
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
// Verify scripts were properly parsed.
const UserScriptList& content_scripts =
ContentScriptsInfo::GetContentScripts(extension.get());
ASSERT_EQ(1u, content_scripts.size());
// Wait for scripts to load (if they haven't already).
UserScriptManager* user_script_manager =
ExtensionSystem::Get(GetBrowserContext())->user_script_manager();
ExtensionUserScriptLoader* user_script_loader =
user_script_manager->GetUserScriptLoaderForExtension(extension->id());
if (!user_script_loader->HasLoadedScripts()) {
ContentScriptLoadWaiter waiter(user_script_loader);
waiter.Wait();
}
// First, navigate to a site that *doesn't* match the extension. The script
// should not inject.
const GURL no_match_test =
embedded_test_server()->GetURL("no-match.test", "/title1.html");
EXPECT_TRUE(content::NavigateToURL(GetActiveWebContents(), no_match_test));
EXPECT_EQ(no_match_test, GetActiveWebContents()->GetLastCommittedURL());
EXPECT_EQ("This page has no title.",
content::EvalJs(GetActiveWebContents(), "document.body.innerText"));
// Next, navigate to a site that *does* match. The script should inject.
const GURL match_test =
embedded_test_server()->GetURL("match.test", "/title1.html");
EXPECT_TRUE(content::NavigateToURL(GetActiveWebContents(), match_test));
EXPECT_EQ(match_test, GetActiveWebContents()->GetLastCommittedURL());
EXPECT_EQ("This page has no title. content script injected",
content::EvalJs(GetActiveWebContents(), "document.body.innerText"));
}
// Tests Storage API for StorageArea.local.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
StorageApiTestStorageAreaLocal) {
static constexpr char kManifest[] =
R"({
"name": "StorageArea.OnChanged",
"version": "0.1",
"manifest_version": 3,
"permissions": ["storage"],
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackgroundJs[] =
R"(chrome.test.runTests([
function storageAreaOnChanged() {
var localStorageArea = chrome.storage.local;
chrome.test.listenOnce(localStorageArea.onChanged,
function(changes) {
chrome.test.assertEq({key:{newValue:'value'}}, changes);
});
chrome.test.listenOnce(chrome.storage.onChanged,
function(changes, namespace) {
chrome.test.assertEq({key:{newValue:'value'}}, changes);
chrome.test.assertEq('local', namespace);
});
chrome.storage.session.onChanged.addListener(
function(changes, namespace) {
chrome.test.notifyFail(
'session.onChanged should not be called when local storage update');
});
localStorageArea.set({key: 'value'});
}
]);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
ResultCatcher result_catcher;
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
// Tests Storage API for StorageArea.session.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
StorageApiTestStorageAreaSession) {
static constexpr char kManifest[] =
R"({
"name": "StorageArea.OnChanged",
"version": "0.1",
"manifest_version": 3,
"permissions": ["storage"],
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackgroundJs[] =
R"(chrome.test.runTests([
function storageAreaOnChanged() {
var sessionStorageArea = chrome.storage.session;
chrome.test.listenOnce(sessionStorageArea.onChanged,
function(changes) {
chrome.test.assertEq({key:{newValue:'value'}}, changes);
});
chrome.test.listenOnce(chrome.storage.onChanged,
function(changes, namespace) {
chrome.test.assertEq({key:{newValue:'value'}}, changes);
chrome.test.assertEq('session', namespace);
});
chrome.storage.local.onChanged.addListener(
function(changes, namespace) {
chrome.test.notifyFail(
'local.onChanged should not be called when session storage update');
});
sessionStorageArea.set({key: 'value'});
}
]);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
ResultCatcher result_catcher;
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
// Tests reading a chrome.extension.* property.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest,
ExtensionPropertyRead) {
static constexpr char kManifest[] =
R"({
"name": "chrome.extension property read",
"version": "0.1",
"manifest_version": 3,
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackgroundJs[] =
R"(chrome.test.runTests([
function readProperty() {
// Test that we can read a property.
let incognito = chrome.extension.inIncognitoContext;
chrome.test.assertFalse(incognito);
chrome.test.succeed();
}
]);)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
ResultCatcher result_catcher;
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
// Tests passing a basic message between extension contexts.
IN_PROC_BROWSER_TEST_F(DesktopAndroidExtensionsBrowserTest, MessagePassing) {
static constexpr char kManifest[] =
R"({
"name": "Message passing",
"version": "0.1",
"manifest_version": 3,
"background": {"service_worker": "background.js"}
})";
// A service worker that will send a message and wait for a reply.
static constexpr char kBackgroundJs[] =
R"(chrome.test.runTests([
async function sendMessage() {
// We wait for the C++ side to be ready; this allows us to open a
// tab to listen to the message we'll send.
await chrome.test.sendMessage('ready');
const reply = await chrome.runtime.sendMessage('ping');
chrome.test.assertEq('pong', reply);
chrome.test.succeed();
},
]);)";
// This is a basic page that will reply to an incoming message.
static constexpr char kPageHtml[] =
R"(<html><script src="page.js"></script></html>)";
static constexpr char kPageJs[] =
R"(chrome.runtime.onMessage.addListener(async(msg, sender, sendReply) => {
chrome.test.assertEq(msg, 'ping');
sendReply('pong');
});)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs);
test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
ResultCatcher result_catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
scoped_refptr<const Extension> extension =
LoadExtensionFromDirectory(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Open a tab to the listening page, and reply to the extension when it's
// loaded.
GURL extension_page = extension->GetResourceURL("page.html");
EXPECT_TRUE(content::NavigateToURL(GetActiveWebContents(), extension_page));
EXPECT_TRUE(content::WaitForLoadStop(GetActiveWebContents()));
EXPECT_EQ(extension_page, GetActiveWebContents()->GetLastCommittedURL());
listener.Reply("done");
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
} // namespace extensions