keyboard_access_browsertest.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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
30namespace {
31
32// An async version of SendKeyPressSync since we don't get notified when a
33// menu is showing.
34void 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.
41class 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
83class 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
131class 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
182void 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
252LRESULT 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
272void 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)
306void 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
340IN_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
351IN_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
361IN_PROC_BROWSER_TEST_F(KeyboardAccessTest,
362                       MAYBE_TestShiftAltMenuKeyboardAccess) {
363  TestMenuKeyboardAccess(true, true, false);
364}
365
366#if defined(OS_WIN)
367IN_PROC_BROWSER_TEST_F(KeyboardAccessTest,
368                       DISABLED_TestAltMenuKeyboardAccessFocusOmnibox) {
369  TestMenuKeyboardAccess(true, false, true);
370}
371
372IN_PROC_BROWSER_TEST_F(KeyboardAccessTest,
373                       DISABLED_TestSystemMenuWithKeyboard) {
374  TestSystemMenuWithKeyboard();
375}
376#endif
377
378#if !defined(OS_WIN) && defined(USE_AURA)
379IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, TestMenuKeyboardOpenDismiss) {
380  TestMenuKeyboardAccessAndDismiss();
381}
382#endif
383
384// Test that JavaScript cannot intercept reserved keyboard accelerators like
385// ctrl-t to open a new tab or ctrl-f4 to close a tab.
386// TODO(isherman): This test times out on ChromeOS.  We should merge it with
387// BrowserKeyEventsTest.ReservedAccelerators, but just disable for now.
388// If this flakes, use http://crbug.com/62311.
389IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, ReserveKeyboardAccelerators) {
390  const std::string kBadPage =
391      "<html><script>"
392      "document.onkeydown = function() {"
393      "  event.preventDefault();"
394      "  return false;"
395      "}"
396      "</script></html>";
397  GURL url("data:text/html," + kBadPage);
398  ui_test_utils::NavigateToURLWithDisposition(
399      browser(), url, NEW_FOREGROUND_TAB,
400      ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
401
402  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
403      browser(), ui::VKEY_TAB, true, false, false, false));
404  ASSERT_EQ(0, browser()->tab_strip_model()->active_index());
405
406  ui_test_utils::NavigateToURLWithDisposition(
407      browser(), url, NEW_FOREGROUND_TAB,
408      ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
409  ASSERT_EQ(2, browser()->tab_strip_model()->active_index());
410
411  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
412      browser(), ui::VKEY_W, true, false, false, false));
413  ASSERT_EQ(0, browser()->tab_strip_model()->active_index());
414}
415
416#if defined(OS_WIN)  // These keys are Windows-only.
417IN_PROC_BROWSER_TEST_F(KeyboardAccessTest, BackForwardKeys) {
418  // Navigate to create some history.
419  ui_test_utils::NavigateToURL(browser(), GURL("chrome://version/"));
420  ui_test_utils::NavigateToURL(browser(), GURL("chrome://about/"));
421
422  base::string16 before_back;
423  ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &before_back));
424
425  // Navigate back.
426  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
427      browser(), ui::VKEY_BROWSER_BACK, false, false, false, false));
428
429  base::string16 after_back;
430  ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_back));
431
432  EXPECT_NE(before_back, after_back);
433
434  // And then forward.
435  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
436      browser(), ui::VKEY_BROWSER_FORWARD, false, false, false, false));
437
438  base::string16 after_forward;
439  ASSERT_TRUE(ui_test_utils::GetCurrentTabTitle(browser(), &after_forward));
440
441  EXPECT_EQ(before_back, after_forward);
442}
443#endif
444
445}  // namespace
446