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   // Uses the keyboard to select the wrench menu i.e. with the F10 key.
    173   // It verifies that the menu when dismissed by sending the ESC key it does
    174   // not display twice.
    175   void TestMenuKeyboardAccessAndDismiss();
    176 
    177   DISALLOW_COPY_AND_ASSIGN(KeyboardAccessTest);
    178 };
    179 
    180 void KeyboardAccessTest::TestMenuKeyboardAccess(bool alternate_key_sequence,
    181                                                 bool shift,
    182                                                 bool focus_omnibox) {
    183   // Navigate to a page in the first tab, which makes sure that focus is
    184   // set to the browser window.
    185   ui_test_utils::NavigateToURL(browser(), GURL("about:"));
    186 
    187   // The initial tab index should be 0.
    188   ASSERT_EQ(0, browser()->tab_strip_model()->active_index());
    189 
    190   ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
    191 
    192   // Get the focused view ID, then press a key to activate the
    193   // page menu, then wait until the focused view changes.
    194   int original_view_id = GetFocusedViewID();
    195 
    196   content::WindowedNotificationObserver new_tab_observer(
    197       chrome::NOTIFICATION_TAB_ADDED,
    198       content::Source<content::WebContentsDelegate>(browser()));
    199 
    200   BrowserView* browser_view = reinterpret_cast<BrowserView*>(
    201       browser()->window());
    202   ToolbarView* toolbar_view = browser_view->GetToolbarView();
    203   SendKeysMenuListener menu_listener(toolbar_view, browser(), false);
    204 
    205   if (focus_omnibox)
    206     browser()->window()->GetLocationBar()->FocusLocation(false);
    207 
    208 #if defined(OS_CHROMEOS)
    209   // Chrome OS doesn't have a way to just focus the wrench menu, so we use Alt+F
    210   // to bring up the menu.
    211   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    212       browser(), ui::VKEY_F, false, shift, true, false));
    213 #else
    214   ui::KeyboardCode menu_key =
    215       alternate_key_sequence ? ui::VKEY_MENU : ui::VKEY_F10;
    216   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    217       browser(), menu_key, false, shift, false, false));
    218 #endif
    219 
    220   if (shift) {
    221     // Verify Chrome does not move the view focus. We should not move the view
    222     // focus when typing a menu key with modifier keys, such as shift keys or
    223     // control keys.
    224     int new_view_id = GetFocusedViewID();
    225     ASSERT_EQ(original_view_id, new_view_id);
    226     return;
    227   }
    228 
    229   WaitForFocusedViewIDToChange(original_view_id);
    230 
    231   // See above comment. Since we already brought up the menu, no need to do this
    232   // on ChromeOS.
    233 #if !defined(OS_CHROMEOS)
    234   if (alternate_key_sequence)
    235     SendKeyPress(browser(), ui::VKEY_DOWN);
    236   else
    237     SendKeyPress(browser(), ui::VKEY_RETURN);
    238 #endif
    239 
    240   // Wait for the new tab to appear.
    241   new_tab_observer.Wait();
    242 
    243   // Make sure that the new tab index is 1.
    244   ASSERT_EQ(1, browser()->tab_strip_model()->active_index());
    245 }
    246 
    247 #if defined(OS_WIN)
    248 
    249 // This CBT hook is set for the duration of the TestSystemMenuWithKeyboard test
    250 LRESULT CALLBACK SystemMenuTestCBTHook(int n_code,
    251                                        WPARAM w_param,
    252                                        LPARAM l_param) {
    253   // Look for the system menu window getting created or becoming visible and
    254   // then select the New Tab option from the menu.
    255   if (n_code == HCBT_ACTIVATE || n_code == HCBT_CREATEWND) {
    256     wchar_t class_name[MAX_PATH] = {0};
    257     GetClassName(reinterpret_cast<HWND>(w_param),
    258                  class_name,
    259                  arraysize(class_name));
    260     if (LowerCaseEqualsASCII(class_name, "#32768")) {
    261       // Select the New Tab option and then send the enter key to execute it.
    262       ::PostMessage(reinterpret_cast<HWND>(w_param), WM_CHAR, 'T', 0);
    263       ::PostMessage(reinterpret_cast<HWND>(w_param), WM_KEYDOWN, VK_RETURN, 0);
    264       ::PostMessage(reinterpret_cast<HWND>(w_param), WM_KEYUP, VK_RETURN, 0);
    265     }
    266   }
    267   return ::CallNextHookEx(0, n_code, w_param, l_param);
    268 }
    269 
    270 void KeyboardAccessTest::TestSystemMenuWithKeyboard() {
    271   // Navigate to a page in the first tab, which makes sure that focus is
    272   // set to the browser window.
    273   ui_test_utils::NavigateToURL(browser(), GURL("about:"));
    274 
    275   ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
    276 
    277   content::WindowedNotificationObserver new_tab_observer(
    278       chrome::NOTIFICATION_TAB_ADDED,
    279       content::Source<content::WebContentsDelegate>(browser()));
    280   // Sending the Alt space keys to the browser will bring up the system menu
    281   // which runs a model loop. We set a CBT hook to look for the menu and send
    282   // keystrokes to it.
    283   HHOOK cbt_hook = ::SetWindowsHookEx(WH_CBT,
    284                                       SystemMenuTestCBTHook,
    285                                       NULL,
    286                                       ::GetCurrentThreadId());
    287   ASSERT_TRUE(cbt_hook != NULL);
    288 
    289   bool ret = ui_test_utils::SendKeyPressSync(
    290       browser(), ui::VKEY_SPACE, false, false, true, false);
    291   EXPECT_TRUE(ret);
    292 
    293   if (ret) {
    294     // Wait for the new tab to appear.
    295     new_tab_observer.Wait();
    296     // Make sure that the new tab index is 1.
    297     ASSERT_EQ(1, browser()->tab_strip_model()->active_index());
    298   }
    299   ::UnhookWindowsHookEx(cbt_hook);
    300 }
    301 #endif
    302 
    303 void KeyboardAccessTest::TestMenuKeyboardAccessAndDismiss() {
    304   ui_test_utils::NavigateToURL(browser(), GURL("about:"));
    305 
    306   ASSERT_EQ(0, browser()->tab_strip_model()->active_index());
    307 
    308   ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
    309 
    310   int original_view_id = GetFocusedViewID();
    311 
    312   BrowserView* browser_view = reinterpret_cast<BrowserView*>(
    313       browser()->window());
    314   ToolbarView* toolbar_view = browser_view->GetToolbarView();
    315   SendKeysMenuListener menu_listener(toolbar_view, browser(), true);
    316 
    317   browser()->window()->GetLocationBar()->FocusLocation(false);
    318 
    319   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    320       browser(), ui::VKEY_F10, false, false, false, false));
    321 
    322   WaitForFocusedViewIDToChange(original_view_id);
    323 
    324   SendKeyPress(browser(), ui::VKEY_DOWN);
    325   content::RunMessageLoop();
    326   ASSERT_EQ(1, menu_listener.menu_open_count());
    327 }
    328 
    329 // http://crbug.com/62310.
    330 #if defined(OS_CHROMEOS)
    331 #define MAYBE_TestMenuKeyboardAccess DISABLED_TestMenuKeyboardAccess
    332 #else
    333 #define MAYBE_TestMenuKeyboardAccess TestMenuKeyboardAccess
    334 #endif
    335 
    336 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, MAYBE_TestMenuKeyboardAccess) {
    337   TestMenuKeyboardAccess(false, false, false);
    338 }
    339 
    340 // http://crbug.com/62310.
    341 #if defined(OS_CHROMEOS)
    342 #define MAYBE_TestAltMenuKeyboardAccess DISABLED_TestAltMenuKeyboardAccess
    343 #else
    344 #define MAYBE_TestAltMenuKeyboardAccess TestAltMenuKeyboardAccess
    345 #endif
    346 
    347 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, MAYBE_TestAltMenuKeyboardAccess) {
    348   TestMenuKeyboardAccess(true, false, false);
    349 }
    350 
    351 // If this flakes, use http://crbug.com/62311.
    352 #if defined(OS_WIN)
    353 #define MAYBE_TestShiftAltMenuKeyboardAccess DISABLED_TestShiftAltMenuKeyboardAccess
    354 #else
    355 #define MAYBE_TestShiftAltMenuKeyboardAccess TestShiftAltMenuKeyboardAccess
    356 #endif
    357 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest,
    358                        MAYBE_TestShiftAltMenuKeyboardAccess) {
    359   TestMenuKeyboardAccess(true, true, false);
    360 }
    361 
    362 #if defined(OS_WIN)
    363 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest,
    364                        DISABLED_TestAltMenuKeyboardAccessFocusOmnibox) {
    365   TestMenuKeyboardAccess(true, false, true);
    366 }
    367 
    368 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest,
    369                        DISABLED_TestSystemMenuWithKeyboard) {
    370   TestSystemMenuWithKeyboard();
    371 }
    372 #endif
    373 
    374 #if !defined(OS_WIN) && defined(USE_AURA)
    375 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, TestMenuKeyboardOpenDismiss) {
    376   TestMenuKeyboardAccessAndDismiss();
    377 }
    378 #endif
    379 
    380 // Test that JavaScript cannot intercept reserved keyboard accelerators like
    381 // ctrl-t to open a new tab or ctrl-f4 to close a tab.
    382 // TODO(isherman): This test times out on ChromeOS.  We should merge it with
    383 // BrowserKeyEventsTest.ReservedAccelerators, but just disable for now.
    384 // If this flakes, use http://crbug.com/62311.
    385 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, ReserveKeyboardAccelerators) {
    386   const std::string kBadPage =
    387       "<html><script>"
    388       "document.onkeydown = function() {"
    389       "  event.preventDefault();"
    390       "  return false;"
    391       "}"
    392       "</script></html>";
    393   GURL url("data:text/html," + kBadPage);
    394   ui_test_utils::NavigateToURLWithDisposition(
    395       browser(), url, NEW_FOREGROUND_TAB,
    396       ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
    397 
    398   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    399       browser(), ui::VKEY_TAB, true, false, false, false));
    400   ASSERT_EQ(0, browser()->tab_strip_model()->active_index());
    401 
    402   ui_test_utils::NavigateToURLWithDisposition(
    403       browser(), url, NEW_FOREGROUND_TAB,
    404       ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
    405   ASSERT_EQ(2, browser()->tab_strip_model()->active_index());
    406 
    407   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    408       browser(), ui::VKEY_W, true, false, false, false));
    409   ASSERT_EQ(0, browser()->tab_strip_model()->active_index());
    410 }
    411 
    412 #if defined(OS_WIN)  // These keys are Windows-only.
    413 IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, BackForwardKeys) {
    414   // Navigate to create some history.
    415   ui_test_utils::NavigateToURL(browser(), GURL("chrome://version/"));
    416   ui_test_utils::NavigateToURL(browser(), GURL("chrome://about/"));
    417 
    418   base::string16 before_back;
    419   ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &before_back));
    420 
    421   // Navigate back.
    422   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    423       browser(), ui::VKEY_BROWSER_BACK, false, false, false, false));
    424 
    425   base::string16 after_back;
    426   ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_back));
    427 
    428   EXPECT_NE(before_back, after_back);
    429 
    430   // And then forward.
    431   ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
    432       browser(), ui::VKEY_BROWSER_FORWARD, false, false, false, false));
    433 
    434   base::string16 after_forward;
    435   ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_forward));
    436 
    437   EXPECT_EQ(before_back, after_forward);
    438 }
    439 #endif
    440 
    441 }  // namespace
    442