1 // Copyright (c) 2012 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 // This functionality currently works on Windows and on Linux when 6 // toolkit_views is defined (i.e. for Chrome OS). It's not needed 7 // on the Mac, and it's not yet implemented on Linux. 8 9 #include "base/memory/weak_ptr.h" 10 #include "base/message_loop/message_loop.h" 11 #include "base/strings/string_util.h" 12 #include "base/time/time.h" 13 #include "chrome/browser/chrome_notification_types.h" 14 #include "chrome/browser/ui/browser.h" 15 #include "chrome/browser/ui/browser_window.h" 16 #include "chrome/browser/ui/tabs/tab_strip_model.h" 17 #include "chrome/browser/ui/views/frame/browser_view.h" 18 #include "chrome/browser/ui/views/toolbar/toolbar_view.h" 19 #include "chrome/test/base/in_process_browser_test.h" 20 #include "chrome/test/base/interactive_test_utils.h" 21 #include "chrome/test/base/ui_test_utils.h" 22 #include "ui/base/test/ui_controls.h" 23 #include "ui/events/event_constants.h" 24 #include "ui/events/keycodes/keyboard_codes.h" 25 #include "ui/views/controls/menu/menu_listener.h" 26 #include "ui/views/focus/focus_manager.h" 27 #include "ui/views/view.h" 28 #include "ui/views/widget/widget.h" 29 30 namespace { 31 32 // An async version of SendKeyPressSync since we don't get notified when a 33 // menu is showing. 34 void SendKeyPress(Browser* browser, ui::KeyboardCode key) { 35 ASSERT_TRUE(ui_controls::SendKeyPress( 36 browser->window()->GetNativeWindow(), key, false, false, false, false)); 37 } 38 39 // Helper class that waits until the focus has changed to a view other 40 // than the one with the provided view id. 41 class ViewFocusChangeWaiter : public views::FocusChangeListener { 42 public: 43 ViewFocusChangeWaiter(views::FocusManager* focus_manager, 44 int previous_view_id) 45 : focus_manager_(focus_manager), 46 previous_view_id_(previous_view_id), 47 weak_factory_(this) { 48 focus_manager_->AddFocusChangeListener(this); 49 // Call the focus change notification once in case the focus has 50 // already changed. 51 OnWillChangeFocus(NULL, focus_manager_->GetFocusedView()); 52 } 53 54 virtual ~ViewFocusChangeWaiter() { 55 focus_manager_->RemoveFocusChangeListener(this); 56 } 57 58 void Wait() { 59 content::RunMessageLoop(); 60 } 61 62 private: 63 // Inherited from FocusChangeListener 64 virtual void OnWillChangeFocus(views::View* focused_before, 65 views::View* focused_now) OVERRIDE { 66 } 67 68 virtual void OnDidChangeFocus(views::View* focused_before, 69 views::View* focused_now) OVERRIDE { 70 if (focused_now && focused_now->id() != previous_view_id_) { 71 base::MessageLoop::current()->PostTask(FROM_HERE, 72 base::MessageLoop::QuitClosure()); 73 } 74 } 75 76 views::FocusManager* focus_manager_; 77 int previous_view_id_; 78 base::WeakPtrFactory<ViewFocusChangeWaiter> weak_factory_; 79 80 DISALLOW_COPY_AND_ASSIGN(ViewFocusChangeWaiter); 81 }; 82 83 class SendKeysMenuListener : public views::MenuListener { 84 public: 85 SendKeysMenuListener(ToolbarView* toolbar_view, 86 Browser* browser, 87 bool test_dismiss_menu) 88 : toolbar_view_(toolbar_view), browser_(browser), menu_open_count_(0), 89 test_dismiss_menu_(test_dismiss_menu) { 90 toolbar_view_->AddMenuListener(this); 91 } 92 93 virtual ~SendKeysMenuListener() { 94 if (test_dismiss_menu_) 95 toolbar_view_->RemoveMenuListener(this); 96 } 97 98 int menu_open_count() const { 99 return menu_open_count_; 100 } 101 102 private: 103 // Overridden from views::MenuListener: 104 virtual void OnMenuOpened() OVERRIDE { 105 menu_open_count_++; 106 if (!test_dismiss_menu_) { 107 toolbar_view_->RemoveMenuListener(this); 108 // Press DOWN to select the first item, then RETURN to select it. 109 SendKeyPress(browser_, ui::VKEY_DOWN); 110 SendKeyPress(browser_, ui::VKEY_RETURN); 111 } else { 112 SendKeyPress(browser_, ui::VKEY_ESCAPE); 113 base::MessageLoop::current()->PostDelayedTask( 114 FROM_HERE, 115 base::MessageLoop::QuitClosure(), 116 base::TimeDelta::FromMilliseconds(200)); 117 } 118 } 119 120 ToolbarView* toolbar_view_; 121 Browser* browser_; 122 // Keeps track of the number of times the menu was opened. 123 int menu_open_count_; 124 // If this is set then on receiving a notification that the menu was opened 125 // we dismiss it by sending the ESC key. 126 bool test_dismiss_menu_; 127 128 DISALLOW_COPY_AND_ASSIGN(SendKeysMenuListener); 129 }; 130 131 class KeyboardAccessTest : public InProcessBrowserTest { 132 public: 133 KeyboardAccessTest() {} 134 135 // Use the keyboard to select "New Tab" from the app menu. 136 // This test depends on the fact that there is one menu and that 137 // New Tab is the first item in the menu. If the menus change, 138 // this test will need to be changed to reflect that. 139 // 140 // If alternate_key_sequence is true, use "Alt" instead of "F10" to 141 // open the menu bar, and "Down" instead of "Enter" to open a menu. 142 // If focus_omnibox is true then the test on startup sets focus to the 143 // omnibox. 144 void TestMenuKeyboardAccess(bool alternate_key_sequence, 145 bool shift, 146 bool focus_omnibox); 147 148 int GetFocusedViewID() { 149 gfx::NativeWindow window = browser()->window()->GetNativeWindow(); 150 views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); 151 const views::FocusManager* focus_manager = widget->GetFocusManager(); 152 const views::View* focused_view = focus_manager->GetFocusedView(); 153 return focused_view ? focused_view->id() : -1; 154 } 155 156 void WaitForFocusedViewIDToChange(int original_view_id) { 157 if (GetFocusedViewID() != original_view_id) 158 return; 159 gfx::NativeWindow window = browser()->window()->GetNativeWindow(); 160 views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); 161 views::FocusManager* focus_manager = widget->GetFocusManager(); 162 ViewFocusChangeWaiter waiter(focus_manager, original_view_id); 163 waiter.Wait(); 164 } 165 166 #if defined(OS_WIN) 167 // Opens the system menu on Windows with the Alt Space combination and selects 168 // the New Tab option from the menu. 169 void TestSystemMenuWithKeyboard(); 170 #endif 171 172 #if defined(USE_AURA) 173 // Uses the keyboard to select the wrench menu i.e. with the F10 key. 174 // It verifies that the menu when dismissed by sending the ESC key it does 175 // not display twice. 176 void TestMenuKeyboardAccessAndDismiss(); 177 #endif 178 179 DISALLOW_COPY_AND_ASSIGN(KeyboardAccessTest); 180 }; 181 182 void KeyboardAccessTest::TestMenuKeyboardAccess(bool alternate_key_sequence, 183 bool shift, 184 bool focus_omnibox) { 185 // Navigate to a page in the first tab, which makes sure that focus is 186 // set to the browser window. 187 ui_test_utils::NavigateToURL(browser(), GURL("about:")); 188 189 // The initial tab index should be 0. 190 ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); 191 192 ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser())); 193 194 // Get the focused view ID, then press a key to activate the 195 // page menu, then wait until the focused view changes. 196 int original_view_id = GetFocusedViewID(); 197 198 content::WindowedNotificationObserver new_tab_observer( 199 chrome::NOTIFICATION_TAB_ADDED, 200 content::Source<content::WebContentsDelegate>(browser())); 201 202 BrowserView* browser_view = reinterpret_cast<BrowserView*>( 203 browser()->window()); 204 ToolbarView* toolbar_view = browser_view->GetToolbarView(); 205 SendKeysMenuListener menu_listener(toolbar_view, browser(), false); 206 207 if (focus_omnibox) 208 browser()->window()->GetLocationBar()->FocusLocation(false); 209 210 #if defined(OS_CHROMEOS) 211 // Chrome OS doesn't have a way to just focus the wrench menu, so we use Alt+F 212 // to bring up the menu. 213 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 214 browser(), ui::VKEY_F, false, shift, true, false)); 215 #else 216 ui::KeyboardCode menu_key = 217 alternate_key_sequence ? ui::VKEY_MENU : ui::VKEY_F10; 218 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 219 browser(), menu_key, false, shift, false, false)); 220 #endif 221 222 if (shift) { 223 // Verify Chrome does not move the view focus. We should not move the view 224 // focus when typing a menu key with modifier keys, such as shift keys or 225 // control keys. 226 int new_view_id = GetFocusedViewID(); 227 ASSERT_EQ(original_view_id, new_view_id); 228 return; 229 } 230 231 WaitForFocusedViewIDToChange(original_view_id); 232 233 // See above comment. Since we already brought up the menu, no need to do this 234 // on ChromeOS. 235 #if !defined(OS_CHROMEOS) 236 if (alternate_key_sequence) 237 SendKeyPress(browser(), ui::VKEY_DOWN); 238 else 239 SendKeyPress(browser(), ui::VKEY_RETURN); 240 #endif 241 242 // Wait for the new tab to appear. 243 new_tab_observer.Wait(); 244 245 // Make sure that the new tab index is 1. 246 ASSERT_EQ(1, browser()->tab_strip_model()->active_index()); 247 } 248 249 #if defined(OS_WIN) 250 251 // This CBT hook is set for the duration of the TestSystemMenuWithKeyboard test 252 LRESULT CALLBACK SystemMenuTestCBTHook(int n_code, 253 WPARAM w_param, 254 LPARAM l_param) { 255 // Look for the system menu window getting created or becoming visible and 256 // then select the New Tab option from the menu. 257 if (n_code == HCBT_ACTIVATE || n_code == HCBT_CREATEWND) { 258 wchar_t class_name[MAX_PATH] = {0}; 259 GetClassName(reinterpret_cast<HWND>(w_param), 260 class_name, 261 arraysize(class_name)); 262 if (LowerCaseEqualsASCII(class_name, "#32768")) { 263 // Select the New Tab option and then send the enter key to execute it. 264 ::PostMessage(reinterpret_cast<HWND>(w_param), WM_CHAR, 'T', 0); 265 ::PostMessage(reinterpret_cast<HWND>(w_param), WM_KEYDOWN, VK_RETURN, 0); 266 ::PostMessage(reinterpret_cast<HWND>(w_param), WM_KEYUP, VK_RETURN, 0); 267 } 268 } 269 return ::CallNextHookEx(0, n_code, w_param, l_param); 270 } 271 272 void KeyboardAccessTest::TestSystemMenuWithKeyboard() { 273 // Navigate to a page in the first tab, which makes sure that focus is 274 // set to the browser window. 275 ui_test_utils::NavigateToURL(browser(), GURL("about:")); 276 277 ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser())); 278 279 content::WindowedNotificationObserver new_tab_observer( 280 chrome::NOTIFICATION_TAB_ADDED, 281 content::Source<content::WebContentsDelegate>(browser())); 282 // Sending the Alt space keys to the browser will bring up the system menu 283 // which runs a model loop. We set a CBT hook to look for the menu and send 284 // keystrokes to it. 285 HHOOK cbt_hook = ::SetWindowsHookEx(WH_CBT, 286 SystemMenuTestCBTHook, 287 NULL, 288 ::GetCurrentThreadId()); 289 ASSERT_TRUE(cbt_hook != NULL); 290 291 bool ret = ui_test_utils::SendKeyPressSync( 292 browser(), ui::VKEY_SPACE, false, false, true, false); 293 EXPECT_TRUE(ret); 294 295 if (ret) { 296 // Wait for the new tab to appear. 297 new_tab_observer.Wait(); 298 // Make sure that the new tab index is 1. 299 ASSERT_EQ(1, browser()->tab_strip_model()->active_index()); 300 } 301 ::UnhookWindowsHookEx(cbt_hook); 302 } 303 #endif 304 305 #if defined(USE_AURA) 306 void KeyboardAccessTest::TestMenuKeyboardAccessAndDismiss() { 307 ui_test_utils::NavigateToURL(browser(), GURL("about:")); 308 309 ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); 310 311 ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser())); 312 313 int original_view_id = GetFocusedViewID(); 314 315 BrowserView* browser_view = reinterpret_cast<BrowserView*>( 316 browser()->window()); 317 ToolbarView* toolbar_view = browser_view->GetToolbarView(); 318 SendKeysMenuListener menu_listener(toolbar_view, browser(), true); 319 320 browser()->window()->GetLocationBar()->FocusLocation(false); 321 322 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 323 browser(), ui::VKEY_F10, false, false, false, false)); 324 325 WaitForFocusedViewIDToChange(original_view_id); 326 327 SendKeyPress(browser(), ui::VKEY_DOWN); 328 content::RunMessageLoop(); 329 ASSERT_EQ(1, menu_listener.menu_open_count()); 330 } 331 #endif 332 333 // http://crbug.com/62310. 334 #if defined(OS_CHROMEOS) 335 #define MAYBE_TestMenuKeyboardAccess DISABLED_TestMenuKeyboardAccess 336 #else 337 #define MAYBE_TestMenuKeyboardAccess TestMenuKeyboardAccess 338 #endif 339 340 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, MAYBE_TestMenuKeyboardAccess) { 341 TestMenuKeyboardAccess(false, false, false); 342 } 343 344 // http://crbug.com/62310. 345 #if defined(OS_CHROMEOS) 346 #define MAYBE_TestAltMenuKeyboardAccess DISABLED_TestAltMenuKeyboardAccess 347 #else 348 #define MAYBE_TestAltMenuKeyboardAccess TestAltMenuKeyboardAccess 349 #endif 350 351 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, MAYBE_TestAltMenuKeyboardAccess) { 352 TestMenuKeyboardAccess(true, false, false); 353 } 354 355 // If this flakes, use http://crbug.com/62311. 356 #if defined(OS_WIN) 357 #define MAYBE_TestShiftAltMenuKeyboardAccess DISABLED_TestShiftAltMenuKeyboardAccess 358 #else 359 #define MAYBE_TestShiftAltMenuKeyboardAccess TestShiftAltMenuKeyboardAccess 360 #endif 361 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, 362 MAYBE_TestShiftAltMenuKeyboardAccess) { 363 TestMenuKeyboardAccess(true, true, false); 364 } 365 366 #if defined(OS_WIN) && !defined(USE_AURA) 367 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, 368 TestAltMenuKeyboardAccessFocusOmnibox) { 369 TestMenuKeyboardAccess(true, false, true); 370 } 371 372 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, TestSystemMenuWithKeyboard) { 373 TestSystemMenuWithKeyboard(); 374 } 375 #endif 376 377 #if !defined(OS_WIN) && defined(USE_AURA) 378 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, TestMenuKeyboardOpenDismiss) { 379 TestMenuKeyboardAccessAndDismiss(); 380 } 381 #endif 382 383 // Test that JavaScript cannot intercept reserved keyboard accelerators like 384 // ctrl-t to open a new tab or ctrl-f4 to close a tab. 385 // TODO(isherman): This test times out on ChromeOS. We should merge it with 386 // BrowserKeyEventsTest.ReservedAccelerators, but just disable for now. 387 // If this flakes, use http://crbug.com/62311. 388 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, ReserveKeyboardAccelerators) { 389 const std::string kBadPage = 390 "<html><script>" 391 "document.onkeydown = function() {" 392 " event.preventDefault();" 393 " return false;" 394 "}" 395 "</script></html>"; 396 GURL url("data:text/html," + kBadPage); 397 ui_test_utils::NavigateToURLWithDisposition( 398 browser(), url, NEW_FOREGROUND_TAB, 399 ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); 400 401 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 402 browser(), ui::VKEY_TAB, true, false, false, false)); 403 ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); 404 405 ui_test_utils::NavigateToURLWithDisposition( 406 browser(), url, NEW_FOREGROUND_TAB, 407 ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); 408 ASSERT_EQ(2, browser()->tab_strip_model()->active_index()); 409 410 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 411 browser(), ui::VKEY_W, true, false, false, false)); 412 ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); 413 } 414 415 #if defined(OS_WIN) // These keys are Windows-only. 416 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, BackForwardKeys) { 417 // Navigate to create some history. 418 ui_test_utils::NavigateToURL(browser(), GURL("chrome://version/")); 419 ui_test_utils::NavigateToURL(browser(), GURL("chrome://about/")); 420 421 base::string16 before_back; 422 ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &before_back)); 423 424 // Navigate back. 425 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 426 browser(), ui::VKEY_BROWSER_BACK, false, false, false, false)); 427 428 base::string16 after_back; 429 ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_back)); 430 431 EXPECT_NE(before_back, after_back); 432 433 // And then forward. 434 ASSERT_TRUE(ui_test_utils::SendKeyPressSync( 435 browser(), ui::VKEY_BROWSER_FORWARD, false, false, false, false)); 436 437 base::string16 after_forward; 438 ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_forward)); 439 440 EXPECT_EQ(before_back, after_forward); 441 } 442 #endif 443 444 } // namespace 445