| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/macros.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/bookmarks/bookmark_model_factory.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/extensions/extension_action.h" |
| #include "chrome/browser/extensions/extension_action_manager.h" |
| #include "chrome/browser/extensions/extension_action_test_util.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/sessions/session_tab_helper.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/test_extension_registry_observer.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| namespace extensions { |
| namespace { |
| |
| const char kDeclarativeContentManifest[] = |
| "{\n" |
| " \"name\": \"Declarative Content apitest\",\n" |
| " \"version\": \"0.1\",\n" |
| " \"manifest_version\": 2,\n" |
| " \"description\": \n" |
| " \"end-to-end browser test for the declarative Content API\",\n" |
| " \"background\": {\n" |
| " \"scripts\": [\"background.js\"]\n" |
| " },\n" |
| " \"page_action\": {},\n" |
| " \"permissions\": [\n" |
| " \"declarativeContent\", \"bookmarks\"\n" |
| " ],\n" |
| " \"incognito\": \"spanning\"\n" |
| "}\n"; |
| |
| const char kIncognitoSpecificBackground[] = |
| "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n" |
| "var ShowAction = chrome.declarativeContent.ShowAction;\n" |
| "var inIncognitoContext = chrome.extension.inIncognitoContext;\n" |
| "\n" |
| "var hostPrefix = chrome.extension.inIncognitoContext ?\n" |
| " 'test_split' : 'test_normal';\n" |
| "var rule = {\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: hostPrefix}})],\n" |
| " actions: [new ShowAction()]\n" |
| "}\n" |
| "\n" |
| "var onPageChanged = chrome.declarativeContent.onPageChanged;\n" |
| "onPageChanged.removeRules(undefined, function() {\n" |
| " onPageChanged.addRules([rule], function() {\n" |
| " chrome.test.sendMessage(\n" |
| " !inIncognitoContext ? \"ready\" : \"ready (split)\");\n" |
| " });\n" |
| "});\n"; |
| |
| const char kBackgroundHelpers[] = |
| "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n" |
| "var ShowAction = chrome.declarativeContent.ShowAction;\n" |
| "var onPageChanged = chrome.declarativeContent.onPageChanged;\n" |
| "var reply = window.domAutomationController.send.bind(\n" |
| " window.domAutomationController);\n" |
| "\n" |
| "function setRules(rules, responseString) {\n" |
| " onPageChanged.removeRules(undefined, function() {\n" |
| " onPageChanged.addRules(rules, function() {\n" |
| " if (chrome.runtime.lastError) {\n" |
| " reply(chrome.runtime.lastError.message);\n" |
| " return;\n" |
| " }\n" |
| " reply(responseString);\n" |
| " });\n" |
| " });\n" |
| "};\n" |
| "\n" |
| "function addRules(rules, responseString) {\n" |
| " onPageChanged.addRules(rules, function() {\n" |
| " if (chrome.runtime.lastError) {\n" |
| " reply(chrome.runtime.lastError.message);\n" |
| " return;\n" |
| " }\n" |
| " reply(responseString);\n" |
| " });\n" |
| "};\n" |
| "\n" |
| "function removeRule(id, responseString) {\n" |
| " onPageChanged.removeRules([id], function() {\n" |
| " if (chrome.runtime.lastError) {\n" |
| " reply(chrome.runtime.lastError.message);\n" |
| " return;\n" |
| " }\n" |
| " reply(responseString);\n" |
| " });\n" |
| "};\n"; |
| |
| class DeclarativeContentApiTest : public ExtensionApiTest { |
| public: |
| DeclarativeContentApiTest() {} |
| |
| protected: |
| enum IncognitoMode { SPANNING, SPLIT }; |
| |
| // Checks that the rules are correctly evaluated for an extension in incognito |
| // mode |mode| when the extension's incognito enable state is |
| // |is_enabled_in_incognito|. |
| void CheckIncognito(IncognitoMode mode, bool is_enabled_in_incognito); |
| |
| // Checks that the rules matching a bookmarked state of |is_bookmarked| are |
| // correctly evaluated on bookmark events. |
| void CheckBookmarkEvents(bool is_bookmarked); |
| |
| TestExtensionDir ext_dir_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DeclarativeContentApiTest); |
| }; |
| |
| void DeclarativeContentApiTest::CheckIncognito(IncognitoMode mode, |
| bool is_enabled_in_incognito) { |
| std::string manifest = kDeclarativeContentManifest; |
| if (mode == SPLIT) { |
| base::ReplaceSubstringsAfterOffset( |
| &manifest, 0, "\"incognito\": \"spanning\"", |
| "\"incognito\": \"split\""); |
| ASSERT_NE(kDeclarativeContentManifest, manifest); |
| } |
| ext_dir_.WriteManifest(manifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), |
| kIncognitoSpecificBackground); |
| |
| ExtensionTestMessageListener ready("ready", false); |
| ExtensionTestMessageListener ready_incognito("ready (split)", false); |
| |
| const Extension* extension = |
| is_enabled_in_incognito ? LoadExtensionIncognito(ext_dir_.UnpackedPath()) |
| : LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| const ExtensionAction* incognito_action = |
| ExtensionActionManager::Get(incognito_browser->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(incognito_action); |
| |
| ASSERT_TRUE(ready.WaitUntilSatisfied()); |
| if (is_enabled_in_incognito && mode == SPLIT) |
| ASSERT_TRUE(ready_incognito.WaitUntilSatisfied()); |
| |
| content::WebContents* const incognito_tab = |
| incognito_browser->tab_strip_model()->GetWebContentsAt(0); |
| const int incognito_tab_id = ExtensionTabUtil::GetTabId(incognito_tab); |
| |
| EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id)); |
| |
| NavigateInRenderer(incognito_tab, GURL("http://test_split/")); |
| if (mode == SPLIT) { |
| EXPECT_EQ(is_enabled_in_incognito, |
| incognito_action->GetIsVisible(incognito_tab_id)); |
| } else { |
| EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id)); |
| } |
| |
| NavigateInRenderer(incognito_tab, GURL("http://test_normal/")); |
| if (mode != SPLIT) { |
| EXPECT_EQ(is_enabled_in_incognito, |
| incognito_action->GetIsVisible(incognito_tab_id)); |
| } else { |
| EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id)); |
| } |
| |
| // Verify that everything works as expected in the non-incognito browser. |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| NavigateInRenderer(tab, GURL("http://test_normal/")); |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| |
| NavigateInRenderer(tab, GURL("http://test_split/")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| } |
| |
| void DeclarativeContentApiTest::CheckBookmarkEvents(bool match_is_bookmarked) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| NavigateInRenderer(tab, GURL("http://test1/")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| static const char kSetIsBookmarkedRule[] = |
| "setRules([{\n" |
| " conditions: [new PageStateMatcher({isBookmarked: %s})],\n" |
| " actions: [new ShowAction()]\n" |
| "}], 'test_rule');\n"; |
| |
| EXPECT_EQ("test_rule", ExecuteScriptInBackgroundPage( |
| extension->id(), |
| base::StringPrintf(kSetIsBookmarkedRule, |
| match_is_bookmarked ? "true" : "false"))); |
| EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id)); |
| |
| // Check rule evaluation on add/remove bookmark. |
| bookmarks::BookmarkModel* bookmark_model = |
| BookmarkModelFactory::GetForBrowserContext(browser()->profile()); |
| const bookmarks::BookmarkNode* node = |
| bookmark_model->AddURL(bookmark_model->other_node(), 0, |
| base::ASCIIToUTF16("title"), |
| GURL("http://test1/")); |
| EXPECT_EQ(match_is_bookmarked, action->GetIsVisible(tab_id)); |
| |
| bookmark_model->Remove(node); |
| EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id)); |
| |
| // Check rule evaluation on navigate to bookmarked and non-bookmarked URL. |
| bookmark_model->AddURL(bookmark_model->other_node(), 0, |
| base::ASCIIToUTF16("title"), |
| GURL("http://test2/")); |
| |
| NavigateInRenderer(tab, GURL("http://test2/")); |
| EXPECT_EQ(match_is_bookmarked, action->GetIsVisible(tab_id)); |
| |
| NavigateInRenderer(tab, GURL("http://test3/")); |
| EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id)); |
| } |
| |
| // Disabled due to flake. https://crbug.com/606574. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, DISABLED_Overview) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| "var declarative = chrome.declarative;\n" |
| "\n" |
| "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n" |
| "var ShowAction = chrome.declarativeContent.ShowAction;\n" |
| "\n" |
| "var rule = {\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test1\"}}),\n" |
| " new PageStateMatcher({\n" |
| " css: [\"input[type='password']\"]})],\n" |
| " actions: [new ShowAction()]\n" |
| "}\n" |
| "\n" |
| "var testEvent = chrome.declarativeContent.onPageChanged;\n" |
| "\n" |
| "testEvent.removeRules(undefined, function() {\n" |
| " testEvent.addRules([rule], function() {\n" |
| " chrome.test.sendMessage(\"ready\")\n" |
| " });\n" |
| "});\n"); |
| ExtensionTestMessageListener ready("ready", false); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| ASSERT_TRUE(ready.WaitUntilSatisfied()); |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| NavigateInRenderer(tab, GURL("http://test1/")); |
| |
| // The declarative API should show the page action instantly, rather |
| // than waiting for the extension to run. |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| |
| // Make sure leaving a matching page unshows the page action. |
| NavigateInRenderer(tab, GURL("http://not_checked/")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| // Insert a password field to make sure that's noticed. |
| // Notice that we touch offsetTop to force a synchronous layout. |
| ASSERT_TRUE(content::ExecuteScript( |
| tab, "document.body.innerHTML = '<input type=\"password\">';" |
| "document.body.offsetTop;")); |
| |
| // Give the style match a chance to run and send back the matching-selector |
| // update. |
| ASSERT_TRUE(content::ExecuteScript(tab, std::string())); |
| |
| EXPECT_TRUE(action->GetIsVisible(tab_id)) |
| << "Adding a matching element should show the page action."; |
| |
| // Remove it again to make sure that reverts the action. |
| // Notice that we touch offsetTop to force a synchronous layout. |
| ASSERT_TRUE(content::ExecuteScript( |
| tab, "document.body.innerHTML = 'Hello world';" |
| "document.body.offsetTop;")); |
| |
| // Give the style match a chance to run and send back the matching-selector |
| // update. |
| ASSERT_TRUE(content::ExecuteScript(tab, std::string())); |
| |
| EXPECT_FALSE(action->GetIsVisible(tab_id)) |
| << "Removing the matching element should hide the page action again."; |
| } |
| |
| // Test that adds two rules pointing to single action instance. |
| // Regression test for http://crbug.com/574149. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, ReusedActionInstance) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| "var declarative = chrome.declarative;\n" |
| "\n" |
| "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n" |
| "var ShowAction = chrome.declarativeContent.ShowAction;\n" |
| "var actionInstance = new ShowAction();\n" |
| "\n" |
| "var rule1 = {\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test1\"}})],\n" |
| " actions: [actionInstance]\n" |
| "};\n" |
| "var rule2 = {\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test\"}})],\n" |
| " actions: [actionInstance]\n" |
| "};\n" |
| "\n" |
| "var testEvent = chrome.declarativeContent.onPageChanged;\n" |
| "\n" |
| "testEvent.removeRules(undefined, function() {\n" |
| " testEvent.addRules([rule1, rule2], function() {\n" |
| " chrome.test.sendMessage(\"ready\");\n" |
| " });\n" |
| "});\n"); |
| ExtensionTestMessageListener ready("ready", false); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| // Wait for declarative rules to be set up. |
| content::BrowserContext::GetDefaultStoragePartition(profile()) |
| ->FlushNetworkInterfaceForTesting(); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| ASSERT_TRUE(ready.WaitUntilSatisfied()); |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| // This navigation matches both rules. |
| NavigateInRenderer(tab, GURL("http://test1/")); |
| |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| } |
| |
| // Tests that the rules are evaluated at the time they are added or removed. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, RulesEvaluatedOnAddRemove) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| NavigateInRenderer(tab, GURL("http://test1/")); |
| |
| const std::string kAddTestRules = |
| "addRules([{\n" |
| " id: '1',\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test1\"}})],\n" |
| " actions: [new ShowAction()]\n" |
| "}, {\n" |
| " id: '2',\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test2\"}})],\n" |
| " actions: [new ShowAction()]\n" |
| "}], 'add_rules');\n"; |
| EXPECT_EQ("add_rules", |
| ExecuteScriptInBackgroundPage(extension->id(), kAddTestRules)); |
| |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| |
| const std::string kRemoveTestRule1 = "removeRule('1', 'remove_rule1');\n"; |
| EXPECT_EQ("remove_rule1", |
| ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule1)); |
| |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| NavigateInRenderer(tab, GURL("http://test2/")); |
| |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| |
| const std::string kRemoveTestRule2 = "removeRule('2', 'remove_rule2');\n"; |
| EXPECT_EQ("remove_rule2", |
| ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule2)); |
| |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| } |
| |
| class ParameterizedShowActionDeclarativeContentApiTest |
| : public DeclarativeContentApiTest, |
| public testing::WithParamInterface<const char*> { |
| public: |
| ParameterizedShowActionDeclarativeContentApiTest() {} |
| ~ParameterizedShowActionDeclarativeContentApiTest() override {} |
| |
| void TestShowAction(base::Optional<ActionInfo::Type> action_type); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ParameterizedShowActionDeclarativeContentApiTest); |
| }; |
| |
| void ParameterizedShowActionDeclarativeContentApiTest::TestShowAction( |
| base::Optional<ActionInfo::Type> action_type) { |
| std::string manifest_with_custom_action = kDeclarativeContentManifest; |
| std::string action_key; |
| if (action_type) { |
| switch (*action_type) { |
| case ActionInfo::TYPE_BROWSER: |
| action_key = R"("browser_action": {},)"; |
| break; |
| case ActionInfo::TYPE_PAGE: |
| action_key = R"("page_action": {},)"; |
| break; |
| case ActionInfo::TYPE_ACTION: |
| action_key = R"("action": {},)"; |
| break; |
| } |
| } |
| |
| base::ReplaceSubstringsAfterOffset(&manifest_with_custom_action, 0, |
| "\"page_action\": {},", action_key); |
| ext_dir_.WriteManifest(manifest_with_custom_action); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| |
| ChromeTestExtensionLoader loader(profile()); |
| scoped_refptr<const Extension> extension = |
| loader.LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| // Wait for declarative rules to be set up. |
| content::BrowserContext::GetDefaultStoragePartition(profile()) |
| ->FlushNetworkInterfaceForTesting(); |
| |
| ExtensionAction* action = ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| // Extensions that don't provide an action are given a page action by default |
| // (for visibility reasons). |
| ASSERT_TRUE(action); |
| |
| // Ensure actions are hidden (so that the ShowAction() rule has an effect). |
| if (action->default_state() == ActionInfo::STATE_DISABLED) |
| action->SetIsVisible(ExtensionAction::kDefaultTabId, false); |
| |
| const char kScript[] = |
| "setRules([{\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test\"}})],\n" |
| " actions: [new chrome.declarativeContent.%s()]\n" |
| "}], 'test_rule');\n"; |
| const char kSuccessStr[] = "test_rule"; |
| |
| std::string result = ExecuteScriptInBackgroundPage( |
| extension->id(), base::StringPrintf(kScript, GetParam())); |
| |
| // Since extensions with no action provided are given a page action by default |
| // (for visibility reasons) and ShowAction() should also work with |
| // browser actions, both of these should pass. |
| EXPECT_THAT(result, testing::HasSubstr(kSuccessStr)); |
| |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| NavigateInRenderer(tab, GURL("http://test/")); |
| |
| const int tab_id = SessionTabHelper::IdForTab(tab).id(); |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| |
| // If an extension had no action specified in the manifest, it will get a |
| // synthesized page action. |
| ActionInfo::Type expected_type = action_type.value_or(ActionInfo::TYPE_PAGE); |
| EXPECT_EQ(expected_type, action->action_type()); |
| EXPECT_EQ(expected_type == ActionInfo::TYPE_PAGE ? 1u : 0u, |
| extension_action_test_util::GetVisiblePageActionCount(tab)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest, |
| NoActionInManifest) { |
| TestShowAction(base::nullopt); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest, |
| PageActionInManifest) { |
| TestShowAction(ActionInfo::TYPE_PAGE); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest, |
| BrowserActionInManifest) { |
| TestShowAction(ActionInfo::TYPE_BROWSER); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest, |
| ActionInManifest) { |
| TestShowAction(ActionInfo::TYPE_ACTION); |
| } |
| |
| // Tests that rules from manifest are added and evaluated properly. |
| IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest, |
| RulesAddedFromManifest) { |
| const char manifest[] = |
| "{\n" |
| " \"name\": \"Declarative Content apitest\",\n" |
| " \"version\": \"0.1\",\n" |
| " \"manifest_version\": 2,\n" |
| " \"page_action\": {},\n" |
| " \"permissions\": [\n" |
| " \"declarativeContent\"\n" |
| " ],\n" |
| " \"event_rules\": [{\n" |
| " \"event\": \"declarativeContent.onPageChanged\",\n" |
| " \"actions\": [{\n" |
| " \"type\": \"declarativeContent.%s\"\n" |
| " }],\n" |
| " \"conditions\": [{\n" |
| " \"type\": \"declarativeContent.PageStateMatcher\",\n" |
| " \"pageUrl\": {\"hostPrefix\": \"test1\"}\n" |
| " }]\n" |
| " }]\n" |
| "}\n"; |
| ext_dir_.WriteManifest(base::StringPrintf(manifest, GetParam())); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| NavigateInRenderer(tab, GURL("http://blank/")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| NavigateInRenderer(tab, GURL("http://test1/")); |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| NavigateInRenderer(tab, GURL("http://test2/")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(LegacyShowActionKey, |
| ParameterizedShowActionDeclarativeContentApiTest, |
| ::testing::Values("ShowPageAction")); |
| INSTANTIATE_TEST_SUITE_P(ModernShowActionKey, |
| ParameterizedShowActionDeclarativeContentApiTest, |
| ::testing::Values("ShowAction")); |
| |
| // Tests that rules are not evaluated in incognito browser windows when the |
| // extension specifies spanning incognito mode but is not enabled for incognito. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| DisabledForSpanningIncognito) { |
| CheckIncognito(SPANNING, false); |
| } |
| |
| // Tests that rules are evaluated in incognito browser windows when the |
| // extension specifies spanning incognito mode and is enabled for incognito. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, EnabledForSpanningIncognito) { |
| CheckIncognito(SPANNING, true); |
| } |
| |
| // Tests that rules are not evaluated in incognito browser windows when the |
| // extension specifies split incognito mode but is not enabled for incognito. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, DisabledForSplitIncognito) { |
| CheckIncognito(SPLIT, false); |
| } |
| |
| // Tests that rules are evaluated in incognito browser windows when the |
| // extension specifies split incognito mode and is enabled for incognito. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, EnabledForSplitIncognito) { |
| CheckIncognito(SPLIT, true); |
| } |
| |
| // Tests that rules are evaluated for an incognito tab that exists at the time |
| // the rules are added. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| RulesEvaluatedForExistingIncognitoTab) { |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| content::WebContents* const incognito_tab = |
| incognito_browser->tab_strip_model()->GetWebContentsAt(0); |
| const int incognito_tab_id = ExtensionTabUtil::GetTabId(incognito_tab); |
| |
| NavigateInRenderer(incognito_tab, GURL("http://test_normal/")); |
| |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), |
| kIncognitoSpecificBackground); |
| ExtensionTestMessageListener ready("ready", false); |
| const Extension* extension = LoadExtensionIncognito(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(ready.WaitUntilSatisfied()); |
| |
| const ExtensionAction* incognito_action = |
| ExtensionActionManager::Get(incognito_browser->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(incognito_action); |
| |
| // The page action should be shown. |
| EXPECT_TRUE(incognito_action->GetIsVisible(incognito_tab_id)); |
| } |
| |
| // Sets up rules matching http://test1/ in a normal and incognito browser. |
| // Frequently times out on ChromiumOS, Linux ASan, and Windows: |
| // https://crbug.com/512431. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| DISABLED_PRE_RulesPersistence) { |
| ExtensionTestMessageListener ready("ready", false); |
| ExtensionTestMessageListener ready_split("ready (split)", false); |
| // An on-disk extension is required so that it can be reloaded later in the |
| // RulesPersistence test. |
| const Extension* extension = |
| LoadExtensionIncognito(test_data_dir_.AppendASCII("declarative_content") |
| .AppendASCII("persistence")); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(ready.WaitUntilSatisfied()); |
| |
| CreateIncognitoBrowser(); |
| ASSERT_TRUE(ready_split.WaitUntilSatisfied()); |
| } |
| |
| // Reloads the extension from PRE_RulesPersistence and checks that the rules |
| // continue to work as expected after being persisted and reloaded. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, DISABLED_RulesPersistence) { |
| ExtensionTestMessageListener ready("second run ready", false); |
| ExtensionTestMessageListener ready_split("second run ready (split)", false); |
| ASSERT_TRUE(ready.WaitUntilSatisfied()); |
| |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile()); |
| const Extension* extension = |
| GetExtensionByPath(registry->enabled_extensions(), |
| test_data_dir_.AppendASCII("declarative_content") |
| .AppendASCII("persistence")); |
| |
| // Check non-incognito browser. |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| NavigateInRenderer(tab, GURL("http://test_normal/")); |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| |
| NavigateInRenderer(tab, GURL("http://test_split/")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| // Check incognito browser. |
| Browser* incognito_browser = CreateIncognitoBrowser(); |
| ASSERT_TRUE(ready_split.WaitUntilSatisfied()); |
| content::WebContents* const incognito_tab = |
| incognito_browser->tab_strip_model()->GetWebContentsAt(0); |
| const int incognito_tab_id = ExtensionTabUtil::GetTabId(incognito_tab); |
| |
| const ExtensionAction* incognito_action = |
| ExtensionActionManager::Get(incognito_browser->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(incognito_action); |
| |
| NavigateInRenderer(incognito_tab, GURL("http://test_split/")); |
| EXPECT_TRUE(incognito_action->GetIsVisible(incognito_tab_id)); |
| |
| NavigateInRenderer(incognito_tab, GURL("http://test_normal/")); |
| EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id)); |
| } |
| |
| // http://crbug.com/304373 |
| #if defined(OS_WIN) |
| // Fails on XP: http://crbug.com/515717 |
| #define MAYBE_UninstallWhileActivePageAction \ |
| DISABLED_UninstallWhileActivePageAction |
| #else |
| #define MAYBE_UninstallWhileActivePageAction UninstallWhileActivePageAction |
| #endif |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| MAYBE_UninstallWhileActivePageAction) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| // Wait for declarative rules to be set up. |
| content::BrowserContext::GetDefaultStoragePartition(profile()) |
| ->FlushNetworkInterfaceForTesting(); |
| const std::string extension_id = extension->id(); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| const std::string kTestRule = |
| "setRules([{\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test\"}})],\n" |
| " actions: [new ShowAction()]\n" |
| "}], 'test_rule');\n"; |
| EXPECT_EQ("test_rule", |
| ExecuteScriptInBackgroundPage(extension_id, kTestRule)); |
| |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| NavigateInRenderer(tab, GURL("http://test/")); |
| |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(1)); |
| EXPECT_EQ(1u, extension_action_test_util::GetVisiblePageActionCount(tab)); |
| EXPECT_EQ(1u, extension_action_test_util::GetTotalPageActionCount(tab)); |
| |
| ReloadExtension(extension_id); // Invalidates action and extension. |
| // Wait for declarative rules to be removed. |
| content::BrowserContext::GetDefaultStoragePartition(profile()) |
| ->FlushNetworkInterfaceForTesting(); |
| EXPECT_EQ("test_rule", |
| ExecuteScriptInBackgroundPage(extension_id, kTestRule)); |
| // TODO(jyasskin): Apply new rules to existing tabs, without waiting for a |
| // navigation. |
| NavigateInRenderer(tab, GURL("http://test/")); |
| EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(1)); |
| EXPECT_EQ(1u, extension_action_test_util::GetVisiblePageActionCount(tab)); |
| EXPECT_EQ(1u, extension_action_test_util::GetTotalPageActionCount(tab)); |
| |
| UnloadExtension(extension_id); |
| // Wait for declarative rules to be removed. |
| content::BrowserContext::GetDefaultStoragePartition(profile()) |
| ->FlushNetworkInterfaceForTesting(); |
| NavigateInRenderer(tab, GURL("http://test/")); |
| EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(0)); |
| EXPECT_EQ(0u, extension_action_test_util::GetVisiblePageActionCount(tab)); |
| EXPECT_EQ(0u, extension_action_test_util::GetTotalPageActionCount(tab)); |
| } |
| |
| // This tests against a renderer crash that was present during development. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| DISABLED_AddExtensionMatchingExistingTabWithDeadFrames) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| content::WebContents* const tab = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| const int tab_id = ExtensionTabUtil::GetTabId(tab); |
| |
| ASSERT_TRUE(content::ExecuteScript( |
| tab, "document.body.innerHTML = '<iframe src=\"http://test2\">';")); |
| // Replace the iframe to destroy its WebFrame. |
| ASSERT_TRUE(content::ExecuteScript( |
| tab, "document.body.innerHTML = '<span class=\"foo\">';")); |
| |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| |
| EXPECT_EQ("rule0", ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "setRules([{\n" |
| " conditions: [new PageStateMatcher({\n" |
| " css: [\"span[class=foo]\"]})],\n" |
| " actions: [new ShowAction()]\n" |
| "}], 'rule0');\n")); |
| // Give the renderer a chance to apply the rules change and notify the |
| // browser. This takes one time through the Blink message loop to receive |
| // the rule change and apply the new stylesheet, and a second to dedupe the |
| // update. |
| ASSERT_TRUE(content::ExecuteScript(tab, std::string())); |
| ASSERT_TRUE(content::ExecuteScript(tab, std::string())); |
| |
| EXPECT_FALSE(tab->IsCrashed()); |
| EXPECT_TRUE(action->GetIsVisible(tab_id)) |
| << "Loading an extension when an open page matches its rules " |
| << "should show the page action."; |
| |
| EXPECT_EQ("removed", |
| ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "onPageChanged.removeRules(undefined, function() {\n" |
| " window.domAutomationController.send('removed');\n" |
| "});\n")); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| CanonicalizesPageStateMatcherCss) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n" |
| "function Return(obj) {\n" |
| " window.domAutomationController.send('' + obj);\n" |
| "}\n"); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| EXPECT_EQ("input[type=\"password\"]", |
| ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "var psm = new PageStateMatcher(\n" |
| " {css: [\"input[type='password']\"]});\n" |
| "Return(psm.css);")); |
| |
| EXPECT_THAT(ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "try {\n" |
| " new PageStateMatcher({css: 'Not-an-array'});\n" |
| " Return('Failed to throw');\n" |
| "} catch (e) {\n" |
| " Return(e.message);\n" |
| "}\n"), |
| testing::ContainsRegex("css.*xpected '?array'?")); |
| EXPECT_THAT(ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "try {\n" |
| " new PageStateMatcher({css: [null]});\n" // Not a string. |
| " Return('Failed to throw');\n" |
| "} catch (e) {\n" |
| " Return(e.message);\n" |
| "}\n"), |
| testing::ContainsRegex("css.*0.*xpected '?string'?")); |
| EXPECT_THAT(ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "try {\n" |
| // Invalid CSS: |
| " new PageStateMatcher({css: [\"input''\"]});\n" |
| " Return('Failed to throw');\n" |
| "} catch (e) {\n" |
| " Return(e.message);\n" |
| "}\n"), |
| testing::ContainsRegex("valid.*: input''$")); |
| EXPECT_THAT(ExecuteScriptInBackgroundPage( |
| extension->id(), |
| "try {\n" |
| // "Complex" selector: |
| " new PageStateMatcher({css: ['div input']});\n" |
| " Return('Failed to throw');\n" |
| "} catch (e) {\n" |
| " Return(e.message);\n" |
| "}\n"), |
| testing::ContainsRegex("selector.*: div input$")); |
| } |
| |
| // Tests that the rules with isBookmarked: true are evaluated when handling |
| // bookmarking events. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| IsBookmarkedRulesEvaluatedOnBookmarkEvents) { |
| CheckBookmarkEvents(true); |
| } |
| |
| // Tests that the rules with isBookmarked: false are evaluated when handling |
| // bookmarking events. |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| NotBookmarkedRulesEvaluatedOnBookmarkEvents) { |
| CheckBookmarkEvents(false); |
| } |
| |
| // https://crbug.com/497586 |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| WebContentsWithoutTabAddedNotificationAtOnLoaded) { |
| // Add a web contents to the tab strip in a way that doesn't trigger |
| // NOTIFICATION_TAB_ADDED. |
| std::unique_ptr<content::WebContents> contents = content::WebContents::Create( |
| content::WebContents::CreateParams(profile())); |
| browser()->tab_strip_model()->AppendWebContents(std::move(contents), false); |
| |
| // The actual extension contents don't matter here -- we're just looking to |
| // trigger OnExtensionLoaded. |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| ASSERT_TRUE(LoadExtension(ext_dir_.UnpackedPath())); |
| } |
| |
| // https://crbug.com/501225 |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| PendingWebContentsClearedOnRemoveRules) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| const ExtensionAction* action = |
| ExtensionActionManager::Get(browser()->profile()) |
| ->GetExtensionAction(*extension); |
| ASSERT_TRUE(action); |
| |
| // Create two tabs. |
| content::WebContents* const tab1 = |
| browser()->tab_strip_model()->GetWebContentsAt(0); |
| |
| AddTabAtIndex(1, GURL("http://test2/"), ui::PAGE_TRANSITION_LINK); |
| content::WebContents* tab2 = |
| browser()->tab_strip_model()->GetWebContentsAt(1); |
| |
| // Add a rule matching the second tab. |
| const std::string kAddTestRules = |
| "addRules([{\n" |
| " id: '1',\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test1\"}})],\n" |
| " actions: [new ShowAction()]\n" |
| "}, {\n" |
| " id: '2',\n" |
| " conditions: [new PageStateMatcher({\n" |
| " pageUrl: {hostPrefix: \"test2\"}})],\n" |
| " actions: [new ShowAction()]\n" |
| "}], 'add_rules');\n"; |
| EXPECT_EQ("add_rules", |
| ExecuteScriptInBackgroundPage(extension->id(), kAddTestRules)); |
| EXPECT_TRUE(action->GetIsVisible(ExtensionTabUtil::GetTabId(tab2))); |
| |
| // Remove the rule. |
| const std::string kRemoveTestRule1 = "removeRule('2', 'remove_rule1');\n"; |
| EXPECT_EQ("remove_rule1", |
| ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule1)); |
| |
| // Remove the second tab, then trigger a rule evaluation for the remaining |
| // tab. |
| browser()->tab_strip_model()->DetachWebContentsAt( |
| browser()->tab_strip_model()->GetIndexOfWebContents(tab2)); |
| NavigateInRenderer(tab1, GURL("http://test1/")); |
| EXPECT_TRUE(action->GetIsVisible(ExtensionTabUtil::GetTabId(tab1))); |
| } |
| |
| // https://crbug.com/517492 |
| // Fails on XP: http://crbug.com/515717 |
| // Fails on other platfomrs: http://crbug.com/1013457 |
| IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, |
| DISABLED_RemoveAllRulesAfterExtensionUninstall) { |
| ext_dir_.WriteManifest(kDeclarativeContentManifest); |
| ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers); |
| |
| // Load the extension, add a rule, then uninstall the extension. |
| const Extension* extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| const std::string kAddTestRule = |
| "addRules([{\n" |
| " id: '1',\n" |
| " conditions: [],\n" |
| " actions: [new ShowAction()]\n" |
| "}], 'add_rule');\n"; |
| EXPECT_EQ("add_rule", |
| ExecuteScriptInBackgroundPage(extension->id(), kAddTestRule)); |
| |
| ExtensionService* extension_service = extensions::ExtensionSystem::Get( |
| browser()->profile())->extension_service(); |
| |
| base::string16 error; |
| ASSERT_TRUE(extension_service->UninstallExtension( |
| extension->id(), |
| UNINSTALL_REASON_FOR_TESTING, |
| &error)); |
| ASSERT_EQ(base::ASCIIToUTF16(""), error); |
| |
| // Reload the extension, then add and remove a rule. |
| extension = LoadExtension(ext_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| EXPECT_EQ("add_rule", |
| ExecuteScriptInBackgroundPage(extension->id(), kAddTestRule)); |
| |
| const std::string kRemoveTestRule1 = "removeRule('1', 'remove_rule1');\n"; |
| EXPECT_EQ("remove_rule1", |
| ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule1)); |
| } |
| |
| // TODO(wittman): Once ChromeContentRulesRegistry operates on condition and |
| // action interfaces, add a test that checks that a navigation always evaluates |
| // consistent URL state for all conditions. i.e.: if condition1 evaluates to |
| // false on url0 and true on url1, and condition2 evaluates to true on url0 and |
| // false on url1, navigate from url0 to url1 and validate that no action is |
| // triggered. Do the same when navigating back to url0. This kind of test is |
| // unfortunately not feasible with the current implementation and the existing |
| // supported conditions and actions. |
| |
| } // namespace |
| } // namespace extensions |