| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/api/extension_action/extension_action_api.h" |
| #include "chrome/browser/extensions/api/extension_action/test_icon_image_observer.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/test_extension_action_dispatcher_observer.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/extensions/extension_action_test_helper.h" |
| #include "chrome/browser/ui/extensions/extensions_container.h" |
| #include "chrome/browser/ui/tabs/tab_enums.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/toolbar/toolbar_actions_model.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "components/version_info/channel.h" |
| #include "content/public/browser/render_frame_host.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_utils.h" |
| #include "extensions/browser/background_script_executor.h" |
| #include "extensions/browser/extension_action.h" |
| #include "extensions/browser/extension_action_manager.h" |
| #include "extensions/browser/extension_icon_image.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/script_executor.h" |
| #include "extensions/browser/service_worker/service_worker_test_utils.h" |
| #include "extensions/browser/state_store.h" |
| #include "extensions/common/api/extension_action/action_info.h" |
| #include "extensions/common/api/extension_action/action_info_test_util.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/result_catcher.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "ui/base/window_open_disposition.h" |
| #include "ui/gfx/color_utils.h" |
| |
| namespace extensions { |
| namespace { |
| |
| // A background script that allows for setting the icon dynamically. |
| constexpr char kSetIconBackgroundJsTemplate[] = |
| R"(function setIcon(details) { |
| chrome.%s.setIcon(details, () => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| }); |
| } |
| function setIconPromise(details) { |
| chrome.%s.setIcon(details) |
| .then(chrome.test.notifyPass) |
| .catch(chrome.test.notifyFail); |
| })"; |
| |
| constexpr char kPageHtmlTemplate[] = |
| R"(<html><script src="page.js"></script></html>)"; |
| |
| // Runs |script| in the given |web_contents| and waits for it to send a |
| // test-passed result. This will fail if the test in |script| fails. Note: |
| // |web_contents| is expected to be an extension contents with access to |
| // extension APIs. |
| void RunTestAndWaitForSuccess(content::WebContents* web_contents, |
| const std::string& script) { |
| SCOPED_TRACE(script); |
| ResultCatcher result_catcher; |
| content::ExecuteScriptAsync(web_contents, script); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| // A helper class to track StateStore changes. |
| class TestStateStoreObserver : public StateStore::TestObserver { |
| public: |
| TestStateStoreObserver(content::BrowserContext* context, |
| const ExtensionId& extension_id) |
| : extension_id_(extension_id) { |
| scoped_observation_.Observe(ExtensionSystem::Get(context)->state_store()); |
| } |
| |
| TestStateStoreObserver(const TestStateStoreObserver&) = delete; |
| TestStateStoreObserver& operator=(const TestStateStoreObserver&) = delete; |
| |
| ~TestStateStoreObserver() override = default; |
| |
| void WillSetExtensionValue(const ExtensionId& extension_id, |
| const std::string& key) override { |
| if (extension_id == extension_id_) |
| ++updated_values_[key]; |
| } |
| |
| int CountForKey(const std::string& key) const { |
| auto iter = updated_values_.find(key); |
| return iter == updated_values_.end() ? 0 : iter->second; |
| } |
| |
| private: |
| ExtensionId extension_id_; |
| std::map<std::string, int> updated_values_; |
| |
| base::ScopedObservation<StateStore, StateStore::TestObserver> |
| scoped_observation_{this}; |
| }; |
| |
| // A helper class to handle setting or getting the values for an action from JS. |
| class ActionTestHelper { |
| public: |
| ActionTestHelper(const char* api_name, |
| const char* set_method_name, |
| const char* get_method_name, |
| const char* js_property_key, |
| content::WebContents* web_contents) |
| : api_name_(api_name), |
| set_method_name_(set_method_name), |
| get_method_name_(get_method_name), |
| js_property_key_(js_property_key), |
| web_contents_(web_contents) {} |
| |
| ActionTestHelper(const ActionTestHelper&) = delete; |
| ActionTestHelper& operator=(const ActionTestHelper&) = delete; |
| |
| ~ActionTestHelper() = default; |
| |
| // Checks the value for the given |tab_id|. |
| void CheckValueForTab(const char* expected_js_value, int tab_id) const { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s({tabId: %d}, (res) => { |
| chrome.test.assertNoLastError(); |
| chrome.test.assertEq(%s, res); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents_, |
| base::StringPrintf(kScriptTemplate, api_name_, get_method_name_, tab_id, |
| expected_js_value)); |
| } |
| |
| // Checks the default value. |
| void CheckDefaultValue(const char* expected_js_value) const { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s({}, (res) => { |
| chrome.test.assertNoLastError(); |
| chrome.test.assertEq(%s, res); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents_, base::StringPrintf(kScriptTemplate, api_name_, |
| get_method_name_, expected_js_value)); |
| } |
| |
| // Sets the value for a given |tab_id|. |
| void SetValueForTab(const char* new_js_value, int tab_id) const { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s({tabId: %d, %s: %s}, () => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents_, |
| base::StringPrintf(kScriptTemplate, api_name_, set_method_name_, tab_id, |
| js_property_key_, new_js_value)); |
| } |
| |
| // Sets the default value. |
| void SetDefaultValue(const char* new_js_value) const { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s({%s: %s}, () => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents_, |
| base::StringPrintf(kScriptTemplate, api_name_, set_method_name_, |
| js_property_key_, new_js_value)); |
| } |
| |
| private: |
| // The name of the api (e.g., "action"). |
| const char* const api_name_; |
| // The name of the method to call to set the value (e.g., "setPopup"). |
| const char* const set_method_name_; |
| // The name of the method to call to get the value (e.g., "getPopup"). |
| const char* const get_method_name_; |
| // The name of the property in the set method details (e.g., "popup"). |
| const char* const js_property_key_; |
| // The WebContents to use to execute API calls. |
| const raw_ptr<content::WebContents> web_contents_; |
| }; |
| |
| // Forces a flush of the StateStore, where action state is persisted. |
| void FlushStateStore(Profile* profile) { |
| base::RunLoop run_loop; |
| ExtensionSystem::Get(profile)->state_store()->FlushForTesting( |
| run_loop.QuitWhenIdleClosure()); |
| run_loop.Run(); |
| } |
| |
| } // namespace |
| |
| // A class that allows for cross-origin navigations with embedded test server. |
| class ExtensionActionAPITest : public ExtensionApiTest { |
| protected: |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| } |
| }; |
| |
| // Alias these for readability, when a test only exercises one type of action. |
| using BrowserActionAPITest = ExtensionActionAPITest; |
| using PageActionAPITest = ExtensionActionAPITest; |
| |
| // A class that runs tests exercising each type of possible toolbar action. |
| class MultiActionAPITest |
| : public ExtensionActionAPITest, |
| public testing::WithParamInterface<ActionInfo::Type> { |
| public: |
| MultiActionAPITest() = default; |
| |
| // Returns true if the |action| has whatever state its default is on the |
| // tab with the given |tab_id|. |
| bool ActionHasDefaultState(const ExtensionAction& action, int tab_id) const { |
| bool is_visible = action.GetIsVisible(tab_id); |
| bool default_is_visible = |
| action.default_state() == ActionInfo::DefaultState::kEnabled; |
| return is_visible == default_is_visible; |
| } |
| |
| // Ensures the |action| is enabled on the tab with the given |tab_id|. |
| void EnsureActionIsEnabledOnTab(ExtensionAction* action, int tab_id) { |
| if (action->GetIsVisible(tab_id)) |
| return; |
| action->SetIsVisible(tab_id, true); |
| // Just setting the state on the action doesn't update the UI. Ensure |
| // observers are notified. |
| ExtensionActionDispatcher* dispatcher = |
| ExtensionActionDispatcher::Get(profile()); |
| dispatcher->NotifyChange(action, GetActiveTab(), profile()); |
| } |
| |
| // Ensures the |action| is enabled on the currently-active tab. |
| void EnsureActionIsEnabledOnActiveTab(ExtensionAction* action) { |
| EnsureActionIsEnabledOnTab(action, GetActiveTabId()); |
| } |
| |
| // Returns the id of the currently-active tab. |
| int GetActiveTabId() const { |
| content::WebContents* web_contents = GetActiveTab(); |
| return sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| } |
| |
| content::WebContents* GetActiveTab() const { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| // Returns the action associated with |extension|. |
| ExtensionAction* GetExtensionAction(const Extension& extension) { |
| auto* action_manager = ExtensionActionManager::Get(profile()); |
| return action_manager->GetExtensionAction(extension); |
| } |
| }; |
| |
| // Canvas tests rely on the harness producing pixel output in order to read back |
| // pixels from a canvas element. So we have to override the setup function. |
| class MultiActionAPICanvasTest : public MultiActionAPITest { |
| public: |
| void SetUp() override { |
| EnablePixelOutput(); |
| MultiActionAPITest::SetUp(); |
| } |
| }; |
| |
| // Check that updating the browser action badge for a specific tab id does not |
| // cause a disk write (since we only persist the defaults). |
| // Only browser actions persist settings. |
| IN_PROC_BROWSER_TEST_F(BrowserActionAPITest, TestNoUnnecessaryIO) { |
| ExtensionTestMessageListener ready_listener("ready"); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Extension", |
| "description": "An extension", |
| "manifest_version": 2, |
| "version": "0.1", |
| "browser_action": {}, |
| "background": { "scripts": ["background.js"] } |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| "chrome.test.sendMessage('ready');"); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| // The script template to update the browser action. |
| static constexpr char kUpdate[] = |
| R"(chrome.browserAction.setBadgeText(%s); |
| chrome.test.sendScriptResult('pass');)"; |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| SessionID tab_id = sessions::SessionTabHelper::IdForTab(web_contents); |
| static constexpr char kBrowserActionKey[] = "browser_action"; |
| TestStateStoreObserver test_state_store_observer(profile(), extension->id()); |
| |
| { |
| TestExtensionActionDispatcherObserver test_observer(profile(), |
| extension->id()); |
| // First, update a specific tab. |
| std::string update_options = |
| base::StringPrintf("{text: 'New Text', tabId: %d}", tab_id.id()); |
| EXPECT_EQ("pass", ExecuteScriptInBackgroundPage( |
| extension->id(), |
| base::StringPrintf(kUpdate, update_options.c_str()))); |
| test_observer.Wait(); |
| |
| // The action update should be associated with the specific tab. |
| EXPECT_EQ(web_contents, test_observer.last_web_contents()); |
| // Since this was only updating a specific tab, this should *not* result in |
| // a StateStore write. We should only write to the StateStore with new |
| // default values. |
| EXPECT_EQ(0, test_state_store_observer.CountForKey(kBrowserActionKey)); |
| } |
| |
| { |
| TestExtensionActionDispatcherObserver test_observer(profile(), |
| extension->id()); |
| // Next, update the default badge text. |
| EXPECT_EQ("pass", |
| ExecuteScriptInBackgroundPage( |
| extension->id(), |
| base::StringPrintf(kUpdate, "{text: 'Default Text'}"))); |
| test_observer.Wait(); |
| // The action update should not be associated with a specific tab. |
| EXPECT_EQ(nullptr, test_observer.last_web_contents()); |
| |
| // This *should* result in a StateStore write, since we persist the default |
| // state of the extension action. |
| EXPECT_EQ(1, test_state_store_observer.CountForKey(kBrowserActionKey)); |
| } |
| } |
| |
| // Verify that tab-specific values are cleared on navigation and on tab |
| // removal. Regression test for https://crbug.com/834033. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, |
| ValuesAreClearedOnNavigationAndTabRemoval) { |
| TestExtensionDir test_dir; |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Extension", |
| "description": "An extension", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": {} |
| })"; |
| |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| auto* action_manager = ExtensionActionManager::Get(profile()); |
| ExtensionAction* action = action_manager->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| GURL initial_url = embedded_test_server()->GetURL("/title1.html"); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), initial_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| |
| TabStripModel* tab_strip_model = browser()->tab_strip_model(); |
| content::WebContents* web_contents = tab_strip_model->GetActiveWebContents(); |
| int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| |
| // There should be no explicit title to start, but should be one if we set |
| // one. |
| EXPECT_FALSE(action->HasTitle(tab_id)); |
| action->SetTitle(tab_id, "alpha"); |
| EXPECT_TRUE(action->HasTitle(tab_id)); |
| |
| // Navigating should clear the title. |
| GURL second_url = embedded_test_server()->GetURL("/title2.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), second_url)); |
| |
| EXPECT_EQ(second_url, web_contents->GetLastCommittedURL()); |
| EXPECT_FALSE(action->HasTitle(tab_id)); |
| |
| action->SetTitle(tab_id, "alpha"); |
| { |
| content::WebContentsDestroyedWatcher destroyed_watcher(web_contents); |
| tab_strip_model->CloseWebContentsAt(tab_strip_model->active_index(), |
| TabCloseTypes::CLOSE_NONE); |
| destroyed_watcher.Wait(); |
| } |
| // The title should have been cleared on tab removal as well. |
| EXPECT_FALSE(action->HasTitle(tab_id)); |
| } |
| |
| // Tests that tooltips of an extension action icon can be specified using UTF8. |
| // See http://crbug.com/25349. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, TitleLocalization) { |
| TestExtensionDir test_dir; |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Hreggvi\u00F0ur is my name", |
| "description": "Hreggvi\u00F0ur: l10n action", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": { |
| "default_title": "Hreggvi\u00F0ur" |
| } |
| })"; |
| |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| auto* action_manager = ExtensionActionManager::Get(profile()); |
| ExtensionAction* action = action_manager->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| EXPECT_EQ(base::WideToUTF8(L"Hreggvi\u00F0ur: l10n action"), |
| extension->description()); |
| EXPECT_EQ(base::WideToUTF8(L"Hreggvi\u00F0ur is my name"), extension->name()); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| EXPECT_EQ(base::WideToUTF8(L"Hreggvi\u00F0ur"), action->GetTitle(tab_id)); |
| EXPECT_EQ(base::WideToUTF8(L"Hreggvi\u00F0ur"), |
| action->GetTitle(ExtensionAction::kDefaultTabId)); |
| } |
| |
| // Tests dispatching the onClicked event to listeners when the extension action |
| // in the toolbar is pressed. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, OnClickedDispatching) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Test Clicking", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": {}, |
| "background": { %s } |
| })"; |
| constexpr char kBackgroundJsTemplate[] = |
| R"(chrome.%s.onClicked.addListener((tab) => { |
| // Check a few properties on the tabs object to make sure it's sane. |
| chrome.test.assertTrue(!!tab); |
| chrome.test.assertTrue(tab.id > 0); |
| chrome.test.assertTrue(tab.index > -1); |
| chrome.test.notifyPass(); |
| });)"; |
| |
| const char* background_specification = |
| GetParam() == ActionInfo::Type::kAction |
| ? R"("service_worker": "background.js")" |
| : R"("scripts": ["background.js"])"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()), |
| background_specification)); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(kBackgroundJsTemplate, |
| GetAPINameForActionType(GetParam()))); |
| |
| // Though this says "ExtensionActionTestHelper", it's actually used for all |
| // toolbar actions. |
| // TODO(devlin): Rename it to ToolbarActionTestUtil. |
| std::unique_ptr<ExtensionActionTestHelper> toolbar_helper = |
| ExtensionActionTestHelper::Create(browser()); |
| EXPECT_EQ(0, toolbar_helper->NumberOfBrowserActions()); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ASSERT_EQ(1, toolbar_helper->NumberOfBrowserActions()); |
| EXPECT_TRUE(toolbar_helper->HasAction(extension->id())); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| const int tab_id = GetActiveTabId(); |
| EXPECT_TRUE(ActionHasDefaultState(*action, tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| EXPECT_FALSE(action->HasPopup(tab_id)); |
| |
| ResultCatcher result_catcher; |
| toolbar_helper->Press(extension->id()); |
| ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| // Tests the creation of a popup when one is specified in the manifest. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, PopupCreation) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Test Clicking", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": { |
| "default_popup": "popup.html" |
| } |
| })"; |
| |
| constexpr char kPopupHtml[] = |
| R"(<!doctype html> |
| <html> |
| <script src="popup.js"></script> |
| </html>)"; |
| constexpr char kPopupJs[] = |
| "window.onload = function() { chrome.test.notifyPass(); };"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("popup.js"), kPopupJs); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| std::unique_ptr<ExtensionActionTestHelper> toolbar_helper = |
| ExtensionActionTestHelper::Create(browser()); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| const int tab_id = GetActiveTabId(); |
| EXPECT_TRUE(ActionHasDefaultState(*action, tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| EXPECT_TRUE(action->HasPopup(tab_id)); |
| |
| ResultCatcher result_catcher; |
| toolbar_helper->Press(extension->id()); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| |
| ProcessManager* process_manager = ProcessManager::Get(profile()); |
| ProcessManager::FrameSet frames = |
| process_manager->GetRenderFrameHostsForExtension(extension->id()); |
| ASSERT_EQ(1u, frames.size()); |
| content::RenderFrameHost* render_frame_host = *frames.begin(); |
| EXPECT_EQ(extension->GetResourceURL("popup.html"), |
| render_frame_host->GetLastCommittedURL()); |
| |
| content::WebContents* popup_contents = |
| content::WebContents::FromRenderFrameHost(render_frame_host); |
| ASSERT_TRUE(popup_contents); |
| |
| content::WebContentsDestroyedWatcher contents_destroyed(popup_contents); |
| EXPECT_TRUE(content::ExecJs(popup_contents, "window.close()")); |
| contents_destroyed.Wait(); |
| |
| frames = process_manager->GetRenderFrameHostsForExtension(extension->id()); |
| EXPECT_EQ(0u, frames.size()); |
| } |
| |
| // Tests that sessionStorage does not persist between closing and opening of a |
| // popup. |
| // TODO(crbug.com/40795982): Flaky on Linux. |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_SessionStorageDoesNotPersistBetweenOpenings \ |
| DISABLED_SessionStorageDoesNotPersistBetweenOpenings |
| #else |
| #define MAYBE_SessionStorageDoesNotPersistBetweenOpenings \ |
| SessionStorageDoesNotPersistBetweenOpenings |
| #endif |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, |
| MAYBE_SessionStorageDoesNotPersistBetweenOpenings) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Test sessionStorage", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": { |
| "default_popup": "popup.html" |
| } |
| })"; |
| |
| constexpr char kPopupHtml[] = |
| R"(<!doctype html> |
| <html> |
| <script src="popup.js"></script> |
| </html>)"; |
| |
| constexpr char kPopupJs[] = |
| R"(window.onload = function() { |
| if (!sessionStorage.foo) { |
| sessionStorage.foo = 1; |
| } else { |
| sessionStorage.foo = parseInt(sessionStorage.foo) + 1; |
| } |
| chrome.test.notifyPass();}; |
| )"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("popup.js"), kPopupJs); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ExtensionsContainer* extensions_container = |
| browser()->window()->GetExtensionsContainer(); |
| ASSERT_TRUE(extensions_container); |
| ToolbarActionViewController* action_controller = |
| extensions_container->GetActionForId(extension->id()); |
| ASSERT_TRUE(action_controller); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| int tab_id = GetActiveTabId(); |
| EnsureActionIsEnabledOnActiveTab(action); |
| EXPECT_TRUE(action->HasPopup(tab_id)); |
| |
| ResultCatcher result_catcher; |
| action_controller->ExecuteUserAction( |
| ToolbarActionViewController::InvocationSource::kToolbarButton); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| |
| ProcessManager* process_manager = ProcessManager::Get(profile()); |
| ProcessManager::FrameSet frames = |
| process_manager->GetRenderFrameHostsForExtension(extension->id()); |
| ASSERT_EQ(1u, frames.size()); |
| content::RenderFrameHost* render_frame_host = *frames.begin(); |
| |
| content::WebContents* popup_contents = |
| content::WebContents::FromRenderFrameHost(render_frame_host); |
| ASSERT_TRUE(popup_contents); |
| |
| EXPECT_EQ("1", content::EvalJs(popup_contents, "sessionStorage.foo")); |
| |
| const std::string session_storage_id1 = |
| popup_contents->GetController().GetDefaultSessionStorageNamespace()->id(); |
| |
| // Close the popup. |
| content::WebContentsDestroyedWatcher contents_destroyed(popup_contents); |
| EXPECT_TRUE(content::ExecJs(popup_contents, "window.close()")); |
| contents_destroyed.Wait(); |
| |
| frames = process_manager->GetRenderFrameHostsForExtension(extension->id()); |
| EXPECT_EQ(0u, frames.size()); |
| |
| // Open the popup again. |
| action_controller->ExecuteUserAction( |
| ToolbarActionViewController::InvocationSource::kToolbarButton); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| |
| frames = process_manager->GetRenderFrameHostsForExtension(extension->id()); |
| ASSERT_EQ(1u, frames.size()); |
| render_frame_host = *frames.begin(); |
| |
| popup_contents = content::WebContents::FromRenderFrameHost(render_frame_host); |
| const std::string session_storage_id2 = |
| popup_contents->GetController().GetDefaultSessionStorageNamespace()->id(); |
| |
| // Verify that sessionStorage did not persist. The reason is that closing the |
| // popup ends the session and clears objects in sessionStorage. |
| EXPECT_NE(session_storage_id1, session_storage_id2); |
| EXPECT_EQ("1", content::EvalJs(popup_contents, "sessionStorage.foo")); |
| } |
| |
| using ActionAndBrowserActionAPITest = MultiActionAPITest; |
| |
| // Tests whether action values persist across sessions. |
| // Note: Since pageActions are only applicable on a specific tab, this test |
| // doesn't apply to them. |
| IN_PROC_BROWSER_TEST_P(ActionAndBrowserActionAPITest, PRE_ValuesArePersisted) { |
| const char* dir_name = nullptr; |
| switch (GetParam()) { |
| case ActionInfo::Type::kAction: |
| dir_name = "extension_action/action_persistence"; |
| break; |
| case ActionInfo::Type::kBrowser: |
| dir_name = "extension_action/browser_action_persistence"; |
| break; |
| case ActionInfo::Type::kPage: |
| NOTREACHED(); |
| } |
| // Load up an extension, which then modifies the popup, title, and badge text |
| // of the action. We need to use a "real" extension on disk here (rather than |
| // a TestExtensionDir owned by the test fixture), because it needs to persist |
| // to the next test. |
| ResultCatcher catcher; |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII(dir_name)); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| |
| // Verify the values were modified. |
| auto* action_manager = ExtensionActionManager::Get(profile()); |
| ExtensionAction* action = action_manager->GetExtensionAction(*extension); |
| EXPECT_EQ(extension->GetResourceURL("modified_popup.html"), |
| action->GetPopupUrl(ExtensionAction::kDefaultTabId)); |
| EXPECT_EQ("modified title", action->GetTitle(ExtensionAction::kDefaultTabId)); |
| EXPECT_EQ("custom badge text", |
| action->GetExplicitlySetBadgeText(ExtensionAction::kDefaultTabId)); |
| |
| // We flush the state store to ensure the modified state is correctly stored |
| // on-disk (which could otherwise be potentially racy). |
| FlushStateStore(profile()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ActionAndBrowserActionAPITest, ValuesArePersisted) { |
| const Extension* extension = GetSingleLoadedExtension(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ("Action persistence check", extension->name()); |
| |
| // The previous action states are read from the state store on start-up. |
| // Flushing it ensures that any pending tasks have run, and the action |
| // should be up-to-date. |
| FlushStateStore(profile()); |
| |
| auto* action_manager = ExtensionActionManager::Get(profile()); |
| ExtensionAction* action = action_manager->GetExtensionAction(*extension); |
| |
| // Only browser actions - not generic actions - persist values. |
| bool expect_persisted_values = GetParam() == ActionInfo::Type::kBrowser; |
| |
| std::string expected_badge_text = |
| expect_persisted_values ? "custom badge text" : ""; |
| |
| EXPECT_EQ(expected_badge_text, |
| action->GetExplicitlySetBadgeText(ExtensionAction::kDefaultTabId)); |
| |
| // Due to https://crbug.com/1110156, action values with defaults specified in |
| // the manifest - like popup and title - aren't persisted, even for browser |
| // actions. |
| EXPECT_EQ(extension->GetResourceURL("default_popup.html"), |
| action->GetPopupUrl(ExtensionAction::kDefaultTabId)); |
| EXPECT_EQ("default title", action->GetTitle(ExtensionAction::kDefaultTabId)); |
| } |
| |
| // Tests setting the icon dynamically from the background page. |
| // TODO(crbug.com/40230315): flaky. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPICanvasTest, DISABLED_DynamicSetIcon) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Test Clicking", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": { |
| "default_icon": "red_icon.png" |
| } |
| })"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), |
| base::StringPrintf(kSetIconBackgroundJsTemplate, |
| GetAPINameForActionType(GetParam()), |
| GetAPINameForActionType(GetParam()))); |
| test_dir.CopyFileTo(test_data_dir_.AppendASCII("icon_rgb_0_0_255.png"), |
| FILE_PATH_LITERAL("blue_icon.png")); |
| test_dir.CopyFileTo(test_data_dir_.AppendASCII("icon_rgb_255_0_0.png"), |
| FILE_PATH_LITERAL("red_icon.png")); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| ASSERT_TRUE(action->default_icon()); |
| // Wait for the default icon to finish loading; otherwise it may be empty |
| // when we check it. |
| TestIconImageObserver::WaitForIcon(action->default_icon_image()); |
| |
| int tab_id = GetActiveTabId(); |
| EXPECT_TRUE(ActionHasDefaultState(*action, tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| |
| std::unique_ptr<ExtensionActionTestHelper> toolbar_helper = |
| ExtensionActionTestHelper::Create(browser()); |
| |
| ASSERT_EQ(1, toolbar_helper->NumberOfBrowserActions()); |
| EXPECT_TRUE(toolbar_helper->HasAction(extension->id())); |
| |
| gfx::Image default_icon = toolbar_helper->GetIcon(extension->id()); |
| EXPECT_FALSE(default_icon.IsEmpty()); |
| |
| // Check the midpoint. All these icons are solid, but the rendered icon |
| // includes padding. |
| const int mid_x = default_icon.Width() / 2; |
| const int mid_y = default_icon.Height() / 2; |
| // Note: We only validate the color here as a quick-and-easy way of validating |
| // the icon is what we expect. Other tests do much more rigorous testing of |
| // the icon's rendering. |
| EXPECT_EQ(SK_ColorRED, default_icon.AsBitmap().getColor(mid_x, mid_y)); |
| |
| // Open a tab to run the extension commands in. |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), extension->GetResourceURL("page.html"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Create a new tab. |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), GURL("chrome://newtab"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| |
| const int new_tab_id = GetActiveTabId(); |
| EXPECT_NE(new_tab_id, tab_id); |
| EXPECT_TRUE(ActionHasDefaultState(*action, new_tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| |
| // The new tab should still have the same icon (the default). |
| gfx::Image new_tab_icon = toolbar_helper->GetIcon(extension->id()); |
| EXPECT_FALSE(default_icon.IsEmpty()); |
| EXPECT_EQ(SK_ColorRED, default_icon.AsBitmap().getColor(mid_x, mid_y)); |
| |
| // Set the icon for the new tab to a different icon in the extension package. |
| RunTestAndWaitForSuccess( |
| web_contents, |
| base::StringPrintf("setIcon({tabId: %d, path: 'blue_icon.png'});", |
| new_tab_id)); |
| |
| new_tab_icon = toolbar_helper->GetIcon(extension->id()); |
| EXPECT_FALSE(new_tab_icon.IsEmpty()); |
| EXPECT_EQ(SK_ColorBLUE, new_tab_icon.AsBitmap().getColor(mid_x, mid_y)); |
| |
| // Next, set the icon to a dynamically-generated one (from canvas image data). |
| constexpr char kSetIconFromImageData[] = |
| R"({ |
| let canvas = document.createElement('canvas'); |
| canvas.width = 32; |
| canvas.height = 32; |
| let context = canvas.getContext('2d'); |
| context.clearRect(0, 0, 32, 32); |
| context.fillStyle = '#00FF00'; // Green |
| context.fillRect(0, 0, 32, 32); |
| let imageData = context.getImageData(0, 0, 32, 32); |
| setIcon({tabId: %d, imageData: imageData}); |
| })"; |
| RunTestAndWaitForSuccess( |
| web_contents, base::StringPrintf(kSetIconFromImageData, new_tab_id)); |
| |
| new_tab_icon = toolbar_helper->GetIcon(extension->id()); |
| EXPECT_FALSE(new_tab_icon.IsEmpty()); |
| EXPECT_EQ(SK_ColorGREEN, new_tab_icon.AsBitmap().getColor(mid_x, mid_y)); |
| |
| // Manifest V3 extensions using the action API should also be able to use a |
| // promise version of setIcon. |
| if (GetManifestVersionForActionType(GetParam()) == 3) { |
| constexpr char kSetIconPromiseScript[] = |
| "setIconPromise({tabId: %d, path: 'blue_icon.png'});"; |
| RunTestAndWaitForSuccess( |
| web_contents, base::StringPrintf(kSetIconPromiseScript, new_tab_id)); |
| |
| new_tab_icon = toolbar_helper->GetIcon(extension->id()); |
| EXPECT_FALSE(new_tab_icon.IsEmpty()); |
| EXPECT_EQ(SK_ColorBLUE, new_tab_icon.AsBitmap().getColor(mid_x, mid_y)); |
| } |
| |
| // Switch back to the first tab. The icon should still be red, since the other |
| // changes were for specific tabs. |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| gfx::Image first_tab_icon = toolbar_helper->GetIcon(extension->id()); |
| EXPECT_FALSE(first_tab_icon.IsEmpty()); |
| EXPECT_EQ(SK_ColorRED, first_tab_icon.AsBitmap().getColor(mid_x, mid_y)); |
| |
| // TODO(devlin): Add tests for setting icons as a dictionary of |
| // { size -> image_data }. |
| } |
| |
| // Tests calling setIcon() from JS with hooks that might cause issues with our |
| // custom bindings. |
| // Regression test for https://crbug.com/1087948. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, SetIconWithJavascriptHooks) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "JS Fun", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": {} |
| })"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), |
| base::StringPrintf(kSetIconBackgroundJsTemplate, |
| GetAPINameForActionType(GetParam()), |
| GetAPINameForActionType(GetParam()))); |
| test_dir.CopyFileTo(test_data_dir_.AppendASCII("icon_rgb_0_0_255.png"), |
| FILE_PATH_LITERAL("blue_icon.png")); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), extension->GetResourceURL("page.html"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| int tab_id = GetActiveTabId(); |
| EXPECT_TRUE(ActionHasDefaultState(*action, tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| |
| // Define a setter for objects on the imageData key. This could previously |
| // result in an invalid arguments object being sent to the browser. |
| constexpr char kScript[] = |
| R"(Object.defineProperty( |
| Object.prototype, 'imageData', |
| { set() { console.warn('intercepted set'); } }); |
| 'done';)"; |
| ASSERT_EQ("done", content::EvalJs(web_contents, kScript)); |
| |
| constexpr char kOnePathScript[] = |
| "setIcon({tabId: %d, path: 'blue_icon.png'});"; |
| RunTestAndWaitForSuccess(web_contents, |
| base::StringPrintf(kOnePathScript, tab_id)); |
| constexpr char kMultiPathScript[] = |
| R"(setIcon({tabId: %d, |
| path: {16: 'blue_icon.png', 24: 'blue_icon.png'}});)"; |
| RunTestAndWaitForSuccess(web_contents, |
| base::StringPrintf(kMultiPathScript, tab_id)); |
| constexpr char kRawImageDataScript[] = |
| R"(setIcon({tabId: %d, |
| imageData: {width:4,height:4,data:'a'.repeat(64)}});)"; |
| RunTestAndWaitForSuccess(web_contents, |
| base::StringPrintf(kRawImageDataScript, tab_id)); |
| } |
| |
| // Tests calling setIcon() from JS with `self` defined at the top-level. |
| // Regression test for https://crbug.com/1087948. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, SetIconWithSelfDefined) { |
| // TODO(devlin): Pull code to load an extension like this into a helper |
| // function. |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "JS Fun", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": {} |
| })"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), |
| base::StringPrintf(kSetIconBackgroundJsTemplate, |
| GetAPINameForActionType(GetParam()), |
| GetAPINameForActionType(GetParam()))); |
| test_dir.CopyFileTo(test_data_dir_.AppendASCII("icon_rgb_0_0_255.png"), |
| FILE_PATH_LITERAL("blue_icon.png")); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), extension->GetResourceURL("page.html"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| int tab_id = GetActiveTabId(); |
| EXPECT_TRUE(ActionHasDefaultState(*action, tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| |
| // Override 'self' in a local variable. |
| constexpr char kOverrideSelfScript[] = "var self = ''; 'done';"; |
| ASSERT_EQ("done", content::EvalJs(web_contents, kOverrideSelfScript)); |
| |
| // Try setting the icon. This should succeed. Previously, the custom bindings |
| // for the setIcon code looked at the 'self' variable, but this could be |
| // overridden by the extension. |
| // See also https://crbug.com/1087948. |
| constexpr char kSetIconScript[] = |
| "setIcon({tabId: %d, path: 'blue_icon.png'});"; |
| RunTestAndWaitForSuccess(web_contents, |
| base::StringPrintf(kSetIconScript, tab_id)); |
| } |
| |
| // Tests calling setIcon() for a tab with an invalid icon path specified. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, SetIconInTabWithInvalidPath) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Bad Icon Path", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": {} |
| })"; |
| |
| constexpr char kPageJsTemplate[] = |
| R"(function setIcon(details) { |
| chrome.%s.setIcon(details, () => { |
| chrome.test.assertLastError("%s"); |
| chrome.test.notifyPass(); |
| }); |
| })"; |
| |
| constexpr char kExpectedError[] = |
| "Could not load action icon 'does_not_exist.png'."; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("page.js"), |
| base::StringPrintf(kPageJsTemplate, GetAPINameForActionType(GetParam()), |
| kExpectedError)); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), extension->GetResourceURL("page.html"))); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| int tab_id = GetActiveTabId(); |
| EXPECT_TRUE(ActionHasDefaultState(*action, tab_id)); |
| EnsureActionIsEnabledOnActiveTab(action); |
| |
| // Calling setIcon with an invalid path in a non-service worker context should |
| // emit a console error in that context and call the callback with lastError |
| // set. |
| content::WebContentsConsoleObserver console_observer(web_contents); |
| console_observer.SetPattern(kExpectedError); |
| |
| constexpr char kSetIconScript[] = |
| "setIcon({tabId: %d, path: 'does_not_exist.png'});"; |
| RunTestAndWaitForSuccess(web_contents, |
| base::StringPrintf(kSetIconScript, tab_id)); |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Tests calling setIcon() in the service worker with an invalid icon paths |
| // specified. Regression test for https://crbug.com/1262029. Regression test for |
| // https://crbug.com/1372518. |
| IN_PROC_BROWSER_TEST_F(ExtensionActionAPITest, SetIconInWorkerWithInvalidPath) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Bad Icon Path In Worker", |
| "manifest_version": 3, |
| "version": "0.1", |
| "action": {}, |
| "background": {"service_worker": "worker.js" } |
| })"; |
| |
| constexpr char kBackgroundJs[] = |
| R"(let expectedError = "%s"; |
| let anotherExpectedError = "%s"; |
| const singlePath = 'does_not_exist.png'; |
| const multiplePaths = { |
| 16: 'does_not_exist.png', |
| 32: 'also_does_not_exist.png' |
| }; |
| |
| chrome.test.runTests([ |
| function singleWithCallback() { |
| chrome.action.setIcon({path: singlePath}, () => { |
| chrome.test.assertLastError(expectedError); |
| chrome.test.succeed(); |
| }); |
| }, |
| async function singleWithPromise() { |
| await chrome.test.assertPromiseRejects( |
| chrome.action.setIcon({path: singlePath}), |
| 'Error: ' + expectedError); |
| chrome.test.succeed(); |
| }, |
| /* |
| Multiple icons are loaded asynchronously and either one could |
| end up failing first. However only the first error is emitted, |
| we check against both possibilities. |
| */ |
| function multipleWithCallback() { |
| chrome.action.setIcon({ path: multiplePaths }, () => { |
| let errorMessage = chrome.runtime.lastError.message; |
| chrome.test.assertTrue(errorMessage === expectedError |
| || errorMessage === anotherExpectedError); |
| chrome.test.succeed(); |
| }); |
| }, |
| function multipleWithPromise() { |
| chrome.action.setIcon({ path: multiplePaths }) |
| .then(() => { |
| chrome.test.fail(); |
| }) |
| .catch((error) => { |
| chrome.test.assertTrue(error.message === expectedError |
| || error.message === anotherExpectedError); |
| chrome.test.succeed(); |
| }); |
| } |
| ]);)"; |
| |
| constexpr char kExpectedError[] = |
| "Failed to set icon 'does_not_exist.png': Failed to fetch"; |
| constexpr char kAnotherExpectedError[] = |
| "Failed to set icon 'also_does_not_exist.png': Failed to fetch"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifestTemplate); |
| |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("worker.js"), |
| base::StringPrintf(kBackgroundJs, kExpectedError, kAnotherExpectedError)); |
| |
| // Calling setIcon with an invalid path in a service worker context should |
| // reject the promise or call the callback with lastError set. |
| ResultCatcher result_catcher; |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| // Tests multiple cases of setting an invalid popup that violate same-origin |
| // checks. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, SetPopupWithInvalidPath) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Invalid Popup Path", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": {} |
| })"; |
| constexpr char kSetPopupJsTemplate[] = |
| R"( |
| function setPopup(details, expectedError) { |
| chrome.%s.setPopup(details, () => { |
| chrome.test.assertLastError(expectedError); |
| chrome.test.succeed(); |
| }); |
| }; |
| )"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), |
| "// This space left blank."); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), |
| base::StringPrintf(kSetPopupJsTemplate, |
| GetAPINameForActionType(GetParam()))); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| auto get_script = [](int tab_id, const char* popup_input) { |
| constexpr char kSetPopup[] = R"(setPopup({tabId: %d, popup: '%s'}, "%s");)"; |
| return base::StringPrintf(kSetPopup, tab_id, popup_input, |
| manifest_errors::kInvalidExtensionOriginPopup); |
| }; |
| |
| content::RenderFrameHost* navigated_host = ui_test_utils::NavigateToURL( |
| browser(), extension->GetResourceURL("page.html")); |
| ASSERT_TRUE(navigated_host); |
| content::WebContents* web_contents = GetActiveTab(); |
| int tab_id = GetActiveTabId(); |
| |
| // Set the popup to an invalid nonexistent extension URL and expect an error. |
| { |
| static constexpr char kInvalidPopupUrl[] = |
| "chrome-extension://notavalidextensionid/popup.html"; |
| RunTestAndWaitForSuccess(web_contents, |
| get_script(tab_id, kInvalidPopupUrl)); |
| } |
| |
| // Set the popup to a web URL and expect an error. |
| { |
| static constexpr char kWebUrl[] = "http://test.com"; |
| RunTestAndWaitForSuccess(web_contents, get_script(tab_id, kWebUrl)); |
| } |
| |
| // Set the popup to another existing extension and expect an error. |
| { |
| TestExtensionDir different_extension_dir; |
| different_extension_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| different_extension_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), |
| "// This space left blank."); |
| const Extension* different_extension = |
| LoadExtension(different_extension_dir.UnpackedPath()); |
| ASSERT_TRUE(different_extension); |
| const std::string different_extension_popup_url = |
| different_extension->GetResourceURL("popup.html").spec(); |
| RunTestAndWaitForSuccess( |
| web_contents, |
| get_script(tab_id, different_extension_popup_url.c_str())); |
| } |
| } |
| |
| // Tests various getter and setter methods. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, GettersAndSetters) { |
| // Load up an extension with default values. |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Test Getters and Setters", |
| "manifest_version": %d, |
| "version": "0.1", |
| "%s": { |
| "default_title": "default title", |
| "default_popup": "default_popup.html" |
| } |
| })"; |
| constexpr char kPageJs[] = "// Intentionally blank."; |
| constexpr char kPopupHtml[] = |
| "<!doctype html><html><body>Blank</body></html>"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs); |
| test_dir.WriteFile(FILE_PATH_LITERAL("default_popup.html"), kPopupHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("custom_popup1.html"), kPopupHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("custom_popup2.html"), kPopupHtml); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| int first_tab_id = GetActiveTabId(); |
| |
| // Open a tab to run the extension commands in. |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), extension->GetResourceURL("page.html"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // And a second new tab. |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), GURL("chrome://newtab"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| int second_tab_id = GetActiveTabId(); |
| |
| // A simple structure to hold different representations of values (one JS, |
| // one C++). |
| struct ValuePair { |
| std::string cpp; |
| std::string js; |
| |
| bool operator!=(const ValuePair& rhs) const { |
| return rhs.cpp != this->cpp || rhs.js != this->js; |
| } |
| }; |
| |
| // A function that returns the the C++ result for the given ExtensionAction |
| // and tab id. |
| using CPPValueGetter = |
| base::RepeatingCallback<std::string(ExtensionAction*, int)>; |
| |
| auto run_test = |
| [action, first_tab_id, second_tab_id]( |
| ActionTestHelper& test_helper, const ValuePair& default_value, |
| const ValuePair& custom_value1, const ValuePair& custom_value2, |
| CPPValueGetter value_getter) { |
| // Ensure all values are mutually exclusive. |
| EXPECT_NE(default_value, custom_value1); |
| EXPECT_NE(default_value, custom_value2); |
| EXPECT_NE(custom_value1, custom_value2); |
| |
| SCOPED_TRACE(base::StringPrintf( |
| "default: '%s', custom1: '%s', custom2: '%s'", |
| default_value.cpp.c_str(), custom_value1.cpp.c_str(), |
| custom_value2.cpp.c_str())); |
| |
| // A helper to check the value of a property of the action in both |
| // C++ (from the ExtensionAction object) and in JS (through the API |
| // method). |
| auto check_value = [action, &value_getter, &test_helper]( |
| const ValuePair& expected_value, int tab_id) { |
| EXPECT_EQ(expected_value.cpp, value_getter.Run(action, tab_id)); |
| if (tab_id == ExtensionAction::kDefaultTabId) |
| test_helper.CheckDefaultValue(expected_value.js.c_str()); |
| else |
| test_helper.CheckValueForTab(expected_value.js.c_str(), tab_id); |
| }; |
| |
| // Page actions don't support setting a default value (because they are |
| // inherently tab-specific). |
| bool supports_default = GetParam() != ActionInfo::Type::kPage; |
| |
| // Check the initial state. These should start at the defaults. |
| if (supports_default) |
| check_value(default_value, ExtensionAction::kDefaultTabId); |
| check_value(default_value, first_tab_id); |
| check_value(default_value, second_tab_id); |
| |
| // Set the value for the first tab to be the first custom value. |
| test_helper.SetValueForTab(custom_value1.js.c_str(), first_tab_id); |
| |
| // The first tab should have the custom value, while the second tab |
| // (and the default tab, if supported) should still have the default |
| // value. |
| if (supports_default) |
| check_value(default_value, ExtensionAction::kDefaultTabId); |
| check_value(custom_value1, first_tab_id); |
| check_value(default_value, second_tab_id); |
| |
| if (supports_default) { |
| // Change the default value to the second custom value. |
| test_helper.SetDefaultValue(custom_value2.js.c_str()); |
| |
| // Now, the default and second tab should each have the second custom |
| // value. Since the first tab had its own value set, it should still |
| // be set to the first custom value. |
| check_value(custom_value2, ExtensionAction::kDefaultTabId); |
| check_value(custom_value1, first_tab_id); |
| check_value(custom_value2, second_tab_id); |
| } |
| }; |
| |
| const char* kApiName = GetAPINameForActionType(GetParam()); |
| |
| { |
| // setPopup/getPopup. |
| GURL default_popup_url = extension->GetResourceURL("default_popup.html"); |
| GURL custom_popup_url1 = extension->GetResourceURL("custom_popup1.html"); |
| GURL custom_popup_url2 = extension->GetResourceURL("custom_popup2.html"); |
| ValuePair default_popup{default_popup_url.spec(), |
| base::StrCat({"'", default_popup_url.spec(), "'"})}; |
| ValuePair custom_popup1{custom_popup_url1.spec(), |
| base::StrCat({"'", custom_popup_url1.spec(), "'"})}; |
| ValuePair custom_popup2{custom_popup_url2.spec(), |
| base::StrCat({"'", custom_popup_url2.spec(), "'"})}; |
| |
| auto get_popup = [](ExtensionAction* action, int tab_id) { |
| return action->GetPopupUrl(tab_id).spec(); |
| }; |
| |
| ActionTestHelper popup_helper(kApiName, "setPopup", "getPopup", "popup", |
| web_contents); |
| run_test(popup_helper, default_popup, custom_popup1, custom_popup2, |
| base::BindRepeating(get_popup)); |
| } |
| { |
| // setTitle/getTitle. |
| ValuePair default_title{"default title", "'default title'"}; |
| ValuePair custom_title1{"custom title1", "'custom title1'"}; |
| ValuePair custom_title2{"custom title2", "'custom title2'"}; |
| |
| auto get_title = [](ExtensionAction* action, int tab_id) { |
| return action->GetTitle(tab_id); |
| }; |
| |
| ActionTestHelper title_helper(kApiName, "setTitle", "getTitle", "title", |
| web_contents); |
| run_test(title_helper, default_title, custom_title1, custom_title2, |
| base::BindRepeating(get_title)); |
| } |
| |
| // Page actions don't have badges; for them, the test is done. |
| if (GetParam() == ActionInfo::Type::kPage) { |
| return; |
| } |
| |
| { |
| // setBadgeText/getBadgeText. |
| ValuePair default_badge_text{"", "''"}; |
| ValuePair custom_badge_text1{"custom badge1", "'custom badge1'"}; |
| ValuePair custom_badge_text2{"custom badge2", "'custom badge2'"}; |
| |
| auto get_badge_text = [](ExtensionAction* action, int tab_id) { |
| return action->GetExplicitlySetBadgeText(tab_id); |
| }; |
| |
| ActionTestHelper badge_text_helper(kApiName, "setBadgeText", "getBadgeText", |
| "text", web_contents); |
| run_test(badge_text_helper, default_badge_text, custom_badge_text1, |
| custom_badge_text2, base::BindRepeating(get_badge_text)); |
| } |
| { |
| // setBadgeBackgroundColor/getBadgeBackgroundColor. |
| ValuePair default_badge_color{"0,0,0", "[0, 0, 0, 0]"}; |
| ValuePair custom_badge_color1{"255,0,0", "[255, 0, 0, 255]"}; |
| ValuePair custom_badge_color2{"0,255,0", "[0, 255, 0, 255]"}; |
| |
| auto get_badge_color = [](ExtensionAction* action, int tab_id) { |
| return color_utils::SkColorToRgbString( |
| action->GetBadgeBackgroundColor(tab_id)); |
| }; |
| |
| ActionTestHelper badge_color_helper(kApiName, "setBadgeBackgroundColor", |
| "getBadgeBackgroundColor", "color", |
| web_contents); |
| run_test(badge_color_helper, default_badge_color, custom_badge_color1, |
| custom_badge_color2, base::BindRepeating(get_badge_color)); |
| } |
| |
| // TODO(crbug.com/40870872): Test using HTML colors instead of just color |
| // arrays, including set/getBadgeBackgroundColor. |
| // setBadgeTextColor/getBadgeTextColor. |
| // This API is only supported on MV3. |
| if (GetParam() != ActionInfo::Type::kBrowser) { |
| { |
| ValuePair default_badge_text_color{"0,0,0", "[0, 0, 0, 0]"}; |
| ValuePair custom_badge_text_color1{"255,0,0", "[255, 0, 0, 255]"}; |
| ValuePair custom_badge_text_color2{"0,255,0", "[0, 255, 0, 255]"}; |
| |
| auto get_badge_text_color = [](ExtensionAction* action, int tab_id) { |
| return color_utils::SkColorToRgbString( |
| action->GetBadgeTextColor(tab_id)); |
| }; |
| |
| ActionTestHelper badge_text_color_helper(kApiName, "setBadgeTextColor", |
| "getBadgeTextColor", "color", |
| web_contents); |
| run_test(badge_text_color_helper, default_badge_text_color, |
| custom_badge_text_color1, custom_badge_text_color2, |
| base::BindRepeating(get_badge_text_color)); |
| } |
| } |
| } |
| |
| // Tests the functions to enable and disable extension actions. |
| IN_PROC_BROWSER_TEST_P(MultiActionAPITest, EnableAndDisable) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "enabled/disabled action test", |
| "version": "0.1", |
| "manifest_version": %d, |
| "%s": {} |
| })"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()))); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), "// This space left blank."); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| const int tab_id1 = GetActiveTabId(); |
| EnsureActionIsEnabledOnTab(action, tab_id1); |
| |
| // Open a tab to run the extension commands in. |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), extension->GetResourceURL("page.html"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), GURL("chrome://newtab"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| |
| const int tab_id2 = GetActiveTabId(); |
| EnsureActionIsEnabledOnTab(action, tab_id2); |
| |
| EXPECT_NE(tab_id1, tab_id2); |
| |
| const char* enable_function = nullptr; |
| const char* disable_function = nullptr; |
| switch (GetParam()) { |
| case ActionInfo::Type::kAction: |
| case ActionInfo::Type::kBrowser: |
| enable_function = "enable"; |
| disable_function = "disable"; |
| break; |
| case ActionInfo::Type::kPage: |
| enable_function = "show"; |
| disable_function = "hide"; |
| break; |
| } |
| |
| // Start by toggling the extension action on the current tab. |
| { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s(%d, () => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents, |
| base::StringPrintf(kScriptTemplate, GetAPINameForActionType(GetParam()), |
| disable_function, tab_id2)); |
| EXPECT_FALSE(action->GetIsVisible(tab_id2)); |
| EXPECT_TRUE(action->GetIsVisible(tab_id1)); |
| } |
| |
| { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s(%d, () => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents, |
| base::StringPrintf(kScriptTemplate, GetAPINameForActionType(GetParam()), |
| enable_function, tab_id2)); |
| EXPECT_TRUE(action->GetIsVisible(tab_id2)); |
| EXPECT_TRUE(action->GetIsVisible(tab_id1)); |
| } |
| |
| // Page actions can't be enabled/disabled globally, but others can. Try |
| // toggling global state by omitting the tab id if the type isn't a page |
| // action. |
| if (GetParam() == ActionInfo::Type::kPage) { |
| return; |
| } |
| |
| // We need to undo the explicit enable from above, since tab-specific |
| // values take precedence. |
| action->ClearAllValuesForTab(tab_id2); |
| { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s(() => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents, |
| base::StringPrintf(kScriptTemplate, GetAPINameForActionType(GetParam()), |
| disable_function)); |
| EXPECT_EQ(false, action->GetIsVisible(tab_id2)); |
| EXPECT_EQ(false, action->GetIsVisible(tab_id1)); |
| } |
| |
| { |
| constexpr char kScriptTemplate[] = |
| R"(chrome.%s.%s(() => { |
| chrome.test.assertNoLastError(); |
| chrome.test.notifyPass(); |
| });)"; |
| RunTestAndWaitForSuccess( |
| web_contents, |
| base::StringPrintf(kScriptTemplate, GetAPINameForActionType(GetParam()), |
| enable_function)); |
| EXPECT_EQ(true, action->GetIsVisible(tab_id2)); |
| EXPECT_EQ(true, action->GetIsVisible(tab_id1)); |
| } |
| } |
| |
| // Tests that the check for enabled and disabled status are correctly reported. |
| IN_PROC_BROWSER_TEST_F(ExtensionActionAPITest, IsEnabled) { |
| ASSERT_TRUE(RunExtensionTest("extension_action/is_enabled")) << message_; |
| } |
| |
| // Tests that isEnabled correctly ignores declarativeContent rules for enable. |
| IN_PROC_BROWSER_TEST_F(ExtensionActionAPITest, IsEnabledIgnoreDeclarative) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Declarative content ignored test", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["activeTab", "declarativeContent"], |
| "background": { |
| "service_worker" : "background.js" |
| }, |
| "action": {} |
| })"; |
| constexpr char kSetupDeclarativeContent[] = |
| R"( |
| let rule1 = { |
| conditions: [ |
| new chrome.declarativeContent.PageStateMatcher({ |
| pageUrl: { hostContains: 'google'}, |
| }) |
| ], |
| actions: [ new chrome.declarativeContent.ShowAction() ] |
| }; |
| chrome.runtime.onInstalled.addListener(function(details) { |
| chrome.declarativeContent.onPageChanged.removeRules( |
| undefined, function() { |
| chrome.declarativeContent.onPageChanged.addRules( |
| [rule1], () => { |
| chrome.test.sendMessage('ready'); |
| }); |
| }); |
| }); |
| // Set tab disabled globally so that we can assert that the extension |
| // cannot know the enable status for declarativeContent tabs it is |
| // registered for. |
| chrome.action.disable(); |
| )"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifestTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| kSetupDeclarativeContent); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| auto* action_manager = ExtensionActionManager::Get(profile()); |
| ExtensionAction* action = action_manager->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| GURL url(embedded_test_server()->GetURL("google.com", "/title1.html")); |
| |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| |
| // Confirm that the tab is only visible for declarativeContent. |
| ASSERT_TRUE(action->GetIsVisible(tab_id)); |
| ASSERT_FALSE(action->GetIsVisibleIgnoringDeclarative(tab_id)); |
| |
| constexpr char kCheckIsEnabledStatusForTabId[] = |
| R"( |
| chrome.action.isEnabled(%d, (enabled) => { |
| chrome.test.sendScriptResult(enabled); |
| }); |
| )"; |
| base::Value script_result = BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), |
| base::StringPrintf(kCheckIsEnabledStatusForTabId, tab_id), |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| EXPECT_FALSE(script_result.GetBool()); |
| } |
| |
| using ActionAPITest = ExtensionApiTest; |
| |
| IN_PROC_BROWSER_TEST_F(ActionAPITest, TestGetUserSettings) { |
| constexpr char kManifest[] = |
| R"({ |
| "name": "getUserSettings Test", |
| "manifest_version": 3, |
| "version": "1", |
| "background": {"service_worker": "worker.js"}, |
| "action": {} |
| })"; |
| constexpr char kWorker[] = |
| R"(chrome.action.onClicked.addListener(async () => { |
| const settings = await chrome.action.getUserSettings(); |
| chrome.test.sendMessage(JSON.stringify(settings)); |
| }); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker); |
| |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| ToolbarActionsModel* const toolbar_model = |
| ToolbarActionsModel::Get(profile()); |
| EXPECT_FALSE(toolbar_model->IsActionPinned(extension->id())); |
| |
| std::unique_ptr<ExtensionActionTestHelper> toolbar_helper = |
| ExtensionActionTestHelper::Create(browser()); |
| |
| auto get_response = [extension, toolbar_helper = toolbar_helper.get()]() { |
| ExtensionTestMessageListener listener; |
| listener.set_extension_id(extension->id()); |
| toolbar_helper->Press(extension->id()); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| return listener.message(); |
| }; |
| |
| EXPECT_EQ(R"({"isOnToolbar":false})", get_response()); |
| |
| toolbar_model->SetActionVisibility(extension->id(), true); |
| EXPECT_TRUE(toolbar_model->IsActionPinned(extension->id())); |
| |
| EXPECT_EQ(R"({"isOnToolbar":true})", get_response()); |
| } |
| |
| // Tests dispatching the onUserSettingsChanged event to listeners when the user |
| // pins or unpins the extension action. |
| IN_PROC_BROWSER_TEST_F(ActionAPITest, OnUserSettingsChanged) { |
| constexpr char kManifest[] = |
| R"({ |
| "name": "onUserSettingsChanged Test", |
| "manifest_version": 3, |
| "version": "1", |
| "background": {"service_worker": "worker.js"}, |
| "action": {} |
| })"; |
| constexpr char kWorker[] = |
| R"(chrome.action.onUserSettingsChanged.addListener(change => { |
| chrome.test.sendMessage(JSON.stringify(change)); |
| });)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| ToolbarActionsModel* const toolbar_model = |
| ToolbarActionsModel::Get(profile()); |
| ASSERT_FALSE(toolbar_model->IsActionPinned(extension->id())); |
| |
| auto change_visibility_and_get_response = [extension, |
| toolbar_model](bool pinned_state) { |
| ExtensionTestMessageListener listener; |
| listener.set_extension_id(extension->id()); |
| toolbar_model->SetActionVisibility(extension->id(), pinned_state); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| return listener.message(); |
| }; |
| |
| EXPECT_EQ(R"({"isOnToolbar":true})", |
| change_visibility_and_get_response(/*pinned_state=*/true)); |
| |
| EXPECT_EQ(R"({"isOnToolbar":false})", |
| change_visibility_and_get_response(/*pinned_state=*/false)); |
| } |
| |
| // Tests that invalid badge text colors return an API error to the caller. |
| IN_PROC_BROWSER_TEST_F(ActionAPITest, TestBadgeTextColorErrors) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "alpha transparent error test", |
| "version": "0.1", |
| "manifest_version": 3, |
| "action": {}, |
| "background": {"service_worker": "background.js" } |
| })"; |
| static constexpr char kBackgroundJs[] = |
| R"( |
| const tabId = %d; |
| const expectedError = '%s'; |
| chrome.test.runTests([ |
| async function badgeColorEmptyValueInvalid() { |
| await chrome.test.assertPromiseRejects( |
| chrome.action.setBadgeTextColor( |
| {color: '', tabId}), |
| 'Error: ' + expectedError); |
| chrome.test.succeed(); |
| }, |
| async function badgeColorAlphaTransparentInvalid() { |
| await chrome.test.assertPromiseRejects( |
| chrome.action.setBadgeTextColor( |
| {color: [255, 255, 255, 0], tabId}), |
| 'Error: ' + expectedError); |
| chrome.test.succeed(); |
| } |
| ]); |
| )"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifestTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtmlTemplate); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(kBackgroundJs, tab_id, |
| extension_misc::kInvalidColorError)); |
| |
| ResultCatcher result_catcher; |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| |
| // Tests the setting and unsetting of badge text works for both global and tab |
| // specific cases. |
| IN_PROC_BROWSER_TEST_P(ActionAndBrowserActionAPITest, |
| TestSetBadgeTextGlobalAndTab) { |
| constexpr char kManifestTemplate[] = |
| R"({ |
| "name": "Test unsetting tab specific test", |
| "version": "0.1", |
| "manifest_version": %d, |
| "%s": {}, |
| "background": { %s } |
| })"; |
| const char* background_specification = |
| GetParam() == ActionInfo::Type::kAction |
| ? R"("service_worker": "background.js")" |
| : R"("scripts": ["background.js"])"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(base::StringPrintf( |
| kManifestTemplate, GetManifestVersionForActionType(GetParam()), |
| ActionInfo::GetManifestKeyForActionType(GetParam()), |
| background_specification)); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// Empty"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ExtensionAction* action = GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| const int tab_id1 = GetActiveTabId(); |
| EnsureActionIsEnabledOnTab(action, tab_id1); |
| |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), GURL("chrome://newtab"), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| |
| const int tab_id2 = GetActiveTabId(); |
| EnsureActionIsEnabledOnTab(action, tab_id2); |
| |
| constexpr char kGlobalText[] = "Global text"; |
| constexpr char kTabText[] = "Tab text"; |
| |
| const std::string kSetGlobalText = base::StringPrintf( |
| R"( |
| chrome.%s.setBadgeText({text: 'Global text'}, () => { |
| chrome.test.sendScriptResult(true); |
| }); |
| )", |
| GetAPINameForActionType(GetParam())); |
| const std::string kUnsetGlobalText = base::StringPrintf( |
| R"( |
| chrome.%s.setBadgeText({}, () => { |
| chrome.test.sendScriptResult(true); |
| }); |
| )", |
| GetAPINameForActionType(GetParam())); |
| const std::string kSetTabText = base::StringPrintf( |
| R"( |
| chrome.%s.setBadgeText({tabId: %d, text: 'Tab text'}, () => { |
| chrome.test.sendScriptResult(true); |
| }); |
| )", |
| GetAPINameForActionType(GetParam()), tab_id1); |
| const std::string kUnsetTabText = base::StringPrintf( |
| R"( |
| chrome.%s.setBadgeText({tabId: %d}, () => { |
| chrome.test.sendScriptResult(true); |
| }); |
| )", |
| GetAPINameForActionType(GetParam()), tab_id1); |
| |
| auto run_script_and_wait_for_callback = [&](std::string script) { |
| base::Value script_result = BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), script, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| return script_result; |
| }; |
| |
| EXPECT_EQ("", action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ("", action->GetExplicitlySetBadgeText(tab_id2)); |
| |
| // Set a global text for all tabs. |
| EXPECT_TRUE(run_script_and_wait_for_callback(kSetGlobalText).GetBool()); |
| EXPECT_EQ(kGlobalText, action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ(kGlobalText, action->GetExplicitlySetBadgeText(tab_id2)); |
| |
| // Now set a tab specific text for tab 1. |
| EXPECT_TRUE(run_script_and_wait_for_callback(kSetTabText).GetBool()); |
| EXPECT_EQ(kTabText, action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ(kGlobalText, action->GetExplicitlySetBadgeText(tab_id2)); |
| |
| // Unsetting the global text will leave the tab specific text in place. |
| EXPECT_TRUE(run_script_and_wait_for_callback(kUnsetGlobalText).GetBool()); |
| EXPECT_EQ(kTabText, action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ("", action->GetExplicitlySetBadgeText(tab_id2)); |
| |
| // Adding the global text back will not effect the tab specific text. |
| EXPECT_TRUE(run_script_and_wait_for_callback(kSetGlobalText).GetBool()); |
| EXPECT_EQ(kTabText, action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ(kGlobalText, action->GetExplicitlySetBadgeText(tab_id2)); |
| |
| // Unsetting the tab specific text will return that tab to the global text. |
| EXPECT_TRUE(run_script_and_wait_for_callback(kUnsetTabText).GetBool()); |
| EXPECT_EQ(kGlobalText, action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ(kGlobalText, action->GetExplicitlySetBadgeText(tab_id2)); |
| |
| // Finally unsetting the global text will return us back to nothing set. |
| EXPECT_TRUE(run_script_and_wait_for_callback(kUnsetGlobalText).GetBool()); |
| EXPECT_EQ("", action->GetExplicitlySetBadgeText(tab_id1)); |
| EXPECT_EQ("", action->GetExplicitlySetBadgeText(tab_id2)); |
| } |
| |
| class ExtensionActionWithOpenPopupFeatureDisabledTest |
| : public ExtensionActionAPITest { |
| public: |
| ExtensionActionWithOpenPopupFeatureDisabledTest() { |
| feature_list_.InitAndDisableFeature( |
| extensions_features::kApiActionOpenPopup); |
| } |
| ~ExtensionActionWithOpenPopupFeatureDisabledTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that the action.openPopup() API is available to policy-installed |
| // extensions on even if the feature flag is disabled. Since this is controlled |
| // through our features files (which are tested separately), this is more of a |
| // smoke test than an end-to-end test. |
| // TODO(crbug.com/40057101): Remove this test when the API is available |
| // for all extensions on stable without a feature flag. |
| IN_PROC_BROWSER_TEST_F(ExtensionActionWithOpenPopupFeatureDisabledTest, |
| OpenPopupAvailabilityOnStableChannel) { |
| TestExtensionDir test_dir; |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "Test", |
| "manifest_version": 3, |
| "version": "0.1", |
| "background": {"service_worker": "background.js"}, |
| "action": {} |
| })"; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| "chrome.test.sendMessage('ready');"); |
| |
| auto is_open_popup_defined = [this](const Extension& extension) { |
| static constexpr char kScript[] = |
| R"(chrome.test.sendScriptResult(!!chrome.action.openPopup);)"; |
| return BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension.id(), kScript, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| }; |
| |
| // Technically, we don't need the "ready" listener here, but this ensures we |
| // don't cross streams with the policy extension loaded below (where we do |
| // need the listener). |
| ExtensionTestMessageListener non_policy_listener("ready"); |
| const Extension* non_policy_extension = |
| LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(non_policy_extension); |
| ASSERT_TRUE(non_policy_listener.WaitUntilSatisfied()); |
| |
| // Somewhat annoying: due to how our test helpers are written, |
| // `EXPECT_EQ(false, base::Value)` works, but EXPECT_FALSE(base::Value) does |
| // not. |
| EXPECT_EQ(false, is_open_popup_defined(*non_policy_extension)); |
| |
| // Unlike `LoadExtension()`, `InstallExtension()` doesn't wait for the service |
| // worker to be ready, so we need a few manual waiters. |
| base::FilePath packed_path = test_dir.Pack(); |
| service_worker_test_utils::TestServiceWorkerContextObserver |
| registration_observer(profile()); |
| ExtensionTestMessageListener policy_listener("ready"); |
| const Extension* policy_extension = InstallExtension( |
| packed_path, 1, mojom::ManifestLocation::kExternalPolicyDownload); |
| ASSERT_TRUE(policy_extension); |
| ASSERT_TRUE(policy_listener.WaitUntilSatisfied()); |
| registration_observer.WaitForRegistrationStored(); |
| |
| EXPECT_EQ(true, is_open_popup_defined(*policy_extension)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| MultiActionAPITest, |
| testing::Values(ActionInfo::Type::kAction, |
| ActionInfo::Type::kPage, |
| ActionInfo::Type::kBrowser)); |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| ActionAndBrowserActionAPITest, |
| testing::Values(ActionInfo::Type::kAction, |
| ActionInfo::Type::kBrowser)); |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| MultiActionAPICanvasTest, |
| testing::Values(ActionInfo::Type::kAction, |
| ActionInfo::Type::kPage, |
| ActionInfo::Type::kBrowser)); |
| |
| } // namespace extensions |