extension_context_menu_browsertest.cc revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
1// Copyright (c) 2010 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 "app/menus/menu_model.h"
6#include "chrome/app/chrome_dll_resource.h"
7#include "chrome/browser/browser.h"
8#include "chrome/browser/extensions/extension_browsertest.h"
9#include "chrome/browser/extensions/extension_test_message_listener.h"
10#include "chrome/browser/extensions/extensions_service.h"
11#include "chrome/browser/profile.h"
12#include "chrome/browser/tab_contents/render_view_context_menu.h"
13#include "chrome/browser/tab_contents/tab_contents.h"
14#include "chrome/common/chrome_switches.h"
15#include "chrome/test/ui_test_utils.h"
16#include "net/base/mock_host_resolver.h"
17#include "third_party/WebKit/WebKit/chromium/public/WebContextMenuData.h"
18#include "webkit/glue/context_menu.h"
19
20using menus::MenuModel;
21using WebKit::WebContextMenuData;
22
23// This test class helps us sidestep platform-specific issues with popping up a
24// real context menu, while still running through the actual code in
25// RenderViewContextMenu where extension items get added and executed.
26class TestRenderViewContextMenu : public RenderViewContextMenu {
27 public:
28  TestRenderViewContextMenu(TabContents* tab_contents,
29                            const ContextMenuParams& params)
30      : RenderViewContextMenu(tab_contents, params) {}
31
32  virtual ~TestRenderViewContextMenu() {}
33
34  bool HasExtensionItemWithLabel(const std::string& label) {
35    string16 label16 = UTF8ToUTF16(label);
36    std::map<int, ExtensionMenuItem::Id>::iterator i;
37    for (i = extension_item_map_.begin(); i != extension_item_map_.end(); ++i) {
38      const ExtensionMenuItem::Id& id = i->second;
39      string16 tmp_label;
40      EXPECT_TRUE(GetItemLabel(id, &tmp_label));
41      if (tmp_label == label16)
42        return true;
43    }
44    return false;
45  }
46
47  // Looks in the menu for an extension item with |id|, and if it is found and
48  // has a label, that is put in |result| and we return true. Otherwise returns
49  // false.
50  bool GetItemLabel(const ExtensionMenuItem::Id& id, string16* result) {
51    int command_id = 0;
52    if (!FindCommandId(id, &command_id))
53      return false;
54
55    MenuModel* model = NULL;
56    int index = -1;
57    if (!GetMenuModelAndItemIndex(command_id, &model, &index)) {
58      return false;
59    }
60    *result = model->GetLabelAt(index);
61    return true;
62  }
63
64  // Searches for an menu item with |command_id|. If it's found, the return
65  // value is true and the model and index where it appears in that model are
66  // returned in |found_model| and |found_index|. Otherwise returns false.
67  bool GetMenuModelAndItemIndex(int command_id,
68                                MenuModel** found_model,
69                                int *found_index) {
70    std::vector<MenuModel*> models_to_search;
71    models_to_search.push_back(&menu_model_);
72
73    while (!models_to_search.empty()) {
74      MenuModel* model = models_to_search.back();
75      models_to_search.pop_back();
76      for (int i = 0; i < model->GetItemCount(); i++) {
77        if (model->GetCommandIdAt(i) == command_id) {
78          *found_model = model;
79          *found_index = i;
80          return true;
81        } else if (model->GetTypeAt(i) == MenuModel::TYPE_SUBMENU) {
82          models_to_search.push_back(model->GetSubmenuModelAt(i));
83        }
84      }
85    }
86
87    return false;
88  }
89
90 protected:
91  // These two functions implement pure virtual methods of
92  // RenderViewContextMenu.
93  virtual bool GetAcceleratorForCommandId(int command_id,
94                                          menus::Accelerator* accelerator) {
95    // None of our commands have accelerators, so always return false.
96    return false;
97  }
98  virtual void PlatformInit() {}
99
100
101  // Given an extension menu item id, tries to find the corresponding command id
102  // in the menu.
103  bool FindCommandId(const ExtensionMenuItem::Id& id, int* command_id) {
104    std::map<int, ExtensionMenuItem::Id>::const_iterator i;
105    for (i = extension_item_map_.begin(); i != extension_item_map_.end(); ++i) {
106      if (i->second == id) {
107        *command_id = i->first;
108        return true;
109      }
110    }
111    return false;
112  }
113};
114
115class ExtensionContextMenuBrowserTest : public ExtensionBrowserTest {
116 public:
117  // Helper to load an extension from context_menus/|subdirectory| in the
118  // extensions test data dir.
119  bool LoadContextMenuExtension(std::string subdirectory) {
120    FilePath extension_dir =
121        test_data_dir_.AppendASCII("context_menus").AppendASCII(subdirectory);
122    return LoadExtension(extension_dir);
123  }
124
125  TestRenderViewContextMenu* CreateMenu(const GURL& page_url,
126                                        const GURL& link_url) {
127    TabContents* tab_contents = browser()->GetSelectedTabContents();
128    WebContextMenuData data;
129    ContextMenuParams params(data);
130    params.page_url = page_url;
131    params.link_url = link_url;
132    TestRenderViewContextMenu* menu =
133        new TestRenderViewContextMenu(tab_contents, params);
134    menu->Init();
135    return menu;
136  }
137
138  // Shortcut to return the current ExtensionMenuManager.
139  ExtensionMenuManager* menu_manager() {
140    return browser()->profile()->GetExtensionsService()->menu_manager();
141  }
142
143  // Returns a pointer to the currently loaded extension with |name|, or null
144  // if not found.
145  Extension* GetExtensionNamed(std::string name) {
146    const ExtensionList* extensions =
147        browser()->profile()->GetExtensionsService()->extensions();
148    ExtensionList::const_iterator i;
149    for (i = extensions->begin(); i != extensions->end(); ++i) {
150      if ((*i)->name() == name) {
151        return *i;
152      }
153    }
154    return NULL;
155  }
156
157  // This gets all the items that any extension has registered for possible
158  // inclusion in context menus.
159  ExtensionMenuItem::List GetItems() {
160    ExtensionMenuItem::List result;
161    std::set<std::string> extension_ids = menu_manager()->ExtensionIds();
162    std::set<std::string>::iterator i;
163    for (i = extension_ids.begin(); i != extension_ids.end(); ++i) {
164      const ExtensionMenuItem::List* list = menu_manager()->MenuItems(*i);
165      result.insert(result.end(), list->begin(), list->end());
166    }
167    return result;
168  }
169
170  // This creates a test menu for a page with |page_url| and |link_url|, looks
171  // for an extension item with the given |label|, and returns true if the item
172  // was found.
173  bool MenuHasItemWithLabel(const GURL& page_url,
174                            const GURL& link_url,
175                            const std::string& label) {
176    scoped_ptr<TestRenderViewContextMenu> menu(CreateMenu(page_url, link_url));
177    return menu->HasExtensionItemWithLabel(label);
178  }
179};
180
181// Tests adding a simple context menu item.
182IN_PROC_BROWSER_TEST_F(ExtensionContextMenuBrowserTest, Simple) {
183  ExtensionTestMessageListener listener1("created item");
184  ExtensionTestMessageListener listener2("onclick fired");
185  ASSERT_TRUE(LoadContextMenuExtension("simple"));
186
187  // Wait for the extension to tell us it's created an item.
188  ASSERT_TRUE(listener1.WaitUntilSatisfied());
189
190  GURL page_url("http://www.google.com");
191
192  // Create and build our test context menu.
193  scoped_ptr<TestRenderViewContextMenu> menu(CreateMenu(page_url, GURL()));
194
195  // Look for the extension item in the menu, and execute it.
196  int command_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
197  ASSERT_TRUE(menu->IsCommandIdEnabled(command_id));
198  menu->ExecuteCommand(command_id);
199
200  // Wait for the extension's script to tell us its onclick fired.
201  ASSERT_TRUE(listener2.WaitUntilSatisfied());
202}
203
204// Tests that setting "documentUrlPatterns" for an item properly restricts
205// those items to matching pages.
206IN_PROC_BROWSER_TEST_F(ExtensionContextMenuBrowserTest, Patterns) {
207  ExtensionTestMessageListener listener("created items");
208
209  ASSERT_TRUE(LoadContextMenuExtension("patterns"));
210
211  // Wait for the js test code to create its two items with patterns.
212  ASSERT_TRUE(listener.WaitUntilSatisfied());
213
214  // Check that a document url that should match the items' patterns appears.
215  GURL google_url("http://www.google.com");
216  ASSERT_TRUE(MenuHasItemWithLabel(google_url,
217                                   GURL(),
218                                   std::string("test_item1")));
219  ASSERT_TRUE(MenuHasItemWithLabel(google_url,
220                                   GURL(),
221                                   std::string("test_item2")));
222
223  // Now check with a non-matching url.
224  GURL test_url("http://www.test.com");
225  ASSERT_FALSE(MenuHasItemWithLabel(test_url,
226                                    GURL(),
227                                    std::string("test_item1")));
228  ASSERT_FALSE(MenuHasItemWithLabel(test_url,
229                                    GURL(),
230                                    std::string("test_item2")));
231}
232
233// Tests registering an item with a very long title that should get truncated in
234// the actual menu displayed.
235IN_PROC_BROWSER_TEST_F(ExtensionContextMenuBrowserTest, LongTitle) {
236  ExtensionTestMessageListener listener("created");
237
238  // Load the extension and wait until it's created a menu item.
239  ASSERT_TRUE(LoadContextMenuExtension("long_title"));
240  ASSERT_TRUE(listener.WaitUntilSatisfied());
241
242  // Make sure we have an item registered with a long title.
243  size_t limit = RenderViewContextMenu::kMaxExtensionItemTitleLength;
244  ExtensionMenuItem::List items = GetItems();
245  ASSERT_EQ(1u, items.size());
246  ExtensionMenuItem* item = items.at(0);
247  ASSERT_GT(item->title().size(), limit);
248
249  // Create a context menu, then find the item's label. It should be properly
250  // truncated.
251  GURL url("http://foo.com/");
252  scoped_ptr<TestRenderViewContextMenu> menu(CreateMenu(url, GURL()));
253
254  string16 label;
255  ASSERT_TRUE(menu->GetItemLabel(item->id(), &label));
256  ASSERT_TRUE(label.size() <= limit);
257}
258
259// Checks that in |menu|, the item at |index| has type |expected_type| and a
260// label of |expected_label|.
261static void ExpectLabelAndType(const char* expected_label,
262                               MenuModel::ItemType expected_type,
263                               const MenuModel& menu,
264                               int index) {
265  EXPECT_EQ(expected_type, menu.GetTypeAt(index));
266  EXPECT_EQ(UTF8ToUTF16(expected_label), menu.GetLabelAt(index));
267}
268
269// In the separators test we build a submenu with items and separators in two
270// different ways - this is used to verify the results in both cases.
271static void VerifyMenuForSeparatorsTest(const MenuModel& menu) {
272  // We expect to see the following items in the menu:
273  //  radio1
274  //  radio2
275  //  --separator-- (automatically added)
276  //  normal1
277  //  --separator--
278  //  normal2
279  //  --separator--
280  //  radio3
281  //  radio4
282  //  --separator--
283  //  normal3
284
285  int index = 0;
286  ASSERT_EQ(11, menu.GetItemCount());
287  ExpectLabelAndType("radio1", MenuModel::TYPE_RADIO, menu, index++);
288  ExpectLabelAndType("radio2", MenuModel::TYPE_RADIO, menu, index++);
289  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
290  ExpectLabelAndType("normal1", MenuModel::TYPE_COMMAND, menu, index++);
291  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
292  ExpectLabelAndType("normal2", MenuModel::TYPE_COMMAND, menu, index++);
293  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
294  ExpectLabelAndType("radio3", MenuModel::TYPE_RADIO, menu, index++);
295  ExpectLabelAndType("radio4", MenuModel::TYPE_RADIO, menu, index++);
296  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
297  ExpectLabelAndType("normal3", MenuModel::TYPE_COMMAND, menu, index++);
298}
299
300// Tests a number of cases for auto-generated and explicitly added separators.
301IN_PROC_BROWSER_TEST_F(ExtensionContextMenuBrowserTest, Separators) {
302  // Load the extension.
303  ASSERT_TRUE(LoadContextMenuExtension("separators"));
304  Extension* extension = GetExtensionNamed("Separators Test");
305  ASSERT_TRUE(extension != NULL);
306
307  // Navigate to test1.html inside the extension, which should create a bunch
308  // of items at the top-level (but they'll get pushed into an auto-generated
309  // parent).
310  ExtensionTestMessageListener listener1("test1 create finished");
311  ui_test_utils::NavigateToURL(browser(),
312                               GURL(extension->GetResourceURL("test1.html")));
313  listener1.WaitUntilSatisfied();
314
315  GURL url("http://www.google.com/");
316  scoped_ptr<TestRenderViewContextMenu> menu(CreateMenu(url, GURL()));
317
318  // The top-level item should be an "automagic parent" with the extension's
319  // name.
320  MenuModel* model = NULL;
321  int index = 0;
322  string16 label;
323  ASSERT_TRUE(menu->GetMenuModelAndItemIndex(
324      IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST, &model, &index));
325  EXPECT_EQ(UTF8ToUTF16(extension->name()), model->GetLabelAt(index));
326  ASSERT_EQ(MenuModel::TYPE_SUBMENU, model->GetTypeAt(index));
327
328  // Get the submenu and verify the items there.
329  MenuModel* submenu = model->GetSubmenuModelAt(index);
330  ASSERT_TRUE(submenu != NULL);
331  VerifyMenuForSeparatorsTest(*submenu);
332
333  // Now run our second test - navigate to test2.html which creates an explicit
334  // parent node and populates that with the same items as in test1.
335  ExtensionTestMessageListener listener2("test2 create finished");
336  ui_test_utils::NavigateToURL(browser(),
337                               GURL(extension->GetResourceURL("test2.html")));
338  listener2.WaitUntilSatisfied();
339  menu.reset(CreateMenu(url, GURL()));
340  ASSERT_TRUE(menu->GetMenuModelAndItemIndex(
341      IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST, &model, &index));
342  EXPECT_EQ(UTF8ToUTF16("parent"), model->GetLabelAt(index));
343  submenu = model->GetSubmenuModelAt(index);
344  ASSERT_TRUE(submenu != NULL);
345  VerifyMenuForSeparatorsTest(*submenu);
346}
347
348// Tests that targetUrlPattern keeps items from appearing when there is no
349// target url.
350IN_PROC_BROWSER_TEST_F(ExtensionContextMenuBrowserTest, TargetURLs) {
351  ExtensionTestMessageListener listener("created items");
352  ASSERT_TRUE(LoadContextMenuExtension("target_urls"));
353  ASSERT_TRUE(listener.WaitUntilSatisfied());
354
355  GURL google_url("http://www.google.com");
356  GURL non_google_url("http://www.foo.com");
357
358  // No target url - the item should not appear.
359  ASSERT_FALSE(MenuHasItemWithLabel(google_url, GURL(), std::string("item1")));
360
361  // A matching target url - the item should appear.
362  ASSERT_TRUE(MenuHasItemWithLabel(google_url,
363                                   google_url,
364                                   std::string("item1")));
365
366  // A non-matching target url - the item should not appear.
367  ASSERT_FALSE(MenuHasItemWithLabel(google_url,
368                                    non_google_url,
369                                    std::string("item1")));
370}
371