Home | History | Annotate | Download | only in views
      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