| // 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 |