Home | History | Annotate | Download | only in declarative_content
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/extensions/extension_action.h"
      6 #include "chrome/browser/extensions/extension_action_manager.h"
      7 #include "chrome/browser/extensions/extension_apitest.h"
      8 #include "chrome/browser/extensions/extension_tab_util.h"
      9 #include "chrome/browser/extensions/extension_test_message_listener.h"
     10 #include "chrome/browser/extensions/test_extension_dir.h"
     11 #include "chrome/browser/ui/browser_window.h"
     12 #include "chrome/browser/ui/omnibox/location_bar.h"
     13 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     14 #include "chrome/common/extensions/features/feature_channel.h"
     15 #include "content/public/test/browser_test_utils.h"
     16 #include "testing/gmock/include/gmock/gmock.h"
     17 
     18 namespace extensions {
     19 namespace {
     20 
     21 const char kDeclarativeContentManifest[] =
     22     "{\n"
     23     "  \"name\": \"Declarative Content apitest\",\n"
     24     "  \"version\": \"0.1\",\n"
     25     "  \"manifest_version\": 2,\n"
     26     "  \"description\": \n"
     27     "      \"end-to-end browser test for the declarative Content API\",\n"
     28     "  \"background\": {\n"
     29     "    \"scripts\": [\"background.js\"]\n"
     30     "  },\n"
     31     "  \"page_action\": {},\n"
     32     "  \"permissions\": [\n"
     33     "    \"declarativeContent\"\n"
     34     "  ]\n"
     35     "}\n";
     36 
     37 const char kBackgroundHelpers[] =
     38     "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n"
     39     "var ShowPageAction = chrome.declarativeContent.ShowPageAction;\n"
     40     "var onPageChanged = chrome.declarativeContent.onPageChanged;\n"
     41     "var Reply = window.domAutomationController.send.bind(\n"
     42     "    window.domAutomationController);\n"
     43     "\n"
     44     "function setRules(rules, responseString) {\n"
     45     "  onPageChanged.removeRules(undefined, function() {\n"
     46     "    onPageChanged.addRules(rules, function() {\n"
     47     "      if (chrome.runtime.lastError) {\n"
     48     "        Reply(chrome.runtime.lastError.message);\n"
     49     "        return;\n"
     50     "      }\n"
     51     "      Reply(responseString);\n"
     52     "    });\n"
     53     "  });\n"
     54     "};\n";
     55 
     56 class DeclarativeContentApiTest : public ExtensionApiTest {
     57  public:
     58   DeclarativeContentApiTest()
     59       // Set the channel to "trunk" since declarativeContent is restricted
     60       // to trunk.
     61       : current_channel_(chrome::VersionInfo::CHANNEL_UNKNOWN) {
     62   }
     63   virtual ~DeclarativeContentApiTest() {}
     64 
     65   extensions::ScopedCurrentChannel current_channel_;
     66   TestExtensionDir ext_dir_;
     67 };
     68 
     69 IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, Overview) {
     70   ext_dir_.WriteManifest(kDeclarativeContentManifest);
     71   ext_dir_.WriteFile(
     72       FILE_PATH_LITERAL("background.js"),
     73       "var declarative = chrome.declarative;\n"
     74       "\n"
     75       "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n"
     76       "var ShowPageAction = chrome.declarativeContent.ShowPageAction;\n"
     77       "\n"
     78       "var rule0 = {\n"
     79       "  conditions: [new PageStateMatcher({\n"
     80       "                   pageUrl: {hostPrefix: \"test1\"}}),\n"
     81       "               new PageStateMatcher({\n"
     82       "                   css: [\"input[type='password']\"]})],\n"
     83       "  actions: [new ShowPageAction()]\n"
     84       "}\n"
     85       "\n"
     86       "var testEvent = chrome.declarativeContent.onPageChanged;\n"
     87       "\n"
     88       "testEvent.removeRules(undefined, function() {\n"
     89       "  testEvent.addRules([rule0], function() {\n"
     90       "    chrome.test.sendMessage(\"ready\", function(reply) {\n"
     91       "    })\n"
     92       "  });\n"
     93       "});\n");
     94   ExtensionTestMessageListener ready("ready", true);
     95   const Extension* extension = LoadExtension(ext_dir_.unpacked_path());
     96   ASSERT_TRUE(extension);
     97   const ExtensionAction* page_action =
     98       ExtensionActionManager::Get(browser()->profile())->
     99       GetPageAction(*extension);
    100   ASSERT_TRUE(page_action);
    101 
    102   ASSERT_TRUE(ready.WaitUntilSatisfied());
    103   content::WebContents* const tab =
    104       browser()->tab_strip_model()->GetWebContentsAt(0);
    105   const int tab_id = ExtensionTabUtil::GetTabId(tab);
    106 
    107   NavigateInRenderer(tab, GURL("http://test1/"));
    108 
    109   // The declarative API should show the page action instantly, rather
    110   // than waiting for the extension to run.
    111   EXPECT_TRUE(page_action->GetIsVisible(tab_id));
    112 
    113   // Make sure leaving a matching page unshows the page action.
    114   NavigateInRenderer(tab, GURL("http://not_checked/"));
    115   EXPECT_FALSE(page_action->GetIsVisible(tab_id));
    116 
    117   // Insert a password field to make sure that's noticed.
    118   // Notice that we touch offsetTop to force a synchronous layout.
    119   ASSERT_TRUE(content::ExecuteScript(
    120       tab, "document.body.innerHTML = '<input type=\"password\">';"
    121            "document.body.offsetTop;"));
    122 
    123   // Give the style match a chance to run and send back the matching-selector
    124   // update.  This takes one time through the Blink message loop to apply the
    125   // style to the new element, and a second to dedupe updates.
    126   // FIXME: Remove this after https://codereview.chromium.org/145663012/
    127   ASSERT_TRUE(content::ExecuteScript(tab, std::string()));
    128   ASSERT_TRUE(content::ExecuteScript(tab, std::string()));
    129 
    130   EXPECT_TRUE(page_action->GetIsVisible(tab_id))
    131       << "Adding a matching element should show the page action.";
    132 
    133   // Remove it again to make sure that reverts the action.
    134   // Notice that we touch offsetTop to force a synchronous layout.
    135   ASSERT_TRUE(content::ExecuteScript(
    136       tab, "document.body.innerHTML = 'Hello world';"
    137            "document.body.offsetTop;"));
    138 
    139   // Give the style match a chance to run and send back the matching-selector
    140   // update.  This takes one time through the Blink message loop to apply the
    141   // style to the new element, and a second to dedupe updates.
    142   // FIXME: Remove this after https://codereview.chromium.org/145663012/
    143   ASSERT_TRUE(content::ExecuteScript(tab, std::string()));
    144   ASSERT_TRUE(content::ExecuteScript(tab, std::string()));
    145 
    146   EXPECT_FALSE(page_action->GetIsVisible(tab_id))
    147       << "Removing the matching element should hide the page action again.";
    148 }
    149 
    150 // http://crbug.com/304373
    151 IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest,
    152                        UninstallWhileActivePageAction) {
    153   ext_dir_.WriteManifest(kDeclarativeContentManifest);
    154   ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
    155   const Extension* extension = LoadExtension(ext_dir_.unpacked_path());
    156   ASSERT_TRUE(extension);
    157   const std::string extension_id = extension->id();
    158   const ExtensionAction* page_action = ExtensionActionManager::Get(
    159       browser()->profile())->GetPageAction(*extension);
    160   ASSERT_TRUE(page_action);
    161 
    162   const std::string kTestRule =
    163       "setRules([{\n"
    164       "  conditions: [new PageStateMatcher({\n"
    165       "                   pageUrl: {hostPrefix: \"test\"}})],\n"
    166       "  actions: [new ShowPageAction()]\n"
    167       "}], 'test_rule');\n";
    168   EXPECT_EQ("test_rule",
    169             ExecuteScriptInBackgroundPage(extension_id, kTestRule));
    170 
    171   content::WebContents* const tab =
    172       browser()->tab_strip_model()->GetWebContentsAt(0);
    173   const int tab_id = ExtensionTabUtil::GetTabId(tab);
    174 
    175   NavigateInRenderer(tab, GURL("http://test/"));
    176 
    177   EXPECT_TRUE(page_action->GetIsVisible(tab_id));
    178   EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(1));
    179   LocationBarTesting* location_bar =
    180       browser()->window()->GetLocationBar()->GetLocationBarForTesting();
    181   EXPECT_EQ(1, location_bar->PageActionCount());
    182   EXPECT_EQ(1, location_bar->PageActionVisibleCount());
    183 
    184   ReloadExtension(extension_id);  // Invalidates page_action and extension.
    185   EXPECT_EQ("test_rule",
    186             ExecuteScriptInBackgroundPage(extension_id, kTestRule));
    187   // TODO(jyasskin): Apply new rules to existing tabs, without waiting for a
    188   // navigation.
    189   NavigateInRenderer(tab, GURL("http://test/"));
    190   EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(1));
    191   EXPECT_EQ(1, location_bar->PageActionCount());
    192   EXPECT_EQ(1, location_bar->PageActionVisibleCount());
    193 
    194   UnloadExtension(extension_id);
    195   NavigateInRenderer(tab, GURL("http://test/"));
    196   EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(0));
    197   EXPECT_EQ(0, location_bar->PageActionCount());
    198   EXPECT_EQ(0, location_bar->PageActionVisibleCount());
    199 }
    200 
    201 // This tests against a renderer crash that was present during development.
    202 IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest,
    203                        DISABLED_AddExtensionMatchingExistingTabWithDeadFrames) {
    204   ext_dir_.WriteManifest(kDeclarativeContentManifest);
    205   ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
    206   content::WebContents* const tab =
    207       browser()->tab_strip_model()->GetWebContentsAt(0);
    208   const int tab_id = ExtensionTabUtil::GetTabId(tab);
    209 
    210   ASSERT_TRUE(content::ExecuteScript(
    211       tab, "document.body.innerHTML = '<iframe src=\"http://test2\">';"));
    212   // Replace the iframe to destroy its WebFrame.
    213   ASSERT_TRUE(content::ExecuteScript(
    214       tab, "document.body.innerHTML = '<span class=\"foo\">';"));
    215 
    216   const Extension* extension = LoadExtension(ext_dir_.unpacked_path());
    217   ASSERT_TRUE(extension);
    218   const ExtensionAction* page_action = ExtensionActionManager::Get(
    219       browser()->profile())->GetPageAction(*extension);
    220   ASSERT_TRUE(page_action);
    221   EXPECT_FALSE(page_action->GetIsVisible(tab_id));
    222 
    223   EXPECT_EQ("rule0",
    224             ExecuteScriptInBackgroundPage(
    225                 extension->id(),
    226                 "setRules([{\n"
    227                 "  conditions: [new PageStateMatcher({\n"
    228                 "                   css: [\"span[class=foo]\"]})],\n"
    229                 "  actions: [new ShowPageAction()]\n"
    230                 "}], 'rule0');\n"));
    231   // Give the renderer a chance to apply the rules change and notify the
    232   // browser.  This takes one time through the Blink message loop to receive
    233   // the rule change and apply the new stylesheet, and a second to dedupe the
    234   // update.
    235   ASSERT_TRUE(content::ExecuteScript(tab, std::string()));
    236   ASSERT_TRUE(content::ExecuteScript(tab, std::string()));
    237 
    238   EXPECT_FALSE(tab->IsCrashed());
    239   EXPECT_TRUE(page_action->GetIsVisible(tab_id))
    240       << "Loading an extension when an open page matches its rules "
    241       << "should show the page action.";
    242 
    243   EXPECT_EQ("removed",
    244             ExecuteScriptInBackgroundPage(
    245                 extension->id(),
    246                 "onPageChanged.removeRules(undefined, function() {\n"
    247                 "  window.domAutomationController.send('removed');\n"
    248                 "});\n"));
    249   EXPECT_FALSE(page_action->GetIsVisible(tab_id));
    250 }
    251 
    252 IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest,
    253                        ShowPageActionWithoutPageAction) {
    254   std::string manifest_without_page_action = kDeclarativeContentManifest;
    255   ReplaceSubstringsAfterOffset(
    256       &manifest_without_page_action, 0, "\"page_action\": {},", "");
    257   ext_dir_.WriteManifest(manifest_without_page_action);
    258   ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
    259   const Extension* extension = LoadExtension(ext_dir_.unpacked_path());
    260   ASSERT_TRUE(extension);
    261 
    262   EXPECT_THAT(ExecuteScriptInBackgroundPage(
    263                   extension->id(),
    264                   "setRules([{\n"
    265                   "  conditions: [new PageStateMatcher({\n"
    266                   "                   pageUrl: {hostPrefix: \"test\"}})],\n"
    267                   "  actions: [new ShowPageAction()]\n"
    268                   "}], 'test_rule');\n"),
    269               testing::HasSubstr("without a page action"));
    270 
    271   content::WebContents* const tab =
    272       browser()->tab_strip_model()->GetWebContentsAt(0);
    273   NavigateInRenderer(tab, GURL("http://test/"));
    274 
    275   EXPECT_EQ(NULL,
    276             ExtensionActionManager::Get(browser()->profile())->
    277                 GetPageAction(*extension));
    278   EXPECT_EQ(0,
    279             browser()->window()->GetLocationBar()->GetLocationBarForTesting()->
    280             PageActionCount());
    281 }
    282 
    283 IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest,
    284                        CanonicalizesPageStateMatcherCss) {
    285   ext_dir_.WriteManifest(kDeclarativeContentManifest);
    286   ext_dir_.WriteFile(
    287       FILE_PATH_LITERAL("background.js"),
    288       "var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;\n"
    289       "function Return(obj) {\n"
    290       "  window.domAutomationController.send('' + obj);\n"
    291       "}\n");
    292   const Extension* extension = LoadExtension(ext_dir_.unpacked_path());
    293   ASSERT_TRUE(extension);
    294 
    295   EXPECT_EQ("input[type=\"password\"]",
    296             ExecuteScriptInBackgroundPage(
    297                 extension->id(),
    298                 "var psm = new PageStateMatcher(\n"
    299                 "    {css: [\"input[type='password']\"]});\n"
    300                 "Return(psm.css);"));
    301 
    302   EXPECT_THAT(ExecuteScriptInBackgroundPage(
    303                   extension->id(),
    304                   "try {\n"
    305                   "  new PageStateMatcher({css: 'Not-an-array'});\n"
    306                   "  Return('Failed to throw');\n"
    307                   "} catch (e) {\n"
    308                   "  Return(e.message);\n"
    309                   "}\n"),
    310               testing::ContainsRegex("css.*Expected 'array'"));
    311   EXPECT_THAT(ExecuteScriptInBackgroundPage(
    312                   extension->id(),
    313                   "try {\n"
    314                   "  new PageStateMatcher({css: [null]});\n"  // Not a string.
    315                   "  Return('Failed to throw');\n"
    316                   "} catch (e) {\n"
    317                   "  Return(e.message);\n"
    318                   "}\n"),
    319               testing::ContainsRegex("css\\.0.*Expected 'string'"));
    320   EXPECT_THAT(ExecuteScriptInBackgroundPage(
    321                   extension->id(),
    322                   "try {\n"
    323                   // Invalid CSS:
    324                   "  new PageStateMatcher({css: [\"input''\"]});\n"
    325                   "  Return('Failed to throw');\n"
    326                   "} catch (e) {\n"
    327                   "  Return(e.message);\n"
    328                   "}\n"),
    329               testing::ContainsRegex("valid.*: input''$"));
    330   EXPECT_THAT(ExecuteScriptInBackgroundPage(
    331                   extension->id(),
    332                   "try {\n"
    333                   // "Complex" selector:
    334                   "  new PageStateMatcher({css: ['div input']});\n"
    335                   "  Return('Failed to throw');\n"
    336                   "} catch (e) {\n"
    337                   "  Return(e.message);\n"
    338                   "}\n"),
    339               testing::ContainsRegex("compound selector.*: div input$"));
    340 }
    341 
    342 }  // namespace
    343 }  // namespace extensions
    344