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