1 // Copyright 2018 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 #include "ui/views/controls/menu/menu_controller.h"
5 
6 #include "base/macros.h"
7 #include "base/run_loop.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "build/build_config.h"
10 #include "chrome/browser/ui/browser_commands.h"
11 #include "chrome/browser/ui/views/native_widget_factory.h"
12 #include "chrome/test/base/in_process_browser_test.h"
13 #include "chrome/test/base/interactive_test_utils.h"
14 #include "chrome/test/base/test_browser_window.h"
15 #include "content/public/test/browser_test.h"
16 #include "ui/accessibility/ax_node_data.h"
17 #include "ui/base/test/ui_controls.h"
18 #include "ui/gfx/geometry/point.h"
19 #include "ui/views/accessibility/view_accessibility.h"
20 #include "ui/views/controls/button/button.h"
21 #include "ui/views/controls/menu/menu_item_view.h"
22 #include "ui/views/controls/menu/submenu_view.h"
23 #include "ui/views/focus/focus_manager.h"
24 #include "ui/views/test/ax_event_counter.h"
25 #include "ui/views/test/widget_test.h"
26 #include "ui/views/widget/widget.h"
27 
28 #if !defined(OS_CHROMEOS)
29 #include "ui/accessibility/platform/ax_platform_node.h"
30 #endif
31 
32 namespace views {
33 namespace test {
34 
35 namespace {
36 
37 class TestButton : public Button {
38  public:
TestButton()39   TestButton() : Button(Button::PressedCallback()) {}
40   TestButton(const TestButton&) = delete;
41   TestButton& operator=(const TestButton&) = delete;
42   ~TestButton() override = default;
43 };
44 
45 }  // namespace
46 
47 class MenuControllerUITest : public InProcessBrowserTest {
48  public:
MenuControllerUITest()49   MenuControllerUITest() {}
50 
51   // This method creates a MenuRunner, MenuItemView, etc, adds two menu
52   // items, shows the menu so that it can calculate the position of the first
53   // menu item and move the mouse there, and closes the menu.
SetupMenu(Widget * widget)54   void SetupMenu(Widget* widget) {
55     menu_delegate_ = std::make_unique<MenuDelegate>();
56     MenuItemView* menu_item = new MenuItemView(menu_delegate_.get());
57     menu_runner_ = std::make_unique<MenuRunner>(
58         +menu_item, views::MenuRunner::CONTEXT_MENU);
59     first_item_ = menu_item->AppendMenuItem(1, base::ASCIIToUTF16("One"));
60     menu_item->AppendMenuItem(2, base::ASCIIToUTF16("Two"));
61     // Run the menu, so that the menu item size will be calculated.
62     menu_runner_->RunMenuAt(widget, nullptr, gfx::Rect(),
63                             views::MenuAnchorPosition::kTopLeft,
64                             ui::MENU_SOURCE_NONE);
65     RunPendingMessages();
66     // Figure out the middle of the first menu item.
67     mouse_pos_.set_x(first_item_->width() / 2);
68     mouse_pos_.set_y(first_item_->height() / 2);
69     View::ConvertPointToScreen(
70         menu_item->GetSubmenu()->GetWidget()->GetRootView(), &mouse_pos_);
71     // Move the mouse so that it's where the menu will be shown.
72     base::RunLoop run_loop;
73     ui_controls::SendMouseMoveNotifyWhenDone(mouse_pos_.x(), mouse_pos_.y(),
74                                              run_loop.QuitClosure());
75     run_loop.Run();
76     EXPECT_TRUE(first_item_->IsSelected());
77     ui::AXNodeData item_node_data;
78     first_item_->GetViewAccessibility().GetAccessibleNodeData(&item_node_data);
79     EXPECT_EQ(item_node_data.role, ax::mojom::Role::kMenuItem);
80 
81 #if !defined(OS_CHROMEOS)  // ChromeOS does not use popup focus override.
82     EXPECT_TRUE(first_item_->GetViewAccessibility().IsFocusedForTesting());
83 #endif
84     ui::AXNodeData menu_node_data;
85     menu_item->GetSubmenu()->GetViewAccessibility().GetAccessibleNodeData(
86         &menu_node_data);
87     EXPECT_EQ(menu_node_data.role, ax::mojom::Role::kMenu);
88     menu_runner_->Cancel();
89     RunPendingMessages();
90   }
91 
RunPendingMessages()92   void RunPendingMessages() {
93     base::RunLoop run_loop;
94     run_loop.RunUntilIdle();
95   }
96 
97  protected:
98   MenuItemView* first_item_ = nullptr;
99   std::unique_ptr<MenuRunner> menu_runner_;
100   std::unique_ptr<MenuDelegate> menu_delegate_;
101   // Middle of first menu item.
102   gfx::Point mouse_pos_;
103 
104  private:
105   DISALLOW_COPY_AND_ASSIGN(MenuControllerUITest);
106 };
107 
IN_PROC_BROWSER_TEST_F(MenuControllerUITest,TestMouseOverShownMenu)108 IN_PROC_BROWSER_TEST_F(MenuControllerUITest, TestMouseOverShownMenu) {
109 #if !defined(OS_CHROMEOS)
110   ui::AXPlatformNode::NotifyAddAXModeFlags(ui::kAXModeComplete);
111 #endif
112 
113   // Create a parent widget.
114   Widget* widget = new views::Widget;
115   Widget::InitParams params(Widget::InitParams::TYPE_WINDOW);
116   params.bounds = {0, 0, 200, 200};
117 #if !defined(OS_CHROMEOS) && !defined(OS_MAC)
118   params.native_widget = CreateNativeWidget(
119       NativeWidgetType::DESKTOP_NATIVE_WIDGET_AURA, &params, widget);
120 #endif
121   widget->Init(std::move(params));
122   widget->Show();
123   views::test::WidgetActivationWaiter waiter(widget, true);
124   widget->Activate();
125   waiter.Wait();
126 
127   // Create a focused test button, used to assert that it has accessibility
128   // focus before and after menu item is active, but not during.
129   TestButton button;
130   widget->GetContentsView()->AddChildView(&button);
131   FocusManager* focus_manager = widget->GetFocusManager();
132   focus_manager->SetFocusedView(&button);
133   EXPECT_TRUE(button.HasFocus());
134   EXPECT_TRUE(button.GetViewAccessibility().IsFocusedForTesting());
135 
136   // SetupMenu leaves the mouse position where the first menu item will be
137   // when we run the menu.
138   AXEventCounter ax_counter(views::AXEventManager::Get());
139   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 0);
140   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 0);
141   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 0);
142   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 0);
143   SetupMenu(widget);
144 
145   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 1);
146   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 1);
147   // SetupMenu creates, opens and closes a popup menu, so there will be a
148   // a menu popup end. There is also a menu end since it's the last menu.
149   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 1);
150   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 1);
151   EXPECT_FALSE(first_item_->IsSelected());
152 #if !defined(OS_CHROMEOS)  // ChromeOS does not use popup focus override.
153   EXPECT_FALSE(first_item_->GetViewAccessibility().IsFocusedForTesting());
154 #endif
155   menu_runner_->RunMenuAt(widget, nullptr, gfx::Rect(),
156                           views::MenuAnchorPosition::kTopLeft,
157                           ui::MENU_SOURCE_NONE);
158   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 2);
159   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 2);
160   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 1);
161   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 1);
162   EXPECT_FALSE(first_item_->IsSelected());
163   // One or two mouse events are posted by the menu being shown.
164   // Process event(s), and check what's selected in the menu.
165   RunPendingMessages();
166   EXPECT_FALSE(first_item_->IsSelected());
167 #if !defined(OS_CHROMEOS)  // ChromeOS does not use popup focus override.
168   EXPECT_FALSE(first_item_->GetViewAccessibility().IsFocusedForTesting());
169   EXPECT_TRUE(button.GetViewAccessibility().IsFocusedForTesting());
170 #endif
171   // Move mouse one pixel to left and verify that the first menu item
172   // is selected.
173   mouse_pos_.Offset(-1, 0);
174   base::RunLoop run_loop2;
175   ui_controls::SendMouseMoveNotifyWhenDone(mouse_pos_.x(), mouse_pos_.y(),
176                                            run_loop2.QuitClosure());
177   run_loop2.Run();
178   EXPECT_TRUE(first_item_->IsSelected());
179 #if !defined(OS_CHROMEOS)  // ChromeOS does not use popup focus override.
180   EXPECT_TRUE(first_item_->GetViewAccessibility().IsFocusedForTesting());
181   EXPECT_FALSE(button.GetViewAccessibility().IsFocusedForTesting());
182 #endif
183   menu_runner_->Cancel();
184 #if !defined(OS_CHROMEOS)  // ChromeOS does not use popup focus override.
185   EXPECT_FALSE(first_item_->GetViewAccessibility().IsFocusedForTesting());
186   EXPECT_TRUE(button.GetViewAccessibility().IsFocusedForTesting());
187 #endif
188   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 2);
189   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 2);
190   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 2);
191   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 2);
192   widget->Close();
193 }
194 
195 // This test creates a menu without a parent widget, and tests that it
196 // can receive keyboard events.
197 // TODO(davidbienvenu): If possible, get test working for linux and
198 // mac. Only status_icon_win runs a menu with a null parent widget
199 // currently.
200 #ifdef OS_WIN
IN_PROC_BROWSER_TEST_F(MenuControllerUITest,FocusOnOrphanMenu)201 IN_PROC_BROWSER_TEST_F(MenuControllerUITest, FocusOnOrphanMenu) {
202   // Going into full screen mode prevents pre-test focus and mouse position
203   // state from affecting test, and helps ui_controls function correctly.
204   chrome::ToggleFullscreenMode(browser());
205   ui::AXPlatformNode::NotifyAddAXModeFlags(ui::kAXModeComplete);
206   MenuDelegate menu_delegate;
207   MenuItemView* menu_item = new MenuItemView(&menu_delegate);
208   AXEventCounter ax_counter(views::AXEventManager::Get());
209   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 0);
210   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 0);
211   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 0);
212   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 0);
213   std::unique_ptr<MenuRunner> menu_runner(
214       std::make_unique<MenuRunner>(menu_item, views::MenuRunner::CONTEXT_MENU));
215   MenuItemView* first_item =
216       menu_item->AppendMenuItem(1, base::ASCIIToUTF16("One"));
217   menu_item->AppendMenuItem(2, base::ASCIIToUTF16("Two"));
218   menu_runner->RunMenuAt(nullptr, nullptr, gfx::Rect(),
219                          views::MenuAnchorPosition::kTopLeft,
220                          ui::MENU_SOURCE_NONE);
221   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 1);
222   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 1);
223   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 0);
224   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 0);
225   base::RunLoop loop;
226   // SendKeyPress fails if the window doesn't have focus.
227   ASSERT_TRUE(ui_controls::SendKeyPressNotifyWhenDone(
228       menu_item->GetSubmenu()->GetWidget()->GetNativeWindow(), ui::VKEY_DOWN,
229       false, false, false, false, loop.QuitClosure()));
230   loop.Run();
231   EXPECT_TRUE(first_item->IsSelected());
232   EXPECT_TRUE(first_item->GetViewAccessibility().IsFocusedForTesting());
233   menu_runner->Cancel();
234   EXPECT_FALSE(first_item->GetViewAccessibility().IsFocusedForTesting());
235   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuStart), 1);
236   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupStart), 1);
237   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuPopupEnd), 1);
238   EXPECT_EQ(ax_counter.GetCount(ax::mojom::Event::kMenuEnd), 1);
239 }
240 #endif  // OS_WIN
241 
242 }  // namespace test
243 }  // namespace views
244