| // Copyright (c) 2012 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. |
| |
| // This functionality currently works on Windows and on Linux when |
| // toolkit_views is defined (i.e. for Chrome OS). It's not needed |
| // on the Mac, and it's not yet implemented on Linux. |
| |
| #include "base/memory/weak_ptr.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/strings/string_util.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/interactive_test_utils.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "ui/base/test/ui_controls.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| #include "ui/views/controls/menu/menu_listener.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace { |
| |
| // An async version of SendKeyPressSync since we don't get notified when a |
| // menu is showing. |
| void SendKeyPress(Browser* browser, ui::KeyboardCode key) { |
| ASSERT_TRUE(ui_controls::SendKeyPress( |
| browser->window()->GetNativeWindow(), key, false, false, false, false)); |
| } |
| |
| // Helper class that waits until the focus has changed to a view other |
| // than the one with the provided view id. |
| class ViewFocusChangeWaiter : public views::FocusChangeListener { |
| public: |
| ViewFocusChangeWaiter(views::FocusManager* focus_manager, |
| int previous_view_id) |
| : focus_manager_(focus_manager), |
| previous_view_id_(previous_view_id), |
| weak_factory_(this) { |
| focus_manager_->AddFocusChangeListener(this); |
| // Call the focus change notification once in case the focus has |
| // already changed. |
| OnWillChangeFocus(NULL, focus_manager_->GetFocusedView()); |
| } |
| |
| virtual ~ViewFocusChangeWaiter() { |
| focus_manager_->RemoveFocusChangeListener(this); |
| } |
| |
| void Wait() { |
| content::RunMessageLoop(); |
| } |
| |
| private: |
| // Inherited from FocusChangeListener |
| virtual void OnWillChangeFocus(views::View* focused_before, |
| views::View* focused_now) OVERRIDE { |
| } |
| |
| virtual void OnDidChangeFocus(views::View* focused_before, |
| views::View* focused_now) OVERRIDE { |
| if (focused_now && focused_now->id() != previous_view_id_) { |
| base::MessageLoop::current()->PostTask(FROM_HERE, |
| base::MessageLoop::QuitClosure()); |
| } |
| } |
| |
| views::FocusManager* focus_manager_; |
| int previous_view_id_; |
| base::WeakPtrFactory<ViewFocusChangeWaiter> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ViewFocusChangeWaiter); |
| }; |
| |
| class SendKeysMenuListener : public views::MenuListener { |
| public: |
| SendKeysMenuListener(ToolbarView* toolbar_view, |
| Browser* browser, |
| bool test_dismiss_menu) |
| : toolbar_view_(toolbar_view), browser_(browser), menu_open_count_(0), |
| test_dismiss_menu_(test_dismiss_menu) { |
| toolbar_view_->AddMenuListener(this); |
| } |
| |
| virtual ~SendKeysMenuListener() { |
| if (test_dismiss_menu_) |
| toolbar_view_->RemoveMenuListener(this); |
| } |
| |
| int menu_open_count() const { |
| return menu_open_count_; |
| } |
| |
| private: |
| // Overridden from views::MenuListener: |
| virtual void OnMenuOpened() OVERRIDE { |
| menu_open_count_++; |
| if (!test_dismiss_menu_) { |
| toolbar_view_->RemoveMenuListener(this); |
| // Press DOWN to select the first item, then RETURN to select it. |
| SendKeyPress(browser_, ui::VKEY_DOWN); |
| SendKeyPress(browser_, ui::VKEY_RETURN); |
| } else { |
| SendKeyPress(browser_, ui::VKEY_ESCAPE); |
| base::MessageLoop::current()->PostDelayedTask( |
| FROM_HERE, |
| base::MessageLoop::QuitClosure(), |
| base::TimeDelta::FromMilliseconds(200)); |
| } |
| } |
| |
| ToolbarView* toolbar_view_; |
| Browser* browser_; |
| // Keeps track of the number of times the menu was opened. |
| int menu_open_count_; |
| // If this is set then on receiving a notification that the menu was opened |
| // we dismiss it by sending the ESC key. |
| bool test_dismiss_menu_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SendKeysMenuListener); |
| }; |
| |
| class KeyboardAccessTest : public InProcessBrowserTest { |
| public: |
| KeyboardAccessTest() {} |
| |
| // Use the keyboard to select "New Tab" from the app menu. |
| // This test depends on the fact that there is one menu and that |
| // New Tab is the first item in the menu. If the menus change, |
| // this test will need to be changed to reflect that. |
| // |
| // If alternate_key_sequence is true, use "Alt" instead of "F10" to |
| // open the menu bar, and "Down" instead of "Enter" to open a menu. |
| // If focus_omnibox is true then the test on startup sets focus to the |
| // omnibox. |
| void TestMenuKeyboardAccess(bool alternate_key_sequence, |
| bool shift, |
| bool focus_omnibox); |
| |
| int GetFocusedViewID() { |
| gfx::NativeWindow window = browser()->window()->GetNativeWindow(); |
| views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); |
| const views::FocusManager* focus_manager = widget->GetFocusManager(); |
| const views::View* focused_view = focus_manager->GetFocusedView(); |
| return focused_view ? focused_view->id() : -1; |
| } |
| |
| void WaitForFocusedViewIDToChange(int original_view_id) { |
| if (GetFocusedViewID() != original_view_id) |
| return; |
| gfx::NativeWindow window = browser()->window()->GetNativeWindow(); |
| views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); |
| views::FocusManager* focus_manager = widget->GetFocusManager(); |
| ViewFocusChangeWaiter waiter(focus_manager, original_view_id); |
| waiter.Wait(); |
| } |
| |
| #if defined(OS_WIN) |
| // Opens the system menu on Windows with the Alt Space combination and selects |
| // the New Tab option from the menu. |
| void TestSystemMenuWithKeyboard(); |
| #endif |
| |
| #if defined(USE_AURA) |
| // Uses the keyboard to select the wrench menu i.e. with the F10 key. |
| // It verifies that the menu when dismissed by sending the ESC key it does |
| // not display twice. |
| void TestMenuKeyboardAccessAndDismiss(); |
| #endif |
| |
| DISALLOW_COPY_AND_ASSIGN(KeyboardAccessTest); |
| }; |
| |
| void KeyboardAccessTest::TestMenuKeyboardAccess(bool alternate_key_sequence, |
| bool shift, |
| bool focus_omnibox) { |
| // Navigate to a page in the first tab, which makes sure that focus is |
| // set to the browser window. |
| ui_test_utils::NavigateToURL(browser(), GURL("about:")); |
| |
| // The initial tab index should be 0. |
| ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); |
| |
| ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser())); |
| |
| // Get the focused view ID, then press a key to activate the |
| // page menu, then wait until the focused view changes. |
| int original_view_id = GetFocusedViewID(); |
| |
| content::WindowedNotificationObserver new_tab_observer( |
| chrome::NOTIFICATION_TAB_ADDED, |
| content::Source<content::WebContentsDelegate>(browser())); |
| |
| BrowserView* browser_view = reinterpret_cast<BrowserView*>( |
| browser()->window()); |
| ToolbarView* toolbar_view = browser_view->GetToolbarView(); |
| SendKeysMenuListener menu_listener(toolbar_view, browser(), false); |
| |
| if (focus_omnibox) |
| browser()->window()->GetLocationBar()->FocusLocation(false); |
| |
| #if defined(OS_CHROMEOS) |
| // Chrome OS doesn't have a way to just focus the wrench menu, so we use Alt+F |
| // to bring up the menu. |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_F, false, shift, true, false)); |
| #else |
| ui::KeyboardCode menu_key = |
| alternate_key_sequence ? ui::VKEY_MENU : ui::VKEY_F10; |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), menu_key, false, shift, false, false)); |
| #endif |
| |
| if (shift) { |
| // Verify Chrome does not move the view focus. We should not move the view |
| // focus when typing a menu key with modifier keys, such as shift keys or |
| // control keys. |
| int new_view_id = GetFocusedViewID(); |
| ASSERT_EQ(original_view_id, new_view_id); |
| return; |
| } |
| |
| WaitForFocusedViewIDToChange(original_view_id); |
| |
| // See above comment. Since we already brought up the menu, no need to do this |
| // on ChromeOS. |
| #if !defined(OS_CHROMEOS) |
| if (alternate_key_sequence) |
| SendKeyPress(browser(), ui::VKEY_DOWN); |
| else |
| SendKeyPress(browser(), ui::VKEY_RETURN); |
| #endif |
| |
| // Wait for the new tab to appear. |
| new_tab_observer.Wait(); |
| |
| // Make sure that the new tab index is 1. |
| ASSERT_EQ(1, browser()->tab_strip_model()->active_index()); |
| } |
| |
| #if defined(OS_WIN) |
| |
| // This CBT hook is set for the duration of the TestSystemMenuWithKeyboard test |
| LRESULT CALLBACK SystemMenuTestCBTHook(int n_code, |
| WPARAM w_param, |
| LPARAM l_param) { |
| // Look for the system menu window getting created or becoming visible and |
| // then select the New Tab option from the menu. |
| if (n_code == HCBT_ACTIVATE || n_code == HCBT_CREATEWND) { |
| wchar_t class_name[MAX_PATH] = {0}; |
| GetClassName(reinterpret_cast<HWND>(w_param), |
| class_name, |
| arraysize(class_name)); |
| if (LowerCaseEqualsASCII(class_name, "#32768")) { |
| // Select the New Tab option and then send the enter key to execute it. |
| ::PostMessage(reinterpret_cast<HWND>(w_param), WM_CHAR, 'T', 0); |
| ::PostMessage(reinterpret_cast<HWND>(w_param), WM_KEYDOWN, VK_RETURN, 0); |
| ::PostMessage(reinterpret_cast<HWND>(w_param), WM_KEYUP, VK_RETURN, 0); |
| } |
| } |
| return ::CallNextHookEx(0, n_code, w_param, l_param); |
| } |
| |
| void KeyboardAccessTest::TestSystemMenuWithKeyboard() { |
| // Navigate to a page in the first tab, which makes sure that focus is |
| // set to the browser window. |
| ui_test_utils::NavigateToURL(browser(), GURL("about:")); |
| |
| ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser())); |
| |
| content::WindowedNotificationObserver new_tab_observer( |
| chrome::NOTIFICATION_TAB_ADDED, |
| content::Source<content::WebContentsDelegate>(browser())); |
| // Sending the Alt space keys to the browser will bring up the system menu |
| // which runs a model loop. We set a CBT hook to look for the menu and send |
| // keystrokes to it. |
| HHOOK cbt_hook = ::SetWindowsHookEx(WH_CBT, |
| SystemMenuTestCBTHook, |
| NULL, |
| ::GetCurrentThreadId()); |
| ASSERT_TRUE(cbt_hook != NULL); |
| |
| bool ret = ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_SPACE, false, false, true, false); |
| EXPECT_TRUE(ret); |
| |
| if (ret) { |
| // Wait for the new tab to appear. |
| new_tab_observer.Wait(); |
| // Make sure that the new tab index is 1. |
| ASSERT_EQ(1, browser()->tab_strip_model()->active_index()); |
| } |
| ::UnhookWindowsHookEx(cbt_hook); |
| } |
| #endif |
| |
| #if defined(USE_AURA) |
| void KeyboardAccessTest::TestMenuKeyboardAccessAndDismiss() { |
| ui_test_utils::NavigateToURL(browser(), GURL("about:")); |
| |
| ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); |
| |
| ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser())); |
| |
| int original_view_id = GetFocusedViewID(); |
| |
| BrowserView* browser_view = reinterpret_cast<BrowserView*>( |
| browser()->window()); |
| ToolbarView* toolbar_view = browser_view->GetToolbarView(); |
| SendKeysMenuListener menu_listener(toolbar_view, browser(), true); |
| |
| browser()->window()->GetLocationBar()->FocusLocation(false); |
| |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_F10, false, false, false, false)); |
| |
| WaitForFocusedViewIDToChange(original_view_id); |
| |
| SendKeyPress(browser(), ui::VKEY_DOWN); |
| content::RunMessageLoop(); |
| ASSERT_EQ(1, menu_listener.menu_open_count()); |
| } |
| #endif |
| |
| // http://crbug.com/62310. |
| #if defined(OS_CHROMEOS) |
| #define MAYBE_TestMenuKeyboardAccess DISABLED_TestMenuKeyboardAccess |
| #else |
| #define MAYBE_TestMenuKeyboardAccess TestMenuKeyboardAccess |
| #endif |
| |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, MAYBE_TestMenuKeyboardAccess) { |
| TestMenuKeyboardAccess(false, false, false); |
| } |
| |
| // http://crbug.com/62310. |
| #if defined(OS_CHROMEOS) |
| #define MAYBE_TestAltMenuKeyboardAccess DISABLED_TestAltMenuKeyboardAccess |
| #else |
| #define MAYBE_TestAltMenuKeyboardAccess TestAltMenuKeyboardAccess |
| #endif |
| |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, MAYBE_TestAltMenuKeyboardAccess) { |
| TestMenuKeyboardAccess(true, false, false); |
| } |
| |
| // If this flakes, use http://crbug.com/62311. |
| #if defined(OS_WIN) |
| #define MAYBE_TestShiftAltMenuKeyboardAccess DISABLED_TestShiftAltMenuKeyboardAccess |
| #else |
| #define MAYBE_TestShiftAltMenuKeyboardAccess TestShiftAltMenuKeyboardAccess |
| #endif |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, |
| MAYBE_TestShiftAltMenuKeyboardAccess) { |
| TestMenuKeyboardAccess(true, true, false); |
| } |
| |
| #if defined(OS_WIN) && !defined(USE_AURA) |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, |
| TestAltMenuKeyboardAccessFocusOmnibox) { |
| TestMenuKeyboardAccess(true, false, true); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, TestSystemMenuWithKeyboard) { |
| TestSystemMenuWithKeyboard(); |
| } |
| #endif |
| |
| #if !defined(OS_WIN) && defined(USE_AURA) |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, TestMenuKeyboardOpenDismiss) { |
| TestMenuKeyboardAccessAndDismiss(); |
| } |
| #endif |
| |
| // Test that JavaScript cannot intercept reserved keyboard accelerators like |
| // ctrl-t to open a new tab or ctrl-f4 to close a tab. |
| // TODO(isherman): This test times out on ChromeOS. We should merge it with |
| // BrowserKeyEventsTest.ReservedAccelerators, but just disable for now. |
| // If this flakes, use http://crbug.com/62311. |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, ReserveKeyboardAccelerators) { |
| const std::string kBadPage = |
| "<html><script>" |
| "document.onkeydown = function() {" |
| " event.preventDefault();" |
| " return false;" |
| "}" |
| "</script></html>"; |
| GURL url("data:text/html," + kBadPage); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); |
| |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_TAB, true, false, false, false)); |
| ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); |
| |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); |
| ASSERT_EQ(2, browser()->tab_strip_model()->active_index()); |
| |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_W, true, false, false, false)); |
| ASSERT_EQ(0, browser()->tab_strip_model()->active_index()); |
| } |
| |
| #if defined(OS_WIN) // These keys are Windows-only. |
| IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, BackForwardKeys) { |
| // Navigate to create some history. |
| ui_test_utils::NavigateToURL(browser(), GURL("chrome://version/")); |
| ui_test_utils::NavigateToURL(browser(), GURL("chrome://about/")); |
| |
| base::string16 before_back; |
| ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &before_back)); |
| |
| // Navigate back. |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_BROWSER_BACK, false, false, false, false)); |
| |
| base::string16 after_back; |
| ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_back)); |
| |
| EXPECT_NE(before_back, after_back); |
| |
| // And then forward. |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_BROWSER_FORWARD, false, false, false, false)); |
| |
| base::string16 after_forward; |
| ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_forward)); |
| |
| EXPECT_EQ(before_back, after_forward); |
| } |
| #endif |
| |
| } // namespace |