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
18namespace extensions {
19namespace {
20
21const 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
37const 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
56class 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
69IN_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
151IN_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.
202IN_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
252IN_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
283IN_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