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