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