| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #import "chrome/browser/ui/cocoa/app_menu/app_menu_controller.h" |
| #import "chrome/browser/ui/cocoa/browser_window_controller.h" |
| #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h" |
| #import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h" |
| #import "chrome/browser/ui/cocoa/test/run_loop_testing.h" |
| #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" |
| #include "chrome/browser/ui/global_error/global_error.h" |
| #include "chrome/browser/ui/global_error/global_error_service.h" |
| #include "chrome/browser/ui/global_error/global_error_service_factory.h" |
| #include "chrome/browser/ui/toolbar/component_toolbar_actions_factory.h" |
| #include "chrome/browser/ui/toolbar/media_router_action.h" |
| #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h" |
| #include "chrome/browser/ui/toolbar/toolbar_actions_model.h" |
| #include "chrome/test/base/interactive_test_utils.h" |
| #include "extensions/common/extension_builder.h" |
| #include "ui/base/cocoa/cocoa_base_utils.h" |
| #import "ui/events/test/cocoa_test_event_utils.h" |
| |
| // A helper class to wait for a menu to open and close. |
| @interface MenuWatcher : NSObject |
| - (id)initWithController:(MenuController*)controller; |
| @property(nonatomic, assign) base::Closure openClosure; |
| @property(nonatomic, assign) base::Closure closeClosure; |
| @end |
| |
| namespace { |
| |
| using ActionType = extensions::ExtensionBuilder::ActionType; |
| |
| const int kMenuPadding = 26; |
| |
| // A simple error class that has a menu item. |
| class MenuError : public GlobalError { |
| public: |
| MenuError() {} |
| ~MenuError() override {} |
| |
| bool HasMenuItem() override { return true; } |
| int MenuItemCommandID() override { |
| // An arbitrary high id so that it's not taken. |
| return 65536; |
| } |
| base::string16 MenuItemLabel() override { |
| const char kErrorMessage[] = |
| "This is a really long error message that will cause the app menu " |
| "to have increased width"; |
| return base::ASCIIToUTF16(kErrorMessage); |
| } |
| void ExecuteMenuItem(Browser* browser) override {} |
| |
| bool HasBubbleView() override { return false; } |
| bool HasShownBubbleView() override { return false; } |
| void ShowBubbleView(Browser* browser) override {} |
| GlobalErrorBubbleViewBase* GetBubbleView() override { return nullptr; } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MenuError); |
| }; |
| |
| // Returns the center point for a particular |view|. |
| NSPoint GetCenterPoint(NSView* view) { |
| NSWindow* window = [view window]; |
| NSScreen* screen = [window screen]; |
| DCHECK(screen); |
| |
| // Converts the center position of the view into the coordinates accepted |
| // by ui_controls methods. |
| NSRect bounds = [view bounds]; |
| NSPoint center = NSMakePoint(NSMidX(bounds), NSMidY(bounds)); |
| center = [view convertPoint:center toView:nil]; |
| center = ui::ConvertPointFromWindowToScreen(window, center); |
| return NSMakePoint(center.x, [screen frame].size.height - center.y); |
| } |
| |
| // Moves the mouse (synchronously) to the center of the given |view|. |
| void MoveMouseToCenter(NSView* view) { |
| NSPoint centerPoint = GetCenterPoint(view); |
| base::RunLoop runLoop; |
| ui_controls::SendMouseMoveNotifyWhenDone( |
| centerPoint.x, centerPoint.y, runLoop.QuitClosure()); |
| runLoop.Run(); |
| } |
| |
| // Simulates a right-click on the action button in the overflow menu. |
| void DispatchClickOnOverflowedAction(BrowserActionButton* action_button) { |
| NSEvent* event = nil; // The event doesn't matter when sending directly. |
| [action_button rightMouseDown:event]; |
| } |
| |
| // ui_controls:: methods don't play nice when there is an open menu (like the |
| // app menu). Instead, ClickOnOverflowedAction() simulates a right click by |
| // feeding the event directly to the button. In regular interaction, this is |
| // dispatched after menuDidClose:. To simulate that, start the menu closing |
| // (with no action), but invoke the action on the button directly when the close |
| // is observed. |
| void ClickOnOverflowedAction( |
| base::scoped_nsobject<MenuWatcher> app_menu_watcher, |
| AppMenuController* app_menu_controller) { |
| // The app menu should start as open (since that's where the overflowed |
| // actions are). |
| EXPECT_TRUE([app_menu_controller isMenuOpen]); |
| |
| BrowserActionsController* overflow_controller = |
| [app_menu_controller browserActionsController]; |
| |
| ASSERT_TRUE(overflow_controller); |
| BrowserActionButton* action_button = [overflow_controller buttonWithIndex:0]; |
| |
| // The action should be attached to a superview. |
| EXPECT_TRUE([action_button superview]); |
| |
| base::Closure invoke_action = base::Bind(&DispatchClickOnOverflowedAction, |
| base::Unretained(action_button)); |
| [app_menu_watcher setCloseClosure:invoke_action]; |
| |
| // Close the app menu. |
| [app_menu_controller cancel]; |
| } |
| |
| } // namespace |
| |
| // A simple helper menu delegate that will keep track of if a menu is opened, |
| // and closes them immediately (which is useful because message loops with |
| // menus open in Cocoa don't play nicely with testing). |
| @interface MenuHelper : NSObject<NSMenuDelegate> { |
| // Whether or not a menu has been opened. This can be reset so the helper can |
| // be used multiple times. |
| BOOL menuOpened_; |
| |
| // The closure to run when the menu opens, if any. |
| base::Closure openClosure_; |
| |
| // A function to be called to verify state while the menu is open. |
| base::Closure verify_; |
| } |
| |
| @property(nonatomic, assign) BOOL menuOpened; |
| @property(nonatomic, assign) base::Closure openClosure; |
| @property(nonatomic, assign) base::Closure verify; |
| |
| @end |
| |
| @implementation MenuHelper |
| |
| - (void)menuWillOpen:(NSMenu*)menu { |
| menuOpened_ = YES; |
| if (!openClosure_.is_null()) |
| openClosure_.Run(); |
| if (!verify_.is_null()) |
| verify_.Run(); |
| [menu cancelTracking]; |
| } |
| |
| @synthesize menuOpened = menuOpened_; |
| @synthesize openClosure = openClosure_; |
| @synthesize verify = verify_; |
| |
| @end |
| |
| @implementation MenuWatcher { |
| // The MenuController for the menu this object is watching. |
| MenuController* menuController_; |
| |
| // The closure to run when the menu opens, if any. |
| base::Closure openClosure_; |
| |
| // The closure to run when the menu closes, if any. |
| base::Closure closeClosure_; |
| } |
| |
| @synthesize openClosure = openClosure_; |
| @synthesize closeClosure = closeClosure_; |
| |
| - (id)initWithController:(MenuController*)controller { |
| if (self = [super init]) { |
| menuController_ = controller; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(menuDidOpen:) |
| name:kMenuControllerMenuWillOpenNotification |
| object:menuController_]; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(menuDidClose:) |
| name:kMenuControllerMenuDidCloseNotification |
| object:menuController_]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| [super dealloc]; |
| } |
| |
| - (void)menuDidClose:(NSNotification*)notification { |
| if (!closeClosure_.is_null()) { |
| // Run |closeClosure_| synchronously since it may depend on objects that are |
| // torn down once the menu is closed and execution has returned to the main |
| // run loop. |
| closeClosure_.Run(); |
| closeClosure_.Reset(); |
| } |
| } |
| |
| - (void)menuDidOpen:(NSNotification*)notification { |
| if (!openClosure_.is_null()) { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::ResetAndReturn(&openClosure_)); |
| } |
| } |
| |
| @end |
| |
| class BrowserActionButtonUiTest : public ExtensionBrowserTest { |
| protected: |
| BrowserActionButtonUiTest() {} |
| ~BrowserActionButtonUiTest() override {} |
| |
| void SetUpOnMainThread() override { |
| ExtensionBrowserTest::SetUpOnMainThread(); |
| toolbarController_ = |
| [[BrowserWindowController |
| browserWindowControllerForWindow:browser()-> |
| window()->GetNativeWindow()] |
| toolbarController]; |
| ASSERT_TRUE(toolbarController_); |
| appMenuController_ = [toolbarController_ appMenuController]; |
| model_ = ToolbarActionsModel::Get(profile()); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionBrowserTest::SetUpCommandLine(command_line); |
| ToolbarActionsBar::disable_animations_for_testing_ = true; |
| } |
| |
| void TearDownOnMainThread() override { |
| ToolbarActionsBar::disable_animations_for_testing_ = false; |
| ExtensionBrowserTest::TearDownOnMainThread(); |
| } |
| |
| // Opens the app menu and the context menu of the overflowed action, and |
| // checks that the menus get opened/closed properly. |
| void OpenAppMenuAndActionContextMenu(MenuHelper* context_menu_helper) { |
| // Move the mouse over the app menu button. |
| MoveMouseToCenter(appMenuButton()); |
| |
| // No menu yet (on the browser action). |
| EXPECT_FALSE([context_menu_helper menuOpened]); |
| base::RunLoop run_loop; |
| base::scoped_nsobject<MenuWatcher> app_menu_watcher( |
| [[MenuWatcher alloc] initWithController:appMenuController()]); |
| |
| base::Closure close_with_action = |
| base::Bind(&ClickOnOverflowedAction, app_menu_watcher, |
| base::Unretained(appMenuController())); |
| [app_menu_watcher setOpenClosure:close_with_action]; |
| |
| // Quit the RunLoop below when the context menu opens. This dictates that |
| // the MenuHelper's verify action has also run. |
| [context_menu_helper setOpenClosure:run_loop.QuitClosure()]; |
| |
| // Show the app menu. |
| ui_controls::SendMouseEvents(ui_controls::LEFT, |
| ui_controls::DOWN | ui_controls::UP); |
| run_loop.Run(); |
| |
| // The menu opened on the main bar's action button rather than the |
| // overflow's since Cocoa does not support running a menu within a menu. |
| EXPECT_TRUE([context_menu_helper menuOpened]); |
| } |
| |
| ToolbarController* toolbarController() { return toolbarController_; } |
| AppMenuController* appMenuController() { return appMenuController_; } |
| ToolbarActionsModel* model() { return model_; } |
| NSView* appMenuButton() { return [toolbarController_ appMenuButton]; } |
| |
| private: |
| ToolbarController* toolbarController_ = nil; |
| AppMenuController* appMenuController_ = nil; |
| ToolbarActionsModel* model_ = nullptr; |
| |
| DISALLOW_COPY_AND_ASSIGN(BrowserActionButtonUiTest); |
| }; |
| |
| // Verifies that the action is "popped out" of overflow; that is, it is visible |
| // on the main bar, and is set as the popped out action on the controlling |
| // ToolbarActionsBar. |
| void CheckActionIsPoppedOut(BrowserActionsController* actionsController, |
| BrowserActionButton* actionButton) { |
| EXPECT_EQ([actionsController containerView], [actionButton superview]); |
| EXPECT_EQ([actionButton viewController], |
| [actionsController toolbarActionsBar]->popped_out_action()); |
| // Since the button is popped out for a popup or context menu, it should be |
| // highlighted. |
| EXPECT_TRUE([actionButton isHighlighted]); |
| } |
| |
| // Test that opening a context menu works for both actions on the main bar and |
| // actions in the overflow menu. |
| IN_PROC_BROWSER_TEST_F(BrowserActionButtonUiTest, |
| ContextMenusOnMainAndOverflow) { |
| // Add an extension with a browser action. |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder("browser_action") |
| .SetAction(ActionType::BROWSER_ACTION) |
| .Build(); |
| extension_service()->AddExtension(extension.get()); |
| ASSERT_EQ(1u, model()->toolbar_items().size()); |
| |
| BrowserActionsController* actionsController = |
| [toolbarController() browserActionsController]; |
| BrowserActionButton* actionButton = [actionsController buttonWithIndex:0]; |
| ASSERT_TRUE(actionButton); |
| |
| // Stub out the action button's normal context menu with a fake one so we |
| // can track when it opens. |
| base::scoped_nsobject<NSMenu> testContextMenu( |
| [[NSMenu alloc] initWithTitle:@""]); |
| base::scoped_nsobject<MenuHelper> menuHelper([[MenuHelper alloc] init]); |
| [testContextMenu setDelegate:menuHelper.get()]; |
| [actionButton setTestContextMenu:testContextMenu.get()]; |
| |
| // Right now, the action button should be visible (attached to a superview). |
| EXPECT_TRUE([actionButton superview]); |
| |
| // Move the mouse to the center of the action button, preparing to click. |
| MoveMouseToCenter(actionButton); |
| |
| { |
| // No menu should yet be open. |
| EXPECT_FALSE([menuHelper menuOpened]); |
| base::RunLoop runLoop; |
| ui_controls::SendMouseEventsNotifyWhenDone( |
| ui_controls::RIGHT, |
| ui_controls::DOWN | ui_controls::UP, |
| runLoop.QuitClosure()); |
| runLoop.Run(); |
| // The menu should have opened from the click. |
| EXPECT_TRUE([menuHelper menuOpened]); |
| EXPECT_FALSE([actionButton isHighlighted]); |
| } |
| |
| // Reset the menu helper so we can use it again. |
| [menuHelper setMenuOpened:NO]; |
| [menuHelper setVerify:base::Bind( |
| CheckActionIsPoppedOut, actionsController, actionButton)]; |
| |
| // Shrink the visible count to be 0. This should hide the action button. |
| model()->SetVisibleIconCount(0); |
| EXPECT_EQ(nil, [actionButton superview]); |
| |
| OpenAppMenuAndActionContextMenu(menuHelper.get()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowserActionButtonUiTest, |
| MediaRouterActionContextMenuInOverflow) { |
| model()->AddComponentAction( |
| ComponentToolbarActionsFactory::kMediaRouterActionId); |
| ASSERT_EQ(1u, model()->toolbar_items().size()); |
| |
| BrowserActionButton* actionButton = |
| [[toolbarController() browserActionsController] buttonWithIndex:0]; |
| ASSERT_TRUE(actionButton); |
| |
| // Stub out the action button's normal context menu with a fake one so we |
| // can track when it opens. |
| base::scoped_nsobject<NSMenu> testContextMenu( |
| [[NSMenu alloc] initWithTitle:@""]); |
| base::scoped_nsobject<MenuHelper> menuHelper([[MenuHelper alloc] init]); |
| [testContextMenu setDelegate:menuHelper.get()]; |
| [actionButton setTestContextMenu:testContextMenu.get()]; |
| |
| model()->SetActionVisibility( |
| ComponentToolbarActionsFactory::kMediaRouterActionId, false); |
| |
| OpenAppMenuAndActionContextMenu(menuHelper.get()); |
| |
| ToolbarActionsBar* actionsBar = |
| [[toolbarController() browserActionsController] toolbarActionsBar]; |
| // The action should be back in the overflow. |
| EXPECT_FALSE( |
| actionsBar->IsActionVisibleOnMainBar(actionsBar->GetActions()[0])); |
| } |
| |
| // Checks the layout of the overflow bar in the app menu. |
| void CheckAppMenuLayout(ToolbarController* toolbarController, |
| int overflowStartIndex, |
| const std::string& error_message, |
| const base::Closure& closure) { |
| AppMenuController* appMenuController = |
| [toolbarController appMenuController]; |
| // The app menu should start as open (since that's where the overflowed |
| // actions are). |
| EXPECT_TRUE([appMenuController isMenuOpen]) << error_message; |
| BrowserActionsController* overflowController = |
| [appMenuController browserActionsController]; |
| ASSERT_TRUE(overflowController) << error_message; |
| |
| ToolbarActionsBar* overflowBar = [overflowController toolbarActionsBar]; |
| BrowserActionsContainerView* overflowContainer = |
| [overflowController containerView]; |
| NSMenu* menu = [appMenuController menu]; |
| |
| // The overflow container should be within the bounds of the app menu, as |
| // should its parents. |
| int menu_width = [menu size].width; |
| NSRect frame = [overflowContainer frame]; |
| // The container itself should be indented in the menu. |
| EXPECT_GT(NSMinX(frame), 0) << error_message; |
| // Hierarchy: The overflow container is owned by two different views in the |
| // app menu. Each superview should start at 0 in the x-axis. |
| EXPECT_EQ(0, NSMinX([[overflowContainer superview] frame])) << error_message; |
| EXPECT_EQ(0, NSMinX([[[overflowContainer superview] superview] frame])) << |
| error_message; |
| // The overflow container should fully fit in the app menu, including the |
| // space taken away for padding, and should have its desired size. |
| EXPECT_LE(NSWidth(frame), menu_width - kMenuPadding) << error_message; |
| EXPECT_EQ(NSWidth(frame), overflowBar->GetFullSize().width()) |
| << error_message; |
| |
| // Every button that has an index lower than the overflow start index (i.e., |
| // every button on the main toolbar) should not be attached to a view. |
| for (int i = 0; i < overflowStartIndex; ++i) { |
| BrowserActionButton* button = [overflowController buttonWithIndex:i]; |
| EXPECT_FALSE([button superview]) << error_message; |
| EXPECT_GE(0, NSMaxX([button frame])) << error_message; |
| } |
| |
| // Every other button should be attached to a view, and should be at the |
| // proper bounds. Calculating each button's proper bounds here would just be |
| // a duplication of the logic in the method, but we can test that each button |
| // a) is within the overflow container's bounds, and |
| // b) doesn't intersect with another button. |
| // If both of those are true, then we're probably good. |
| for (NSUInteger i = overflowStartIndex; |
| i < [overflowController buttonCount]; ++i) { |
| BrowserActionButton* button = [overflowController buttonWithIndex:i]; |
| EXPECT_TRUE([button superview]) << error_message; |
| EXPECT_TRUE(NSContainsRect([overflowContainer bounds], [button frame])) << |
| error_message; |
| for (NSUInteger j = 0; j < i; ++j) { |
| EXPECT_FALSE( |
| NSContainsRect([[overflowController buttonWithIndex:j] frame], |
| [button frame])) << error_message; |
| } |
| } |
| |
| // Close the app menu. |
| [appMenuController cancel]; |
| EXPECT_FALSE([appMenuController isMenuOpen]) << error_message; |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, closure); |
| } |
| |
| // Tests the layout of the overflow container in the app menu. |
| // Disabled due to crashing flakily, see crbug.com/602203. |
| IN_PROC_BROWSER_TEST_F(BrowserActionButtonUiTest, |
| DISABLED_TestOverflowContainerLayout) { |
| // Add a bunch of extensions - enough to trigger multiple rows in the overflow |
| // menu. |
| const int kNumExtensions = 12; |
| for (int i = 0; i < kNumExtensions; ++i) { |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder(base::StringPrintf("extension%d", i)) |
| .SetAction(ActionType::BROWSER_ACTION) |
| .Build(); |
| extension_service()->AddExtension(extension.get()); |
| } |
| ASSERT_EQ(kNumExtensions, static_cast<int>(model()->toolbar_items().size())); |
| |
| // A helper function to open the app menu and call the check function. |
| auto resizeAndActivateAppMenu = [this](int visible_count, |
| const std::string& error_message) { |
| model()->SetVisibleIconCount(kNumExtensions - visible_count); |
| MoveMouseToCenter(appMenuButton()); |
| |
| { |
| base::RunLoop runLoop; |
| // Click on the app menu, and pass in a callback to continue the test in |
| // CheckAppMenuLayout (due to the blocking nature of Cocoa menus, |
| // passing in runLoop.QuitClosure() is not sufficient here.) |
| ui_controls::SendMouseEventsNotifyWhenDone( |
| ui_controls::LEFT, ui_controls::DOWN | ui_controls::UP, |
| base::Bind(&CheckAppMenuLayout, |
| base::Unretained(toolbarController()), |
| kNumExtensions - visible_count, |
| error_message, |
| runLoop.QuitClosure())); |
| runLoop.Run(); |
| } |
| }; |
| |
| // Test the layout with gradually more extensions hidden. |
| for (int i = 1; i <= kNumExtensions; ++i) |
| resizeAndActivateAppMenu(i, base::StringPrintf("Normal: %d", i)); |
| |
| // Adding a global error adjusts the app menu size, and has been known to mess |
| // up the overflow container's bounds (crbug.com/511326). |
| GlobalErrorService* error_service = |
| GlobalErrorServiceFactory::GetForProfile(profile()); |
| error_service->AddGlobalError(base::MakeUnique<MenuError>()); |
| |
| // It's probably excessive to test every level of the overflow here. Test |
| // having all actions overflowed, some actions overflowed, and one action |
| // overflowed. |
| resizeAndActivateAppMenu(kNumExtensions, "GlobalError Full"); |
| resizeAndActivateAppMenu(kNumExtensions / 2, "GlobalError Half"); |
| resizeAndActivateAppMenu(1, "GlobalError One"); |
| } |
| |
| void AddExtensionWithMenuOpen(ToolbarController* toolbarController, |
| ExtensionService* extensionService, |
| const base::Closure& closure) { |
| AppMenuController* appMenuController = |
| [toolbarController appMenuController]; |
| EXPECT_TRUE([appMenuController isMenuOpen]); |
| |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder("extension") |
| .SetAction(ActionType::BROWSER_ACTION) |
| .Build(); |
| extensionService->AddExtension(extension.get()); |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| // Close the app menu. |
| [appMenuController cancel]; |
| EXPECT_FALSE([appMenuController isMenuOpen]); |
| |
| closure.Run(); |
| } |
| |
| // Test adding an extension while the app menu is open. Regression test for |
| // crbug.com/561237. |
| IN_PROC_BROWSER_TEST_F(BrowserActionButtonUiTest, |
| AddExtensionWithMenuOpen) { |
| // Add an extension to ensure the overflow menu is present. |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder("original extension") |
| .SetAction(ActionType::BROWSER_ACTION) |
| .Build(); |
| extension_service()->AddExtension(extension.get()); |
| ASSERT_EQ(1, static_cast<int>(model()->toolbar_items().size())); |
| model()->SetVisibleIconCount(0); |
| |
| MoveMouseToCenter(appMenuButton()); |
| |
| base::RunLoop runLoop; |
| // Click on the app menu, and pass in a callback to continue the test in |
| // AddExtensionWithMenuOpen (due to the blocking nature of Cocoa menus, |
| // passing in runLoop.QuitClosure() is not sufficient here.) |
| base::scoped_nsobject<MenuWatcher> menuWatcher( |
| [[MenuWatcher alloc] initWithController:appMenuController()]); |
| [menuWatcher setOpenClosure: |
| base::Bind(&AddExtensionWithMenuOpen, |
| base::Unretained(toolbarController()), extension_service(), |
| runLoop.QuitClosure())]; |
| ui_controls::SendMouseEvents(ui_controls::LEFT, |
| ui_controls::DOWN | ui_controls::UP); |
| runLoop.Run(); |
| } |
| |
| // Test that activating an action that doesn't want to run on the page via the |
| // mouse and the keyboard works. |
| IN_PROC_BROWSER_TEST_F(BrowserActionButtonUiTest, |
| OpenMenuOnDisabledActionWithMouseOrKeyboard) { |
| // Add an extension with a page action. |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder("page action") |
| .SetAction(ActionType::PAGE_ACTION) |
| .Build(); |
| extension_service()->AddExtension(extension.get()); |
| ASSERT_EQ(1u, model()->toolbar_items().size()); |
| |
| BrowserActionsController* actionsController = |
| [toolbarController() browserActionsController]; |
| BrowserActionButton* actionButton = [actionsController buttonWithIndex:0]; |
| ASSERT_TRUE(actionButton); |
| |
| // Stub out the action button's normal context menu with a fake one so we |
| // can track when it opens. |
| base::scoped_nsobject<NSMenu> testContextMenu( |
| [[NSMenu alloc] initWithTitle:@""]); |
| base::scoped_nsobject<MenuHelper> menuHelper([[MenuHelper alloc] init]); |
| [testContextMenu setDelegate:menuHelper.get()]; |
| [actionButton setTestContextMenu:testContextMenu.get()]; |
| |
| // The button should be attached. |
| EXPECT_TRUE([actionButton superview]); |
| |
| // Move the mouse and click on the button. The menu should open. |
| MoveMouseToCenter(actionButton); |
| { |
| EXPECT_FALSE([menuHelper menuOpened]); |
| base::RunLoop runLoop; |
| ui_controls::SendMouseEventsNotifyWhenDone( |
| ui_controls::LEFT, ui_controls::DOWN | ui_controls::UP, |
| runLoop.QuitClosure()); |
| runLoop.Run(); |
| EXPECT_TRUE([menuHelper menuOpened]); |
| } |
| |
| // Reset the menu helper so we can use it again. |
| [menuHelper setMenuOpened:NO]; |
| |
| // Send the 'space' key to the button with it as the first responder. The menu |
| // should open. |
| [[actionButton window] makeFirstResponder:actionButton]; |
| EXPECT_TRUE([[actionButton window] firstResponder] == actionButton); |
| { |
| EXPECT_FALSE([menuHelper menuOpened]); |
| base::RunLoop runLoop; |
| ui_controls::SendKeyPressNotifyWhenDone([actionButton window], |
| ui::VKEY_SPACE, false, false, false, |
| false, runLoop.QuitClosure()); |
| runLoop.Run(); |
| EXPECT_TRUE([menuHelper menuOpened]); |
| } |
| } |
| |
| void CloseAppMenu(AppMenuController* appMenuController, |
| const base::Closure& quitClosure) { |
| EXPECT_TRUE([appMenuController isMenuOpen]); |
| [appMenuController cancel]; |
| quitClosure.Run(); |
| } |
| |
| // Tests opening the app menu with an overflow section needed, then closing it, |
| // removing the need for the overflow section, and re-opening it. |
| // Regression test for crbug.com/603241. |
| IN_PROC_BROWSER_TEST_F(BrowserActionButtonUiTest, |
| TestReopeningAppMenuWithOverflowNotNeeded) { |
| // Add an extension with a browser action and overflow it in the menu. |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder("browser_action") |
| .SetAction(ActionType::BROWSER_ACTION) |
| .Build(); |
| extension_service()->AddExtension(extension.get()); |
| ASSERT_EQ(1u, model()->toolbar_items().size()); |
| model()->SetVisibleIconCount(0); |
| |
| // Move the mouse over the app menu button. |
| MoveMouseToCenter(appMenuButton()); |
| |
| auto openAndCloseAppMenu = [](AppMenuController* controller) { |
| base::RunLoop runLoop; |
| base::scoped_nsobject<MenuWatcher> menuWatcher( |
| [[MenuWatcher alloc] initWithController:controller]); |
| [menuWatcher setOpenClosure: |
| base::Bind(&CloseAppMenu, |
| base::Unretained(controller), |
| runLoop.QuitClosure())]; |
| ui_controls::SendMouseEvents(ui_controls::LEFT, |
| ui_controls::DOWN | ui_controls::UP); |
| runLoop.Run(); |
| }; |
| openAndCloseAppMenu(appMenuController()); |
| |
| // Move the extension back to the main bar, so an overflow bar is no longer |
| // needed. Then open and close the app menu a couple times. |
| // This tests that the menu properly cleans up after itself when an overflow |
| // was present, and is no longer (fix for crbug.com/603241). |
| model()->SetVisibleIconCount(1); |
| openAndCloseAppMenu(appMenuController()); |
| openAndCloseAppMenu(appMenuController()); |
| } |