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