1 // Copyright 2019 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/ui/views/extensions/extensions_menu_view.h"
6
7 #include <algorithm>
8
9 #include "base/strings/utf_string_conversions.h"
10 #include "base/task/post_task.h"
11 #include "build/build_config.h"
12 #include "chrome/browser/extensions/chrome_test_extension_loader.h"
13 #include "chrome/browser/extensions/extension_action_runner.h"
14 #include "chrome/browser/extensions/extension_context_menu_model.h"
15 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/extensions/install_verifier.h"
17 #include "chrome/browser/extensions/scripting_permissions_modifier.h"
18 #include "chrome/browser/ui/extensions/extension_install_ui_default.h"
19 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
20 #include "chrome/browser/ui/views/extensions/extensions_menu_button.h"
21 #include "chrome/browser/ui/views/extensions/extensions_menu_item_view.h"
22 #include "chrome/browser/ui/views/extensions/extensions_toolbar_browsertest.h"
23 #include "chrome/browser/ui/views/extensions/extensions_toolbar_button.h"
24 #include "chrome/browser/ui/views/extensions/extensions_toolbar_container.h"
25 #include "chrome/browser/ui/views/frame/browser_view.h"
26 #include "chrome/browser/ui/views/hover_button_controller.h"
27 #include "chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h"
28 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
29 #include "chrome/common/webui_url_constants.h"
30 #include "chrome/test/base/ui_test_utils.h"
31 #include "content/public/test/browser_test.h"
32 #include "content/public/test/test_navigation_observer.h"
33 #include "extensions/browser/disable_reason.h"
34 #include "extensions/browser/extension_system.h"
35 #include "extensions/browser/pref_names.h"
36 #include "extensions/common/extension.h"
37 #include "extensions/test/test_extension_dir.h"
38 #include "testing/gmock/include/gmock/gmock.h"
39 #include "ui/views/animation/ink_drop.h"
40 #include "ui/views/bubble/bubble_dialog_model_host.h"
41 #include "ui/views/controls/button/image_button.h"
42 #include "ui/views/layout/animating_layout_manager.h"
43 #include "ui/views/layout/animating_layout_manager_test_util.h"
44 #include "ui/views/test/widget_test.h"
45 #include "ui/views/view_class_properties.h"
46 #include "ui/views/widget/any_widget_observer.h"
47
48 using ::testing::ElementsAre;
49
50 class ExtensionsMenuViewBrowserTest : public ExtensionsToolbarBrowserTest {
51 public:
52 enum class ExtensionRemovalMethod {
53 kDisable,
54 kUninstall,
55 kBlocklist,
56 kTerminate,
57 };
58
GetExtensionsMenuItemViews()59 static std::vector<ExtensionsMenuItemView*> GetExtensionsMenuItemViews() {
60 return ExtensionsMenuView::GetExtensionsMenuViewForTesting()
61 ->extensions_menu_items_for_testing();
62 }
63
ShowUi(const std::string & name)64 void ShowUi(const std::string& name) override {
65 #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
66 // The extensions menu can appear offscreen on Linux, so verifying bounds
67 // makes the tests flaky.
68 set_should_verify_dialog_bounds(false);
69 #endif
70 ui_test_name_ = name;
71
72 if (name == "ReloadPageBubble") {
73 ClickExtensionsMenuButton();
74 TriggerSingleExtensionButton();
75 } else if (ui_test_name_ == "UninstallDialog_Accept" ||
76 ui_test_name_ == "UninstallDialog_Cancel") {
77 ExtensionsToolbarContainer* const container =
78 GetExtensionsToolbarContainer();
79
80 LoadTestExtension("extensions/uitest/long_name");
81 LoadTestExtension("extensions/uitest/window_open");
82
83 // Without the uninstall dialog the icon should now be invisible.
84 EXPECT_FALSE(container->IsActionVisibleOnToolbar(
85 container->GetActionForId(extensions()[0]->id())));
86 EXPECT_FALSE(
87 container->GetViewForId(extensions()[0]->id())->GetVisible());
88
89 // Trigger uninstall dialog.
90 views::NamedWidgetShownWaiter waiter(
91 views::test::AnyWidgetTestPasskey{},
92 views::BubbleDialogModelHost::kViewClassName);
93 extensions::ExtensionContextMenuModel menu_model(
94 extensions()[0].get(), browser(),
95 extensions::ExtensionContextMenuModel::PINNED, nullptr,
96 false /* can_show_icon_in_toolbar */);
97 menu_model.ExecuteCommand(
98 extensions::ExtensionContextMenuModel::UNINSTALL, 0);
99 ASSERT_TRUE(waiter.WaitIfNeededAndGet());
100 } else if (ui_test_name_ == "InstallDialog") {
101 LoadTestExtension("extensions/uitest/long_name");
102 LoadTestExtension("extensions/uitest/window_open");
103
104 // Trigger post-install dialog.
105 ExtensionInstallUIDefault::ShowPlatformBubble(extensions()[0], browser(),
106 SkBitmap());
107 } else {
108 ClickExtensionsMenuButton();
109 ASSERT_TRUE(ExtensionsMenuView::GetExtensionsMenuViewForTesting());
110 }
111
112 // Wait for any pending animations to finish so that correct pinned
113 // extensions and dialogs are actually showing.
114 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
115 }
116
VerifyUi()117 bool VerifyUi() override {
118 EXPECT_TRUE(ExtensionsToolbarBrowserTest::VerifyUi());
119
120 if (ui_test_name_ == "ReloadPageBubble") {
121 ExtensionsToolbarContainer* const container =
122 GetExtensionsToolbarContainer();
123 // Clicking the extension should close the extensions menu, pop out the
124 // extension, and display the "reload this page" bubble.
125 EXPECT_TRUE(container->GetAnchoredWidgetForExtensionForTesting(
126 extensions()[0]->id()));
127 EXPECT_FALSE(container->GetPoppedOutAction());
128 EXPECT_FALSE(ExtensionsMenuView::IsShowing());
129 } else if (ui_test_name_ == "UninstallDialog_Accept" ||
130 ui_test_name_ == "UninstallDialog_Cancel" ||
131 ui_test_name_ == "InstallDialog") {
132 ExtensionsToolbarContainer* const container =
133 GetExtensionsToolbarContainer();
134 EXPECT_TRUE(container->IsActionVisibleOnToolbar(
135 container->GetActionForId(extensions()[0]->id())));
136 EXPECT_TRUE(container->GetViewForId(extensions()[0]->id())->GetVisible());
137 }
138
139 return true;
140 }
141
DismissUi()142 void DismissUi() override {
143 if (ui_test_name_ == "UninstallDialog_Accept" ||
144 ui_test_name_ == "UninstallDialog_Cancel") {
145 DismissUninstallDialog();
146 return;
147 }
148
149 if (ui_test_name_ == "InstallDialog") {
150 ExtensionsToolbarContainer* const container =
151 GetExtensionsToolbarContainer();
152 views::DialogDelegate* const install_bubble =
153 container->GetViewForId(extensions()[0]->id())
154 ->GetProperty(views::kAnchoredDialogKey);
155 ASSERT_TRUE(install_bubble);
156 install_bubble->GetWidget()->Close();
157 return;
158 }
159
160 // Use default implementation for other tests.
161 ExtensionsToolbarBrowserTest::DismissUi();
162 }
163
DismissUninstallDialog()164 void DismissUninstallDialog() {
165 ExtensionsToolbarContainer* const container =
166 GetExtensionsToolbarContainer();
167 // Accept or cancel the dialog.
168 views::DialogDelegate* const uninstall_bubble =
169 container->GetViewForId(extensions()[0]->id())
170 ->GetProperty(views::kAnchoredDialogKey);
171 ASSERT_TRUE(uninstall_bubble);
172 views::test::WidgetDestroyedWaiter destroyed_waiter(
173 uninstall_bubble->GetWidget());
174 if (ui_test_name_ == "UninstallDialog_Accept") {
175 uninstall_bubble->AcceptDialog();
176 } else {
177 uninstall_bubble->CancelDialog();
178 }
179 destroyed_waiter.Wait();
180
181 if (ui_test_name_ == "UninstallDialog_Accept") {
182 // Accepting the dialog should remove the item from the container and the
183 // ExtensionRegistry.
184 EXPECT_EQ(nullptr, container->GetActionForId(extensions()[0]->id()));
185 EXPECT_EQ(nullptr, extensions::ExtensionRegistry::Get(profile())
186 ->GetInstalledExtension(extensions()[0]->id()));
187 } else {
188 // After dismissal the icon should become invisible.
189 // Wait for animations to finish.
190 views::test::WaitForAnimatingLayoutManager(
191 GetExtensionsToolbarContainer());
192
193 // The extension should still be present in the ExtensionRegistry (not
194 // uninstalled) when the uninstall dialog is dismissed.
195 EXPECT_NE(nullptr, extensions::ExtensionRegistry::Get(profile())
196 ->GetInstalledExtension(extensions()[0]->id()));
197 // Without the uninstall dialog present the icon should now be
198 // invisible.
199 EXPECT_FALSE(container->IsActionVisibleOnToolbar(
200 container->GetActionForId(extensions()[0]->id())));
201 EXPECT_FALSE(
202 container->GetViewForId(extensions()[0]->id())->GetVisible());
203 }
204 }
205
TriggerSingleExtensionButton()206 void TriggerSingleExtensionButton() {
207 ASSERT_EQ(1u, GetExtensionsMenuItemViews().size());
208 TriggerExtensionButton(0u);
209 }
210
TriggerExtensionButton(size_t item_index)211 void TriggerExtensionButton(size_t item_index) {
212 auto menu_items = GetExtensionsMenuItemViews();
213 ASSERT_LT(item_index, menu_items.size());
214
215 ui::MouseEvent click_event(ui::ET_MOUSE_RELEASED, gfx::Point(),
216 gfx::Point(), base::TimeTicks(),
217 ui::EF_LEFT_MOUSE_BUTTON, 0);
218 menu_items[item_index]
219 ->primary_action_button_for_testing()
220 ->button_controller()
221 ->OnMouseReleased(click_event);
222
223 // Wait for animations to finish.
224 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
225 }
226
RightClickExtensionInToolbar(ToolbarActionView * extension)227 void RightClickExtensionInToolbar(ToolbarActionView* extension) {
228 ui::MouseEvent click_down_event(ui::ET_MOUSE_PRESSED, gfx::Point(),
229 gfx::Point(), base::TimeTicks(),
230 ui::EF_RIGHT_MOUSE_BUTTON, 0);
231 ui::MouseEvent click_up_event(ui::ET_MOUSE_RELEASED, gfx::Point(),
232 gfx::Point(), base::TimeTicks(),
233 ui::EF_RIGHT_MOUSE_BUTTON, 0);
234 extension->OnMouseEvent(&click_down_event);
235 extension->OnMouseEvent(&click_up_event);
236 }
237
ClickExtensionsMenuButton(Browser * browser)238 void ClickExtensionsMenuButton(Browser* browser) {
239 ui::MouseEvent click_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
240 base::TimeTicks(), ui::EF_LEFT_MOUSE_BUTTON, 0);
241 BrowserView::GetBrowserViewForBrowser(browser)
242 ->toolbar()
243 ->GetExtensionsButton()
244 ->OnMousePressed(click_event);
245 }
246
ClickExtensionsMenuButton()247 void ClickExtensionsMenuButton() { ClickExtensionsMenuButton(browser()); }
248
RemoveExtension(ExtensionRemovalMethod method,const std::string & extension_id)249 void RemoveExtension(ExtensionRemovalMethod method,
250 const std::string& extension_id) {
251 extensions::ExtensionService* const extension_service =
252 extensions::ExtensionSystem::Get(browser()->profile())
253 ->extension_service();
254 switch (method) {
255 case ExtensionRemovalMethod::kDisable:
256 extension_service->DisableExtension(
257 extension_id, extensions::disable_reason::DISABLE_USER_ACTION);
258 break;
259 case ExtensionRemovalMethod::kUninstall:
260 extension_service->UninstallExtension(
261 extension_id, extensions::UNINSTALL_REASON_FOR_TESTING, nullptr);
262 break;
263 case ExtensionRemovalMethod::kBlocklist:
264 extension_service->BlocklistExtensionForTest(extension_id);
265 break;
266 case ExtensionRemovalMethod::kTerminate:
267 extension_service->TerminateExtension(extension_id);
268 break;
269 }
270
271 // Removing an extension can result in the container changing visibility.
272 // Allow it to finish laying out appropriately.
273 auto* container = GetExtensionsToolbarContainer();
274 container->GetWidget()->LayoutRootViewIfNecessary();
275 }
276
VerifyContainerVisibility(ExtensionRemovalMethod method,bool expected_visibility)277 void VerifyContainerVisibility(ExtensionRemovalMethod method,
278 bool expected_visibility) {
279 // An empty container should not be shown.
280 EXPECT_FALSE(GetExtensionsToolbarContainer()->GetVisible());
281
282 // Loading the first extension should show the button (and container).
283 LoadTestExtension("extensions/uitest/long_name");
284 EXPECT_TRUE(GetExtensionsToolbarContainer()->IsDrawn());
285
286 // Add another extension so we can make sure that removing some don't change
287 // the visibility.
288 LoadTestExtension("extensions/uitest/window_open");
289
290 // Remove 1/2 extensions, should still be drawn.
291 RemoveExtension(method, extensions()[0]->id());
292 EXPECT_TRUE(GetExtensionsToolbarContainer()->IsDrawn());
293
294 // Removing the last extension. All actions now have the same state.
295 RemoveExtension(method, extensions()[1]->id());
296 EXPECT_EQ(expected_visibility, GetExtensionsToolbarContainer()->IsDrawn());
297 }
298
299 std::string ui_test_name_;
300 };
301
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,InvokeUi_default)302 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest, InvokeUi_default) {
303 LoadTestExtension("extensions/uitest/long_name");
304 LoadTestExtension("extensions/uitest/window_open");
305
306 ShowAndVerifyUi();
307 }
308
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,InvisibleWithoutExtension_Disable)309 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
310 InvisibleWithoutExtension_Disable) {
311 VerifyContainerVisibility(ExtensionRemovalMethod::kDisable, false);
312 }
313
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,InvisibleWithoutExtension_Uninstall)314 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
315 InvisibleWithoutExtension_Uninstall) {
316 VerifyContainerVisibility(ExtensionRemovalMethod::kUninstall, false);
317 }
318
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,InvisibleWithoutExtension_Blocklist)319 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
320 InvisibleWithoutExtension_Blocklist) {
321 VerifyContainerVisibility(ExtensionRemovalMethod::kBlocklist, false);
322 }
323
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,InvisibleWithoutExtension_Terminate)324 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
325 InvisibleWithoutExtension_Terminate) {
326 // TODO(pbos): Keep the container visible when extensions are terminated
327 // (crash). This lets users find and restart them. Then update this test
328 // expectation to be kept visible by terminated extensions. Also update the
329 // test name to reflect that the container should be visible with only
330 // terminated extensions.
331 VerifyContainerVisibility(ExtensionRemovalMethod::kTerminate, false);
332 }
333
334 // Invokes the UI shown when a user has to reload a page in order to run an
335 // extension.
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,InvokeUi_ReloadPageBubble)336 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
337 InvokeUi_ReloadPageBubble) {
338 ASSERT_TRUE(embedded_test_server()->Start());
339 extensions::TestExtensionDir test_dir;
340 // Load an extension that injects scripts at "document_start", which requires
341 // reloading the page to inject if permissions are withheld.
342 test_dir.WriteManifest(
343 R"({
344 "name": "Runs Script Everywhere",
345 "description": "An extension that runs script everywhere",
346 "manifest_version": 2,
347 "version": "0.1",
348 "content_scripts": [{
349 "matches": ["*://*/*"],
350 "js": ["script.js"],
351 "run_at": "document_start"
352 }]
353 })");
354 test_dir.WriteFile(FILE_PATH_LITERAL("script.js"),
355 "console.log('injected!');");
356
357 AppendExtension(
358 extensions::ChromeTestExtensionLoader(profile()).LoadExtension(
359 test_dir.UnpackedPath()));
360 ASSERT_EQ(1u, extensions().size());
361 ASSERT_TRUE(extensions().front());
362
363 extensions::ScriptingPermissionsModifier(profile(), extensions().front())
364 .SetWithholdHostPermissions(true);
365
366 // Navigate to a page the extension wants to run on.
367 content::WebContents* tab =
368 browser()->tab_strip_model()->GetActiveWebContents();
369 {
370 content::TestNavigationObserver observer(tab);
371 GURL url = embedded_test_server()->GetURL("example.com", "/title1.html");
372 ui_test_utils::NavigateToURL(browser(), url);
373 EXPECT_TRUE(observer.last_navigation_succeeded());
374 }
375
376 ShowAndVerifyUi();
377 }
378
IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,ExtensionsMenuButtonHighlight)379 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
380 ExtensionsMenuButtonHighlight) {
381 LoadTestExtension("extensions/uitest/window_open");
382 ClickExtensionsMenuButton();
383 EXPECT_EQ(BrowserView::GetBrowserViewForBrowser(browser())
384 ->toolbar()
385 ->GetExtensionsButton()
386 ->GetInkDrop()
387 ->GetTargetInkDropState(),
388 views::InkDropState::ACTIVATED);
389 }
390
391 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest, TriggerPopup) {
392 LoadTestExtension("extensions/simple_with_popup");
393 ShowUi("");
394 VerifyUi();
395
396 ExtensionsToolbarContainer* const extensions_container =
397 GetExtensionsToolbarContainer();
398
399 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
400 EXPECT_TRUE(GetVisibleToolbarActionViews().empty());
401
402 TriggerSingleExtensionButton();
403
404 // After triggering an extension with a popup, there should a popped-out
405 // action and show the view.
406 auto visible_icons = GetVisibleToolbarActionViews();
407 EXPECT_NE(nullptr, extensions_container->GetPoppedOutAction());
408 ASSERT_EQ(1u, visible_icons.size());
409 EXPECT_EQ(extensions_container->GetPoppedOutAction(),
410 visible_icons[0]->view_controller());
411
412 extensions_container->HideActivePopup();
413
414 // Wait for animations to finish.
415 views::test::WaitForAnimatingLayoutManager(extensions_container);
416
417 // After dismissing the popup there should no longer be a popped-out action
418 // and the icon should no longer be visible in the extensions container.
419 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
420 EXPECT_TRUE(GetVisibleToolbarActionViews().empty());
421 }
422
423 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
424 ContextMenuKeepsExtensionPoppedOut) {
425 LoadTestExtension("extensions/simple_with_popup");
426 ShowUi("");
427 VerifyUi();
428
429 ExtensionsToolbarContainer* const extensions_container =
430 GetExtensionsToolbarContainer();
431
432 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
433 EXPECT_TRUE(GetVisibleToolbarActionViews().empty());
434
435 TriggerSingleExtensionButton();
436
437 // After triggering an extension with a popup, there should a popped-out
438 // action and show the view.
439 auto visible_icons = GetVisibleToolbarActionViews();
440 EXPECT_NE(nullptr, extensions_container->GetPoppedOutAction());
441 EXPECT_EQ(base::nullopt,
442 extensions_container->GetExtensionWithOpenContextMenuForTesting());
443 ASSERT_EQ(1u, visible_icons.size());
444 EXPECT_EQ(extensions_container->GetPoppedOutAction(),
445 visible_icons[0]->view_controller());
446
447 RightClickExtensionInToolbar(extensions_container->GetViewForId(
448 extensions_container->GetPoppedOutAction()->GetId()));
449 extensions_container->HideActivePopup();
450
451 // Wait for animations to finish.
452 views::test::WaitForAnimatingLayoutManager(extensions_container);
453
454 visible_icons = GetVisibleToolbarActionViews();
455 ASSERT_EQ(1u, visible_icons.size());
456 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
457 EXPECT_NE(base::nullopt,
458 extensions_container->GetExtensionWithOpenContextMenuForTesting());
459 EXPECT_EQ(extensions_container->GetExtensionWithOpenContextMenuForTesting(),
460 visible_icons[0]->view_controller()->GetId());
461 }
462
463 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
464 RemoveExtensionShowingPopup) {
465 LoadTestExtension("extensions/simple_with_popup");
466 ShowUi("");
467 VerifyUi();
468 TriggerSingleExtensionButton();
469
470 ExtensionsContainer* const extensions_container =
471 BrowserView::GetBrowserViewForBrowser(browser())
472 ->toolbar()
473 ->extensions_container();
474 ToolbarActionViewController* action =
475 extensions_container->GetPoppedOutAction();
476 ASSERT_NE(nullptr, action);
477 ASSERT_EQ(1u, GetVisibleToolbarActionViews().size());
478
479 extensions::ExtensionSystem::Get(browser()->profile())
480 ->extension_service()
481 ->DisableExtension(action->GetId(),
482 extensions::disable_reason::DISABLE_USER_ACTION);
483
484 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
485 EXPECT_TRUE(GetVisibleToolbarActionViews().empty());
486 }
487
488 // Test for crbug.com/1099456.
489 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
490 RemoveMultipleExtensionsWhileShowingPopup) {
491 auto& id1 = LoadTestExtension("extensions/simple_with_popup")->id();
492 auto& id2 = LoadTestExtension("extensions/uitest/window_open")->id();
493 ShowUi("");
494 VerifyUi();
495 TriggerExtensionButton(0u);
496
497 ExtensionsContainer* const extensions_container =
498 BrowserView::GetBrowserViewForBrowser(browser())
499 ->toolbar()
500 ->extensions_container();
501 ASSERT_NE(nullptr, extensions_container->GetPoppedOutAction());
502
503 auto* extension_service =
504 extensions::ExtensionSystem::Get(browser()->profile())
505 ->extension_service();
506
507 extension_service->DisableExtension(
508 id1, extensions::disable_reason::DISABLE_USER_ACTION);
509 extension_service->DisableExtension(
510 id2, extensions::disable_reason::DISABLE_USER_ACTION);
511
512 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
513 }
514
515 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
516 TriggeringExtensionClosesMenu) {
517 LoadTestExtension("extensions/trigger_actions/browser_action");
518 ShowUi("");
519 VerifyUi();
520
521 EXPECT_TRUE(ExtensionsMenuView::IsShowing());
522
523 views::test::WidgetDestroyedWaiter destroyed_waiter(
524 ExtensionsMenuView::GetExtensionsMenuViewForTesting()->GetWidget());
525 TriggerSingleExtensionButton();
526
527 destroyed_waiter.Wait();
528
529 ExtensionsContainer* const extensions_container =
530 BrowserView::GetBrowserViewForBrowser(browser())
531 ->toolbar()
532 ->extensions_container();
533
534 // This test should not use a popped-out action, as we want to make sure that
535 // the menu closes on its own and not because a popup dialog replaces it.
536 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
537
538 EXPECT_FALSE(ExtensionsMenuView::IsShowing());
539 }
540
541 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
542 CreatesOneMenuItemPerExtension) {
543 LoadTestExtension("extensions/uitest/long_name");
544 LoadTestExtension("extensions/uitest/window_open");
545 ShowUi("");
546 VerifyUi();
547 EXPECT_EQ(2u, extensions().size());
548 EXPECT_EQ(extensions().size(), GetExtensionsMenuItemViews().size());
549 DismissUi();
550 }
551
552 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
553 PinningDisabledInIncognito) {
554 LoadTestExtension("extensions/uitest/window_open", true);
555 SetUpIncognitoBrowser();
556
557 // Make sure the pinning item is disabled for context menus in the Incognito
558 // browser.
559 extensions::ExtensionContextMenuModel menu(
560 extensions()[0].get(), incognito_browser(),
561 extensions::ExtensionContextMenuModel::PINNED, nullptr,
562 true /* can_show_icon_in_toolbar */);
563 EXPECT_FALSE(menu.IsCommandIdEnabled(
564 extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY));
565
566 // Show menu and verify that the in-menu pin button is disabled too.
567 ClickExtensionsMenuButton(incognito_browser());
568
569 ASSERT_TRUE(VerifyUi());
570 ASSERT_EQ(1u, GetExtensionsMenuItemViews().size());
571 EXPECT_EQ(views::Button::STATE_DISABLED, GetExtensionsMenuItemViews()
572 .front()
573 ->pin_button_for_testing()
574 ->GetState());
575
576 DismissUi();
577 }
578
579 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
580 PinnedExtensionShowsCorrectContextMenuPinOption) {
581 LoadTestExtension("extensions/simple_with_popup");
582
583 ClickExtensionsMenuButton();
584 ExtensionsToolbarContainer* const extensions_container =
585 GetExtensionsToolbarContainer();
586
587 // Pin extension from menu.
588 ASSERT_TRUE(VerifyUi());
589 ASSERT_EQ(1u, GetExtensionsMenuItemViews().size());
590 ui::MouseEvent click_pressed_event(ui::ET_MOUSE_PRESSED, gfx::Point(),
591 gfx::Point(), base::TimeTicks(),
592 ui::EF_LEFT_MOUSE_BUTTON, 0);
593 ui::MouseEvent click_released_event(ui::ET_MOUSE_RELEASED, gfx::Point(),
594 gfx::Point(), base::TimeTicks(),
595 ui::EF_LEFT_MOUSE_BUTTON, 0);
596 GetExtensionsMenuItemViews()
597 .front()
598 ->pin_button_for_testing()
599 ->OnMousePressed(click_pressed_event);
600 GetExtensionsMenuItemViews()
601 .front()
602 ->pin_button_for_testing()
603 ->OnMouseReleased(click_released_event);
604
605 // Wait for any pending animations to finish so that correct pinned
606 // extensions and dialogs are actually showing.
607 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
608
609 // Verify extension is pinned but not stored as the popped out action.
610 auto visible_icons = GetVisibleToolbarActionViews();
611 visible_icons = GetVisibleToolbarActionViews();
612 ASSERT_EQ(1u, visible_icons.size());
613 EXPECT_EQ(nullptr, extensions_container->GetPoppedOutAction());
614
615 // Trigger the pinned extension.
616 ToolbarActionView* pinned_extension =
617 extensions_container->GetViewForId(extensions()[0]->id());
618 pinned_extension->OnMouseEvent(&click_pressed_event);
619 pinned_extension->OnMouseEvent(&click_released_event);
620
621 // Wait for any pending animations to finish so that correct pinned
622 // extensions and dialogs are actually showing.
623 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
624
625 EXPECT_NE(nullptr, extensions_container->GetPoppedOutAction());
626
627 // Verify the context menu option is to unpin the extension.
628 ui::SimpleMenuModel* context_menu = static_cast<ui::SimpleMenuModel*>(
629 extensions_container->GetActionForId(extensions()[0]->id())
630 ->GetContextMenu());
631 int visibility_index = context_menu->GetIndexOfCommandId(
632 extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
633 ASSERT_GE(visibility_index, 0);
634 base::string16 visibility_label = context_menu->GetLabelAt(visibility_index);
635 EXPECT_EQ(base::UTF16ToUTF8(visibility_label), "Unpin");
636 }
637
638 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
639 UnpinnedExtensionShowsCorrectContextMenuPinOption) {
640 LoadTestExtension("extensions/simple_with_popup");
641
642 ClickExtensionsMenuButton();
643 ExtensionsToolbarContainer* const extensions_container =
644 GetExtensionsToolbarContainer();
645
646 TriggerSingleExtensionButton();
647
648 // Wait for any pending animations to finish so that correct pinned
649 // extensions and dialogs are actually showing.
650 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
651
652 // Verify extension is visible and tbere is a popped out action.
653 auto visible_icons = GetVisibleToolbarActionViews();
654 ASSERT_EQ(1u, visible_icons.size());
655 EXPECT_NE(nullptr, extensions_container->GetPoppedOutAction());
656
657 // Verify the context menu option is to unpin the extension.
658 ui::SimpleMenuModel* context_menu = static_cast<ui::SimpleMenuModel*>(
659 extensions_container->GetActionForId(extensions()[0]->id())
660 ->GetContextMenu());
661 int visibility_index = context_menu->GetIndexOfCommandId(
662 extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
663 ASSERT_GE(visibility_index, 0);
664 base::string16 visibility_label = context_menu->GetLabelAt(visibility_index);
665 EXPECT_EQ(base::UTF16ToUTF8(visibility_label), "Pin");
666 }
667
668 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
669 ManageExtensionsOpensExtensionsPage) {
670 // Ensure the menu is visible by adding an extension.
671 LoadTestExtension("extensions/trigger_actions/browser_action");
672 ShowUi("");
673 VerifyUi();
674
675 EXPECT_TRUE(ExtensionsMenuView::IsShowing());
676
677 ui::MouseEvent click_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
678 base::TimeTicks(), ui::EF_LEFT_MOUSE_BUTTON, 0);
679 ExtensionsMenuView::GetExtensionsMenuViewForTesting()
680 ->manage_extensions_button_for_testing()
681 ->button_controller()
682 ->OnMouseReleased(click_event);
683
684 // Clicking the Manage Extensions button should open chrome://extensions.
685 EXPECT_EQ(
686 chrome::kChromeUIExtensionsURL,
687 browser()->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
688 }
689
690 // Tests that clicking on the context menu button of an extension item opens the
691 // context menu.
692 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
693 ClickingContextMenuButton) {
694 LoadTestExtension("extensions/uitest/window_open");
695 ClickExtensionsMenuButton();
696
697 auto menu_items = GetExtensionsMenuItemViews();
698 ASSERT_EQ(1u, menu_items.size());
699 ExtensionsMenuItemView* item_view = menu_items[0];
700 EXPECT_FALSE(item_view->IsContextMenuRunning());
701
702 views::ImageButton* context_menu_button =
703 menu_items[0]->context_menu_button_for_testing();
704 ui::MouseEvent press_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
705 base::TimeTicks(), ui::EF_LEFT_MOUSE_BUTTON, 0);
706 context_menu_button->OnMousePressed(press_event);
707 ui::MouseEvent release_event(ui::ET_MOUSE_RELEASED, gfx::Point(),
708 gfx::Point(), base::TimeTicks(),
709 ui::EF_LEFT_MOUSE_BUTTON, 0);
710 context_menu_button->OnMouseReleased(release_event);
711
712 EXPECT_TRUE(item_view->IsContextMenuRunning());
713 }
714
715 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest, InvokeUi_InstallDialog) {
716 ShowAndVerifyUi();
717 }
718
719 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
720 InvokeUi_UninstallDialog_Accept) {
721 ShowAndVerifyUi();
722 }
723
724 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
725 InvokeUi_UninstallDialog_Cancel) {
726 ShowAndVerifyUi();
727 }
728
729 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest, InvocationSourceMetrics) {
730 base::HistogramTester histogram_tester;
731 LoadTestExtension("extensions/uitest/extension_with_action_and_command");
732 ClickExtensionsMenuButton();
733
734 constexpr char kHistogramName[] = "Extensions.Toolbar.InvocationSource";
735 histogram_tester.ExpectTotalCount(kHistogramName, 0);
736
737 TriggerSingleExtensionButton();
738 histogram_tester.ExpectTotalCount(kHistogramName, 1);
739 histogram_tester.ExpectBucketCount(
740 kHistogramName, ToolbarActionViewController::InvocationSource::kMenuEntry,
741 1);
742
743 // TODO(devlin): Add a test for command invocation once
744 // https://crbug.com/1070305 is fixed.
745 }
746
747 namespace {
748 constexpr char kExtensionAId[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf";
749 constexpr char kExtensionBId[] = "mockepjebcnmhmhcahfddgfcdgkdifnc";
750 constexpr char kExtensionCId[] = "dpfmafkdlbmopmcepgpjkpldjbghdibm";
751
752 bool TestShouldEnableToolbarMenuExperiment() {
753 std::string test_name =
754 testing::UnitTest::GetInstance()->current_test_info()->name();
755 // This PRE_PRE_ step sets up pre-migration extension prefs. The experiment
756 // triggers migration so it needs to be off during pre-condition setup.
757 return test_name.find(
758 "PRE_PRE_PostExtensionMigrationChangesPersistAfterRestart") ==
759 std::string::npos;
760 }
761
762 } // namespace
763
764 class ExtensionsToolbarMigrationBrowserTest
765 : public ExtensionsToolbarBrowserTest {
766 protected:
767 ExtensionsToolbarMigrationBrowserTest()
768 : ExtensionsToolbarBrowserTest(TestShouldEnableToolbarMenuExperiment()) {}
769
770 void ShowUi(const std::string& name) override {
771 // Intentionally empty, this tests UI in the toolbar.
772 }
773
774 private:
775 extensions::ScopedInstallVerifierBypassForTest ignore_install_verification_;
776 };
777
778 // Add and verify extensions with extensions toolbar menu feature turned off.
779 // TODO(corising): Remove this series of tests and the |enable_flag| parameter
780 // from initialization of ExtensionsToolbarBrowserTest once the extensions
781 // toolbar menu experiment has been launched for a couple milestones.
782 IN_PROC_BROWSER_TEST_F(
783 ExtensionsToolbarMigrationBrowserTest,
784 PRE_PRE_PostExtensionMigrationChangesPersistAfterRestart) {
785 // Add three extensions.
786 LoadTestExtension("extensions/good.crx");
787 LoadTestExtension("extensions/trivial_extension/extension.crx");
788 LoadTestExtension("extensions/page_action.crx");
789
790 // Verify all extensions have been added.
791 EXPECT_EQ(3u, extensions().size());
792 EXPECT_EQ(extensions()[0]->id(), kExtensionAId);
793 EXPECT_EQ(extensions()[1]->id(), kExtensionBId);
794 EXPECT_EQ(extensions()[2]->id(), kExtensionCId);
795
796 BrowserActionsContainer* browser_actions =
797 BrowserView::GetBrowserViewForBrowser(browser())
798 ->toolbar()
799 ->browser_actions();
800
801 // Hide the last extension and verify that only the first two of the three are
802 // visible.
803 ToolbarActionsModel::Get(profile())->SetActionVisibility(kExtensionCId,
804 false);
805 EXPECT_TRUE(browser_actions->GetViewForId(kExtensionAId)->GetVisible());
806 EXPECT_TRUE(browser_actions->GetViewForId(kExtensionBId)->GetVisible());
807 EXPECT_FALSE(browser_actions->GetViewForId(kExtensionCId)->GetVisible());
808 }
809
810 // Test visible extensions migrate to pinned extensions after Chrome restart and
811 // that any further changes are reflected in the extension prefs.
812 IN_PROC_BROWSER_TEST_F(ExtensionsToolbarMigrationBrowserTest,
813 PRE_PostExtensionMigrationChangesPersistAfterRestart) {
814 // Wait for any animations to finish.
815 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
816
817 auto* toolbar_model = ToolbarActionsModel::Get(profile());
818
819 // Verify that the extensions that were visible are now the pinned extensions
820 // in the extension prefs.
821 extensions::ExtensionPrefs* const extension_prefs =
822 extensions::ExtensionPrefs::Get(profile());
823 EXPECT_THAT(extension_prefs->GetPinnedExtensions(),
824 testing::ElementsAre(kExtensionAId, kExtensionBId));
825 // Verify that the extensions that were visible are now the pinned extensions
826 // in the toolbar model.
827 EXPECT_TRUE(toolbar_model->IsActionPinned(kExtensionAId));
828 EXPECT_TRUE(toolbar_model->IsActionPinned(kExtensionBId));
829 EXPECT_FALSE(toolbar_model->IsActionPinned(kExtensionCId));
830 // Verify that the extensions that were visible are now visible in the toolbar
831 // container.
832 ExtensionsToolbarContainer* extensions_container =
833 GetExtensionsToolbarContainer();
834 EXPECT_TRUE(extensions_container->GetViewForId(kExtensionAId)->GetVisible());
835 EXPECT_TRUE(extensions_container->GetViewForId(kExtensionBId)->GetVisible());
836 EXPECT_FALSE(extensions_container->GetViewForId(kExtensionCId)->GetVisible());
837
838 // Verify that pinning/unpinning action is reflected in preferences.
839 toolbar_model->SetActionVisibility(kExtensionAId, false);
840 EXPECT_THAT(extension_prefs->GetPinnedExtensions(),
841 testing::ElementsAre(kExtensionBId));
842 toolbar_model->SetActionVisibility(kExtensionCId, true);
843 EXPECT_THAT(extension_prefs->GetPinnedExtensions(),
844 testing::ElementsAre(kExtensionBId, kExtensionCId));
845
846 // Verify that moving an action is reflected in preferences.
847 toolbar_model->MovePinnedAction(kExtensionCId, 0);
848 EXPECT_THAT(extension_prefs->GetPinnedExtensions(),
849 testing::ElementsAre(kExtensionCId, kExtensionBId));
850 }
851
852 // Test that any post-migration extension changes are persisent after Chrome
853 // restarts.
854 IN_PROC_BROWSER_TEST_F(ExtensionsToolbarMigrationBrowserTest,
855 PostExtensionMigrationChangesPersistAfterRestart) {
856 // Wait for any animations to finish.
857 views::test::WaitForAnimatingLayoutManager(GetExtensionsToolbarContainer());
858
859 extensions::ExtensionPrefs* const extension_prefs =
860 extensions::ExtensionPrefs::Get(profile());
861 EXPECT_THAT(extension_prefs->GetPinnedExtensions(),
862 testing::ElementsAre(kExtensionCId, kExtensionBId));
863 // Verify that these extensions are also pinned extensions in the toolbar
864 // model.
865 auto* toolbar_model = ToolbarActionsModel::Get(profile());
866 EXPECT_FALSE(toolbar_model->IsActionPinned(kExtensionAId));
867 EXPECT_TRUE(toolbar_model->IsActionPinned(kExtensionBId));
868 EXPECT_TRUE(toolbar_model->IsActionPinned(kExtensionCId));
869 // Verify that these extensions are visible in the toolbar container.
870 ExtensionsToolbarContainer* extensions_container =
871 GetExtensionsToolbarContainer();
872 EXPECT_FALSE(extensions_container->GetViewForId(kExtensionAId)->GetVisible());
873 EXPECT_TRUE(extensions_container->GetViewForId(kExtensionBId)->GetVisible());
874 EXPECT_TRUE(extensions_container->GetViewForId(kExtensionCId)->GetVisible());
875 }
876
877 class ActivateWithReloadExtensionsMenuBrowserTest
878 : public ExtensionsMenuViewBrowserTest,
879 public ::testing::WithParamInterface<bool> {};
880
881 IN_PROC_BROWSER_TEST_P(ActivateWithReloadExtensionsMenuBrowserTest,
882 ActivateWithReload) {
883 ASSERT_TRUE(embedded_test_server()->Start());
884 LoadTestExtension("extensions/blocked_actions/content_scripts");
885 auto extension = extensions().back();
886 extensions::ScriptingPermissionsModifier modifier(profile(), extension);
887 modifier.SetWithholdHostPermissions(true);
888
889 ui_test_utils::NavigateToURL(
890 browser(), embedded_test_server()->GetURL("example.com", "/empty.html"));
891
892 ShowUi("");
893 VerifyUi();
894
895 content::WebContents* web_contents =
896 browser()->tab_strip_model()->GetActiveWebContents();
897
898 extensions::ExtensionActionRunner* action_runner =
899 extensions::ExtensionActionRunner::GetForWebContents(web_contents);
900
901 EXPECT_TRUE(action_runner->WantsToRun(extension.get()));
902
903 TriggerSingleExtensionButton();
904
905 auto* const action_bubble =
906 BrowserView::GetBrowserViewForBrowser(browser())
907 ->toolbar()
908 ->extensions_container()
909 ->GetAnchoredWidgetForExtensionForTesting(extensions()[0]->id())
910 ->widget_delegate()
911 ->AsDialogDelegate();
912 ASSERT_TRUE(action_bubble);
913
914 const bool accept_reload_dialog = GetParam();
915 if (accept_reload_dialog) {
916 content::TestNavigationObserver observer(web_contents);
917 action_bubble->AcceptDialog();
918 EXPECT_TRUE(web_contents->IsLoading());
919 // Wait for reload to finish.
920 observer.WaitForNavigationFinished();
921 EXPECT_TRUE(observer.last_navigation_succeeded());
922 // After reload the extension should be allowed to run.
923 EXPECT_FALSE(action_runner->WantsToRun(extension.get()));
924 } else {
925 action_bubble->CancelDialog();
926 EXPECT_FALSE(web_contents->IsLoading());
927 EXPECT_TRUE(action_runner->WantsToRun(extension.get()));
928 }
929 }
930
931 INSTANTIATE_TEST_SUITE_P(AcceptDialog,
932 ActivateWithReloadExtensionsMenuBrowserTest,
933 testing::Values(true));
934
935 INSTANTIATE_TEST_SUITE_P(CancelDialog,
936 ActivateWithReloadExtensionsMenuBrowserTest,
937 testing::Values(false));
938