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_dialog_auto_confirm.h"
22 #include "extensions/browser/extension_registry.h"
23 #include "extensions/browser/extension_system.h"
24 #include "extensions/browser/test_extension_registry_observer.h"
25 #include "extensions/browser/test_management_policy.h"
26 #include "extensions/common/extension_builder.h"
27 #include "extensions/common/feature_switch.h"
28 #include "extensions/common/manifest.h"
29 #include "extensions/common/manifest_constants.h"
30 #include "extensions/common/manifest_handlers/options_page_info.h"
31 #include "extensions/common/value_builder.h"
32 #include "testing/gtest/include/gtest/gtest.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/gfx/image/image.h"
36 namespace extensions
{
40 // Label for test extension menu item.
41 const char* kTestExtensionItemLabel
= "test-ext-item";
43 // Build an extension to pass to the menu constructor, with the an action
44 // specified by |action_key|.
45 scoped_refptr
<const Extension
> BuildExtension(const std::string
& name
,
46 const char* action_key
,
47 Manifest::Location location
) {
48 return ExtensionBuilder()
49 .SetManifest(DictionaryBuilder()
52 .Set("manifest_version", 2)
53 .Set(action_key
, DictionaryBuilder().Pass()))
54 .SetID(crx_file::id_util::GenerateId(name
))
55 .SetLocation(location
)
59 // Returns the index of the given |command_id| in the given |menu|, or -1 if it
61 int GetCommandIndex(const ExtensionContextMenuModel
& menu
, int command_id
) {
62 int item_count
= menu
.GetItemCount();
63 for (int i
= 0; i
< item_count
; ++i
) {
64 if (menu
.GetCommandIdAt(i
) == command_id
)
72 class ExtensionContextMenuModelTest
: public ExtensionServiceTestBase
{
74 ExtensionContextMenuModelTest();
76 // Creates an extension menu item for |extension| with the given |context|
77 // and adds it to |manager|. Refreshes |model| to show new item.
78 void AddContextItemAndRefreshModel(MenuManager
* manager
,
79 const Extension
* extension
,
80 MenuItem::Context context
,
81 ExtensionContextMenuModel
* model
);
83 // Reinitializes the given |model|.
84 void RefreshMenu(ExtensionContextMenuModel
* model
);
86 // Returns the number of extension menu items that show up in |model|.
87 // For this test, all the extension items have samel label
88 // |kTestExtensionItemLabel|.
89 int CountExtensionItems(const ExtensionContextMenuModel
& model
);
91 Browser
* GetBrowser();
98 scoped_ptr
<TestBrowserWindow
> test_window_
;
99 scoped_ptr
<Browser
> browser_
;
101 DISALLOW_COPY_AND_ASSIGN(ExtensionContextMenuModelTest
);
104 ExtensionContextMenuModelTest::ExtensionContextMenuModelTest() : cur_id_(0) {
107 void ExtensionContextMenuModelTest::AddContextItemAndRefreshModel(
108 MenuManager
* manager
,
109 const Extension
* extension
,
110 MenuItem::Context context
,
111 ExtensionContextMenuModel
* model
) {
112 MenuItem::Type type
= MenuItem::NORMAL
;
113 MenuItem::ContextList
contexts(context
);
114 const MenuItem::ExtensionKey
key(extension
->id());
115 MenuItem::Id
id(false, key
);
117 manager
->AddContextItem(extension
, new MenuItem(id
, kTestExtensionItemLabel
,
124 void ExtensionContextMenuModelTest::RefreshMenu(
125 ExtensionContextMenuModel
* model
) {
127 model
->InitMenu(model
->GetExtension(), ExtensionContextMenuModel::VISIBLE
);
130 int ExtensionContextMenuModelTest::CountExtensionItems(
131 const ExtensionContextMenuModel
& model
) {
132 base::string16 expected_label
= base::ASCIIToUTF16(kTestExtensionItemLabel
);
133 int num_items_found
= 0;
134 for (int i
= 0; i
< model
.GetItemCount(); ++i
) {
135 if (expected_label
== model
.GetLabelAt(i
))
138 EXPECT_EQ(num_items_found
, model
.extension_items_count_
);
139 return num_items_found
;
142 Browser
* ExtensionContextMenuModelTest::GetBrowser() {
146 return browser_
.get();
149 void ExtensionContextMenuModelTest::CreateBrowser() {
150 Browser::CreateParams
params(profile(), chrome::GetActiveDesktop());
151 test_window_
.reset(new TestBrowserWindow());
152 params
.window
= test_window_
.get();
153 browser_
.reset(new Browser(params
));
156 // Tests that applicable menu items are disabled when a ManagementPolicy
158 TEST_F(ExtensionContextMenuModelTest
, RequiredInstallationsDisablesItems
) {
159 InitializeEmptyExtensionService();
161 // Test that management policy can determine whether or not policy-installed
162 // extensions can be installed/uninstalled.
163 scoped_refptr
<const Extension
> extension
=
164 BuildExtension("extension",
165 manifest_keys::kPageAction
,
166 Manifest::EXTERNAL_POLICY
);
167 ASSERT_TRUE(extension
.get());
168 service()->AddExtension(extension
.get());
170 ExtensionContextMenuModel
menu(extension
.get(), GetBrowser());
172 ExtensionSystem
* system
= ExtensionSystem::Get(profile());
173 system
->management_policy()->UnregisterAllProviders();
175 // Uninstallation should be, by default, enabled.
176 EXPECT_TRUE(menu
.IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL
));
178 TestManagementPolicyProvider
policy_provider(
179 TestManagementPolicyProvider::PROHIBIT_MODIFY_STATUS
);
180 system
->management_policy()->RegisterProvider(&policy_provider
);
182 // If there's a policy provider that requires the extension stay enabled, then
183 // uninstallation should be disabled.
184 EXPECT_FALSE(menu
.IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL
));
185 int uninstall_index
=
186 menu
.GetIndexOfCommandId(ExtensionContextMenuModel::UNINSTALL
);
187 // There should also be an icon to visually indicate why uninstallation is
190 EXPECT_TRUE(menu
.GetIconAt(uninstall_index
, &icon
));
191 EXPECT_FALSE(icon
.IsEmpty());
193 // Don't leave |policy_provider| dangling.
194 system
->management_policy()->UnregisterProvider(&policy_provider
);
197 // Tests the context menu for a component extension.
198 TEST_F(ExtensionContextMenuModelTest
, ComponentExtensionContextMenu
) {
199 InitializeEmptyExtensionService();
201 std::string
name("component");
202 scoped_ptr
<base::DictionaryValue
> manifest
=
203 DictionaryBuilder().Set("name", name
)
205 .Set("manifest_version", 2)
206 .Set("browser_action", DictionaryBuilder().Pass())
210 scoped_refptr
<const Extension
> extension
=
211 ExtensionBuilder().SetManifest(make_scoped_ptr(manifest
->DeepCopy()))
212 .SetID(crx_file::id_util::GenerateId("component"))
213 .SetLocation(Manifest::COMPONENT
)
215 service()->AddExtension(extension
.get());
217 ExtensionContextMenuModel
menu(extension
.get(), GetBrowser());
219 // A component extension's context menu should not include options for
220 // managing extensions or removing it, and should only include an option for
221 // the options page if the extension has one (which this one doesn't).
222 EXPECT_EQ(-1, menu
.GetIndexOfCommandId(ExtensionContextMenuModel::CONFIGURE
));
223 EXPECT_EQ(-1, menu
.GetIndexOfCommandId(ExtensionContextMenuModel::UNINSTALL
));
224 EXPECT_EQ(-1, menu
.GetIndexOfCommandId(ExtensionContextMenuModel::MANAGE
));
225 // The "name" option should be present, but not enabled for component
227 EXPECT_NE(-1, menu
.GetIndexOfCommandId(ExtensionContextMenuModel::NAME
));
228 EXPECT_FALSE(menu
.IsCommandIdEnabled(ExtensionContextMenuModel::NAME
));
232 // Check that a component extension with an options page does have the options
233 // menu item, and it is enabled.
234 manifest
->SetString("options_page", "options_page.html");
235 scoped_refptr
<const Extension
> extension
=
237 .SetManifest(manifest
.Pass())
238 .SetID(crx_file::id_util::GenerateId("component_opts"))
239 .SetLocation(Manifest::COMPONENT
)
241 ExtensionContextMenuModel
menu(extension
.get(), GetBrowser());
242 service()->AddExtension(extension
.get());
243 EXPECT_TRUE(extensions::OptionsPageInfo::HasOptionsPage(extension
.get()));
244 EXPECT_NE(-1, menu
.GetIndexOfCommandId(ExtensionContextMenuModel::CONFIGURE
));
245 EXPECT_TRUE(menu
.IsCommandIdEnabled(ExtensionContextMenuModel::CONFIGURE
));
249 TEST_F(ExtensionContextMenuModelTest
, ExtensionItemTest
) {
250 InitializeEmptyExtensionService();
251 scoped_refptr
<const Extension
> extension
=
252 BuildExtension("extension",
253 manifest_keys::kPageAction
,
255 ASSERT_TRUE(extension
.get());
256 service()->AddExtension(extension
.get());
258 // Create a MenuManager for adding context items.
259 MenuManager
* manager
= static_cast<MenuManager
*>(
260 (MenuManagerFactory::GetInstance()->SetTestingFactoryAndUse(
262 &MenuManagerFactory::BuildServiceInstanceForTesting
)));
263 ASSERT_TRUE(manager
);
265 ExtensionContextMenuModel
menu(extension
.get(), GetBrowser());
267 // There should be no extension items yet.
268 EXPECT_EQ(0, CountExtensionItems(menu
));
270 // Add a browser action menu item for |extension| to |manager|.
271 AddContextItemAndRefreshModel(manager
, extension
.get(),
272 MenuItem::BROWSER_ACTION
, &menu
);
274 // Since |extension| has a page action, the browser action menu item should
276 EXPECT_EQ(0, CountExtensionItems(menu
));
278 // Add a page action menu item and reset the context menu.
279 AddContextItemAndRefreshModel(manager
, extension
.get(), MenuItem::PAGE_ACTION
,
282 // The page action item should be present because |extension| has a page
284 EXPECT_EQ(1, CountExtensionItems(menu
));
286 // Create more page action items to test top level menu item limitations.
287 for (int i
= 0; i
< api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
; ++i
)
288 AddContextItemAndRefreshModel(manager
, extension
.get(),
289 MenuItem::PAGE_ACTION
, &menu
);
291 // The menu should only have a limited number of extension items, since they
292 // are all top level items, and we limit the number of top level extension
294 EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
,
295 CountExtensionItems(menu
));
297 AddContextItemAndRefreshModel(manager
, extension
.get(), MenuItem::PAGE_ACTION
,
300 // Adding another top level item should not increase the count.
301 EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
,
302 CountExtensionItems(menu
));
305 // Test that the "show" and "hide" menu items appear correctly in the extension
307 TEST_F(ExtensionContextMenuModelTest
, ExtensionContextMenuShowAndHide
) {
308 InitializeEmptyExtensionService();
309 scoped_refptr
<const Extension
> page_action
=
310 BuildExtension("page_action_extension",
311 manifest_keys::kPageAction
,
313 ASSERT_TRUE(page_action
.get());
314 scoped_refptr
<const Extension
> browser_action
=
315 BuildExtension("browser_action_extension",
316 manifest_keys::kBrowserAction
,
318 ASSERT_TRUE(browser_action
.get());
320 service()->AddExtension(page_action
.get());
321 service()->AddExtension(browser_action
.get());
324 const ExtensionContextMenuModel::MenuEntries visibility_command
=
325 ExtensionContextMenuModel::TOGGLE_VISIBILITY
;
326 base::string16 hide_string
=
327 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_BUTTON
);
328 base::string16 redesign_hide_string
=
329 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_BUTTON_IN_MENU
);
330 base::string16 redesign_show_string
=
331 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_BUTTON_IN_TOOLBAR
);
332 base::string16 redesign_keep_string
=
333 l10n_util::GetStringUTF16(IDS_EXTENSIONS_KEEP_BUTTON_IN_TOOLBAR
);
336 ExtensionContextMenuModel
menu(page_action
.get(), GetBrowser(),
337 ExtensionContextMenuModel::VISIBLE
, nullptr);
339 int index
= GetCommandIndex(menu
, visibility_command
);
340 // Without the toolbar redesign switch, page action menus shouldn't have a
341 // visibility option.
342 EXPECT_EQ(-1, index
);
346 ExtensionContextMenuModel
menu(browser_action
.get(), GetBrowser(),
347 ExtensionContextMenuModel::VISIBLE
, nullptr);
348 int index
= GetCommandIndex(menu
, visibility_command
);
349 // Browser actions should have the visibility option.
350 EXPECT_NE(-1, index
);
351 // Since the action is currently visible, it should have the option to hide
353 EXPECT_EQ(hide_string
, menu
.GetLabelAt(index
));
356 // Enabling the toolbar redesign switch should give page actions the button.
357 FeatureSwitch::ScopedOverride
enable_toolbar_redesign(
358 FeatureSwitch::extension_action_redesign(), true);
360 ExtensionContextMenuModel
menu(page_action
.get(), GetBrowser(),
361 ExtensionContextMenuModel::VISIBLE
, nullptr);
362 int index
= GetCommandIndex(menu
, visibility_command
);
363 EXPECT_NE(-1, index
);
364 EXPECT_EQ(redesign_hide_string
, menu
.GetLabelAt(index
));
368 // If the action is overflowed, it should have the "Show button in toolbar"
370 ExtensionContextMenuModel
menu(browser_action
.get(), GetBrowser(),
371 ExtensionContextMenuModel::OVERFLOWED
,
373 int index
= GetCommandIndex(menu
, visibility_command
);
374 EXPECT_NE(-1, index
);
375 EXPECT_EQ(redesign_show_string
, menu
.GetLabelAt(index
));
379 // If the action is transitively visible, as happens when it is showing a
380 // popup, we should use a "Keep button in toolbar" string.
381 ExtensionContextMenuModel
menu(
382 browser_action
.get(), GetBrowser(),
383 ExtensionContextMenuModel::TRANSITIVELY_VISIBLE
, nullptr);
384 int index
= GetCommandIndex(menu
, visibility_command
);
385 EXPECT_NE(-1, index
);
386 EXPECT_EQ(redesign_keep_string
, menu
.GetLabelAt(index
));
390 TEST_F(ExtensionContextMenuModelTest
, ExtensionContextUninstall
) {
391 InitializeEmptyExtensionService();
393 scoped_refptr
<const Extension
> extension
= BuildExtension(
394 "extension", manifest_keys::kBrowserAction
, Manifest::INTERNAL
);
395 ASSERT_TRUE(extension
.get());
396 service()->AddExtension(extension
.get());
397 const std::string extension_id
= extension
->id();
398 ASSERT_TRUE(registry()->enabled_extensions().GetByID(extension_id
));
400 ScopedTestDialogAutoConfirm
auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT
);
401 TestExtensionRegistryObserver
uninstalled_observer(registry());
403 // Scope the menu so that it's destroyed during the uninstall process. This
404 // reflects what normally happens (Chrome closes the menu when the uninstall
406 ExtensionContextMenuModel
menu(extension
.get(), GetBrowser(),
407 ExtensionContextMenuModel::VISIBLE
, nullptr);
408 menu
.ExecuteCommand(ExtensionContextMenuModel::UNINSTALL
, 0);
410 uninstalled_observer
.WaitForExtensionUninstalled();
411 EXPECT_FALSE(registry()->GetExtensionById(extension_id
,
412 ExtensionRegistry::EVERYTHING
));
415 } // namespace extensions