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, ¶ms, 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