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.
5 #include "chrome/browser/extensions/extension_context_menu_model.h"
7 #include "base/strings/utf_string_conversions.h"
8 #include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
9 #include "chrome/browser/extensions/extension_service.h"
10 #include "chrome/browser/extensions/extension_service_test_base.h"
11 #include "chrome/browser/extensions/menu_manager.h"
12 #include "chrome/browser/extensions/menu_manager_factory.h"
13 #include "chrome/browser/ui/browser.h"
14 #include "chrome/browser/ui/host_desktop.h"
15 #include "chrome/common/extensions/api/context_menus.h"
16 #include "chrome/grit/chromium_strings.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "chrome/test/base/test_browser_window.h"
19 #include "chrome/test/base/testing_profile.h"
20 #include "components/crx_file/id_util.h"
21 #include "extensions/browser/extension_system.h"
22 #include "extensions/browser/test_management_policy.h"
23 #include "extensions/common/extension_builder.h"
24 #include "extensions/common/feature_switch.h"
25 #include "extensions/common/manifest.h"
26 #include "extensions/common/manifest_constants.h"
27 #include "extensions/common/manifest_handlers/options_page_info.h"
28 #include "extensions/common/value_builder.h"
29 #include "testing/gtest/include/gtest/gtest.h"
30 #include "ui/base/l10n/l10n_util.h"
31 #include "ui/gfx/image/image.h"
33 namespace extensions
{
37 // Label for test extension menu item.
38 const char* kTestExtensionItemLabel
= "test-ext-item";
40 // Build an extension to pass to the menu constructor, with the an action
41 // specified by |action_key|.
42 scoped_refptr
<const Extension
> BuildExtension(const std::string
& name
,
43 const char* action_key
,
44 Manifest::Location location
) {
45 return ExtensionBuilder()
46 .SetManifest(DictionaryBuilder()
49 .Set("manifest_version", 2)
50 .Set(action_key
, DictionaryBuilder().Pass()))
51 .SetID(crx_file::id_util::GenerateId(name
))
52 .SetLocation(location
)
56 // Create a Browser for the ExtensionContextMenuModel to use.
57 scoped_ptr
<Browser
> CreateBrowser(Profile
* profile
) {
58 Browser::CreateParams
params(profile
, chrome::GetActiveDesktop());
59 TestBrowserWindow test_window
;
60 params
.window
= &test_window
;
61 return scoped_ptr
<Browser
>(new Browser(params
));
64 // Returns the index of the given |command_id| in the given |menu|, or -1 if it
66 int GetCommandIndex(const scoped_refptr
<ExtensionContextMenuModel
> menu
,
68 int item_count
= menu
->GetItemCount();
69 for (int i
= 0; i
< item_count
; ++i
) {
70 if (menu
->GetCommandIdAt(i
) == command_id
)
78 class ExtensionContextMenuModelTest
: public ExtensionServiceTestBase
{
80 ExtensionContextMenuModelTest();
82 // Creates an extension menu item for |extension| with the given |context|
83 // and adds it to |manager|. Refreshes |model| to show new item.
84 void AddContextItemAndRefreshModel(MenuManager
* manager
,
85 const Extension
* extension
,
86 MenuItem::Context context
,
87 ExtensionContextMenuModel
* model
);
89 // Reinitializes the given |model|.
90 void RefreshMenu(ExtensionContextMenuModel
* model
);
92 // Returns the number of extension menu items that show up in |model|.
93 // For this test, all the extension items have samel label
94 // |kTestExtensionItemLabel|.
95 int CountExtensionItems(ExtensionContextMenuModel
* model
);
101 ExtensionContextMenuModelTest::ExtensionContextMenuModelTest() : cur_id_(0) {
104 void ExtensionContextMenuModelTest::AddContextItemAndRefreshModel(
105 MenuManager
* manager
,
106 const Extension
* extension
,
107 MenuItem::Context context
,
108 ExtensionContextMenuModel
* model
) {
109 MenuItem::Type type
= MenuItem::NORMAL
;
110 MenuItem::ContextList
contexts(context
);
111 const MenuItem::ExtensionKey
key(extension
->id());
112 MenuItem::Id
id(false, key
);
114 manager
->AddContextItem(extension
, new MenuItem(id
, kTestExtensionItemLabel
,
121 void ExtensionContextMenuModelTest::RefreshMenu(
122 ExtensionContextMenuModel
* model
) {
124 model
->InitMenu(model
->GetExtension(), ExtensionContextMenuModel::VISIBLE
);
127 int ExtensionContextMenuModelTest::CountExtensionItems(
128 ExtensionContextMenuModel
* model
) {
129 base::string16 expected_label
= base::ASCIIToUTF16(kTestExtensionItemLabel
);
130 int num_items_found
= 0;
131 for (int i
= 0; i
< model
->GetItemCount(); ++i
) {
132 if (expected_label
== model
->GetLabelAt(i
))
135 EXPECT_EQ(num_items_found
, model
->extension_items_count_
);
136 return num_items_found
;
139 // Tests that applicable menu items are disabled when a ManagementPolicy
141 TEST_F(ExtensionContextMenuModelTest
, RequiredInstallationsDisablesItems
) {
142 InitializeEmptyExtensionService();
144 // Test that management policy can determine whether or not policy-installed
145 // extensions can be installed/uninstalled.
146 scoped_refptr
<const Extension
> extension
=
147 BuildExtension("extension",
148 manifest_keys::kPageAction
,
149 Manifest::EXTERNAL_POLICY
);
150 ASSERT_TRUE(extension
.get());
151 service()->AddExtension(extension
.get());
153 scoped_ptr
<Browser
> browser
= CreateBrowser(profile());
154 scoped_refptr
<ExtensionContextMenuModel
> menu(
155 new ExtensionContextMenuModel(extension
.get(), browser
.get()));
157 ExtensionSystem
* system
= ExtensionSystem::Get(profile());
158 system
->management_policy()->UnregisterAllProviders();
160 // Uninstallation should be, by default, enabled.
161 EXPECT_TRUE(menu
->IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL
));
163 TestManagementPolicyProvider
policy_provider(
164 TestManagementPolicyProvider::PROHIBIT_MODIFY_STATUS
);
165 system
->management_policy()->RegisterProvider(&policy_provider
);
167 // If there's a policy provider that requires the extension stay enabled, then
168 // uninstallation should be disabled.
169 EXPECT_FALSE(menu
->IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL
));
170 int uninstall_index
=
171 menu
->GetIndexOfCommandId(ExtensionContextMenuModel::UNINSTALL
);
172 // There should also be an icon to visually indicate why uninstallation is
175 EXPECT_TRUE(menu
->GetIconAt(uninstall_index
, &icon
));
176 EXPECT_FALSE(icon
.IsEmpty());
178 // Don't leave |policy_provider| dangling.
179 system
->management_policy()->UnregisterProvider(&policy_provider
);
182 // Tests the context menu for a component extension.
183 TEST_F(ExtensionContextMenuModelTest
, ComponentExtensionContextMenu
) {
184 InitializeEmptyExtensionService();
186 std::string
name("component");
187 scoped_ptr
<base::DictionaryValue
> manifest
=
188 DictionaryBuilder().Set("name", name
)
190 .Set("manifest_version", 2)
191 .Set("browser_action", DictionaryBuilder().Pass())
194 scoped_refptr
<const Extension
> extension
=
195 ExtensionBuilder().SetManifest(make_scoped_ptr(manifest
->DeepCopy()))
196 .SetID(crx_file::id_util::GenerateId("component"))
197 .SetLocation(Manifest::COMPONENT
)
199 service()->AddExtension(extension
.get());
201 scoped_ptr
<Browser
> browser
= CreateBrowser(profile());
203 scoped_refptr
<ExtensionContextMenuModel
> menu(
204 new ExtensionContextMenuModel(extension
.get(), browser
.get()));
206 // A component extension's context menu should not include options for
207 // managing extensions or removing it, and should only include an option for
208 // the options page if the extension has one (which this one doesn't).
210 menu
->GetIndexOfCommandId(ExtensionContextMenuModel::CONFIGURE
));
212 menu
->GetIndexOfCommandId(ExtensionContextMenuModel::UNINSTALL
));
213 EXPECT_EQ(-1, menu
->GetIndexOfCommandId(ExtensionContextMenuModel::MANAGE
));
214 // The "name" option should be present, but not enabled for component
216 EXPECT_NE(-1, menu
->GetIndexOfCommandId(ExtensionContextMenuModel::NAME
));
217 EXPECT_FALSE(menu
->IsCommandIdEnabled(ExtensionContextMenuModel::NAME
));
219 // Check that a component extension with an options page does have the options
220 // menu item, and it is enabled.
221 manifest
->SetString("options_page", "options_page.html");
223 ExtensionBuilder().SetManifest(manifest
.Pass())
224 .SetID(crx_file::id_util::GenerateId("component_opts"))
225 .SetLocation(Manifest::COMPONENT
)
227 menu
= new ExtensionContextMenuModel(extension
.get(), browser
.get());
228 service()->AddExtension(extension
.get());
229 EXPECT_TRUE(extensions::OptionsPageInfo::HasOptionsPage(extension
.get()));
231 menu
->GetIndexOfCommandId(ExtensionContextMenuModel::CONFIGURE
));
232 EXPECT_TRUE(menu
->IsCommandIdEnabled(ExtensionContextMenuModel::CONFIGURE
));
235 TEST_F(ExtensionContextMenuModelTest
, ExtensionItemTest
) {
236 InitializeEmptyExtensionService();
237 scoped_refptr
<const Extension
> extension
=
238 BuildExtension("extension",
239 manifest_keys::kPageAction
,
241 ASSERT_TRUE(extension
.get());
242 service()->AddExtension(extension
.get());
244 scoped_ptr
<Browser
> browser
= CreateBrowser(profile());
246 // Create a MenuManager for adding context items.
247 MenuManager
* manager
= static_cast<MenuManager
*>(
248 (MenuManagerFactory::GetInstance()->SetTestingFactoryAndUse(
250 &MenuManagerFactory::BuildServiceInstanceForTesting
)));
251 ASSERT_TRUE(manager
);
253 scoped_refptr
<ExtensionContextMenuModel
> menu(
254 new ExtensionContextMenuModel(extension
.get(), browser
.get()));
256 // There should be no extension items yet.
257 EXPECT_EQ(0, CountExtensionItems(menu
.get()));
259 // Add a browser action menu item for |extension| to |manager|.
260 AddContextItemAndRefreshModel(
261 manager
, extension
.get(), MenuItem::BROWSER_ACTION
, menu
.get());
263 // Since |extension| has a page action, the browser action menu item should
265 EXPECT_EQ(0, CountExtensionItems(menu
.get()));
267 // Add a page action menu item and reset the context menu.
268 AddContextItemAndRefreshModel(
269 manager
, extension
.get(), MenuItem::PAGE_ACTION
, menu
.get());
271 // The page action item should be present because |extension| has a page
273 EXPECT_EQ(1, CountExtensionItems(menu
.get()));
275 // Create more page action items to test top level menu item limitations.
276 for (int i
= 0; i
< api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
; ++i
)
277 AddContextItemAndRefreshModel(
278 manager
, extension
.get(), MenuItem::PAGE_ACTION
, menu
.get());
280 // The menu should only have a limited number of extension items, since they
281 // are all top level items, and we limit the number of top level extension
283 EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
,
284 CountExtensionItems(menu
.get()));
286 AddContextItemAndRefreshModel(
287 manager
, extension
.get(), MenuItem::PAGE_ACTION
, menu
.get());
289 // Adding another top level item should not increase the count.
290 EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
,
291 CountExtensionItems(menu
.get()));
294 // Test that the "show" and "hide" menu items appear correctly in the extension
296 TEST_F(ExtensionContextMenuModelTest
, ExtensionContextMenuShowAndHide
) {
297 InitializeEmptyExtensionService();
298 scoped_refptr
<const Extension
> page_action
=
299 BuildExtension("page_action_extension",
300 manifest_keys::kPageAction
,
302 ASSERT_TRUE(page_action
.get());
303 scoped_refptr
<const Extension
> browser_action
=
304 BuildExtension("browser_action_extension",
305 manifest_keys::kBrowserAction
,
307 ASSERT_TRUE(browser_action
.get());
309 service()->AddExtension(page_action
.get());
310 service()->AddExtension(browser_action
.get());
312 scoped_ptr
<Browser
> browser
= CreateBrowser(profile());
314 scoped_refptr
<ExtensionContextMenuModel
> menu(
315 new ExtensionContextMenuModel(page_action
.get(),
317 ExtensionContextMenuModel::VISIBLE
,
321 const ExtensionContextMenuModel::MenuEntries visibility_command
=
322 ExtensionContextMenuModel::TOGGLE_VISIBILITY
;
323 base::string16 hide_string
=
324 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_BUTTON
);
325 base::string16 redesign_hide_string
=
326 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_BUTTON_IN_MENU
);
327 base::string16 redesign_show_string
=
328 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_BUTTON_IN_TOOLBAR
);
329 base::string16 redesign_keep_string
=
330 l10n_util::GetStringUTF16(IDS_EXTENSIONS_KEEP_BUTTON_IN_TOOLBAR
);
332 int index
= GetCommandIndex(menu
, visibility_command
);
333 // Without the toolbar redesign switch, page action menus shouldn't have a
334 // visibility option.
335 EXPECT_EQ(-1, index
);
337 menu
= new ExtensionContextMenuModel(browser_action
.get(),
339 ExtensionContextMenuModel::VISIBLE
,
341 index
= GetCommandIndex(menu
, visibility_command
);
342 // Browser actions should have the visibility option.
343 EXPECT_NE(-1, index
);
344 // Since the action is currently visible, it should have the option to hide
346 EXPECT_EQ(hide_string
, menu
->GetLabelAt(index
));
348 // Enabling the toolbar redesign switch should give page actions the button.
349 FeatureSwitch::ScopedOverride
enable_toolbar_redesign(
350 FeatureSwitch::extension_action_redesign(), true);
351 menu
= new ExtensionContextMenuModel(page_action
.get(),
353 ExtensionContextMenuModel::VISIBLE
,
355 index
= GetCommandIndex(menu
, visibility_command
);
356 EXPECT_NE(-1, index
);
357 EXPECT_EQ(redesign_hide_string
, menu
->GetLabelAt(index
));
359 // If the action is overflowed, it should have the "Show button in toolbar"
361 menu
= new ExtensionContextMenuModel(browser_action
.get(),
363 ExtensionContextMenuModel::OVERFLOWED
,
365 index
= GetCommandIndex(menu
, visibility_command
);
366 EXPECT_NE(-1, index
);
367 EXPECT_EQ(redesign_show_string
, menu
->GetLabelAt(index
));
369 // If the action is transitively visible, as happens when it is showing a
370 // popup, we should use a "Keep button in toolbar" string.
371 menu
= new ExtensionContextMenuModel(
372 browser_action
.get(),
374 ExtensionContextMenuModel::TRANSITIVELY_VISIBLE
,
376 index
= GetCommandIndex(menu
, visibility_command
);
377 EXPECT_NE(-1, index
);
378 EXPECT_EQ(redesign_keep_string
, menu
->GetLabelAt(index
));
381 } // namespace extensions