Avi Drissman | 4a8573c | 2022-09-09 19:35:54 | [diff] [blame] | 1 | // Copyright 2015 The Chromium Authors |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #import "chrome/browser/global_keyboard_shortcuts_mac.h" |
| 6 | |
Evan Stade | 0526f18 | 2019-08-14 16:08:34 | [diff] [blame] | 7 | #include <Carbon/Carbon.h> |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 8 | #import <Cocoa/Cocoa.h> |
| 9 | |
| 10 | #include "base/run_loop.h" |
Tatiana Gornak | ad05d4a | 2018-06-13 10:09:53 | [diff] [blame] | 11 | #include "build/build_config.h" |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 12 | #include "chrome/app/chrome_command_ids.h" |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 13 | #include "chrome/browser/ui/browser.h" |
| 14 | #include "chrome/browser/ui/browser_commands.h" |
| 15 | #include "chrome/browser/ui/browser_window.h" |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 16 | #include "chrome/browser/ui/location_bar/location_bar.h" |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 17 | #include "chrome/browser/ui/tabs/tab_strip_model.h" |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 18 | #include "chrome/test/base/in_process_browser_test.h" |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 19 | #include "chrome/test/base/interactive_test_utils.h" |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 20 | #include "chrome/test/base/ui_test_utils.h" |
| 21 | #include "components/omnibox/browser/omnibox_view.h" |
Peter Kasting | 919ce65 | 2020-05-07 10:22:36 | [diff] [blame] | 22 | #include "content/public/test/browser_test.h" |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 23 | #include "content/public/test/test_navigation_observer.h" |
| 24 | #include "content/public/test/test_utils.h" |
Evan Stade | 0526f18 | 2019-08-14 16:08:34 | [diff] [blame] | 25 | #include "ui/events/event_constants.h" |
Elly Fong-Jones | be3bb27 | 2019-08-21 21:46:01 | [diff] [blame] | 26 | #include "ui/events/keycodes/keyboard_codes.h" |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 27 | #import "ui/events/test/cocoa_test_event_utils.h" |
| 28 | |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 29 | using cocoa_test_event_utils::SynthesizeKeyEvent; |
| 30 | |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 31 | class GlobalKeyboardShortcutsTest : public InProcessBrowserTest { |
| 32 | public: |
| 33 | GlobalKeyboardShortcutsTest() = default; |
| 34 | void SetUpOnMainThread() override { |
| 35 | // Many hotkeys are defined by the main menu. The value of these hotkeys |
| 36 | // depends on the focused window. We must focus the browser window. This is |
| 37 | // also why this test must be an interactive_ui_test rather than a browser |
| 38 | // test. |
| 39 | ASSERT_TRUE(ui_test_utils::ShowAndFocusNativeWindow( |
| 40 | browser()->window()->GetNativeWindow())); |
| 41 | } |
| 42 | }; |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 43 | |
mblsha | 2394f02 | 2016-12-02 12:03:03 | [diff] [blame] | 44 | namespace { |
| 45 | |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 46 | void SendEvent(NSEvent* ns_event) { |
| 47 | [NSApp sendEvent:ns_event]; |
mblsha | 2394f02 | 2016-12-02 12:03:03 | [diff] [blame] | 48 | } |
| 49 | |
| 50 | } // namespace |
| 51 | |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 52 | IN_PROC_BROWSER_TEST_F(GlobalKeyboardShortcutsTest, SwitchTabsMac) { |
Christopher Cameron | 359c61e2 | 2018-10-18 08:06:20 | [diff] [blame] | 53 | NSWindow* ns_window = |
| 54 | browser()->window()->GetNativeWindow().GetNativeNSWindow(); |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 55 | TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 56 | |
| 57 | // Set up window with 2 tabs. |
| 58 | chrome::NewTab(browser()); |
| 59 | EXPECT_EQ(2, tab_strip->count()); |
| 60 | EXPECT_TRUE(tab_strip->IsTabSelected(1)); |
| 61 | |
| 62 | // Ctrl+Tab goes to the next tab, which loops back to the first tab. |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 63 | SendEvent(SynthesizeKeyEvent(ns_window, true, ui::VKEY_TAB, |
| 64 | NSEventModifierFlagControl)); |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 65 | EXPECT_TRUE(tab_strip->IsTabSelected(0)); |
| 66 | |
| 67 | // Cmd+2 goes to the second tab. |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 68 | SendEvent(SynthesizeKeyEvent(ns_window, true, ui::VKEY_2, |
| 69 | NSEventModifierFlagCommand)); |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 70 | |
| 71 | // Wait for the tab to activate to be selected. |
| 72 | while (true) { |
| 73 | if (tab_strip->IsTabSelected(1)) |
| 74 | break; |
| 75 | base::RunLoop().RunUntilIdle(); |
| 76 | } |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 77 | EXPECT_TRUE(tab_strip->IsTabSelected(1)); |
| 78 | |
| 79 | // Cmd+{ goes to the previous tab. |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 80 | SendEvent(SynthesizeKeyEvent( |
| 81 | ns_window, true, ui::VKEY_OEM_4, |
| 82 | NSEventModifierFlagShift | NSEventModifierFlagCommand)); |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 83 | EXPECT_TRUE(tab_strip->IsTabSelected(0)); |
| 84 | } |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 85 | |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 86 | // Test that cmd + left arrow can be used for history navigation. |
| 87 | IN_PROC_BROWSER_TEST_F(GlobalKeyboardShortcutsTest, HistoryNavigation) { |
| 88 | TabStripModel* tab_strip = browser()->tab_strip_model(); |
Christopher Cameron | 359c61e2 | 2018-10-18 08:06:20 | [diff] [blame] | 89 | NSWindow* ns_window = |
| 90 | browser()->window()->GetNativeWindow().GetNativeNSWindow(); |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 91 | |
| 92 | GURL test_url = ui_test_utils::GetTestUrl( |
| 93 | base::FilePath(), base::FilePath(FILE_PATH_LITERAL("title1.html"))); |
| 94 | ASSERT_NE(tab_strip->GetActiveWebContents()->GetLastCommittedURL(), test_url); |
| 95 | |
| 96 | // Navigate the active tab to a dummy URL. |
| 97 | ui_test_utils::NavigateToURLBlockUntilNavigationsComplete( |
| 98 | browser(), test_url, |
| 99 | /*number_of_navigations=*/1); |
| 100 | ASSERT_EQ(tab_strip->GetActiveWebContents()->GetLastCommittedURL(), test_url); |
| 101 | |
| 102 | // Focus the WebContents. |
| 103 | tab_strip->GetActiveWebContents()->Focus(); |
| 104 | |
| 105 | // Cmd + left arrow performs history navigation, but only after the |
| 106 | // WebContents chooses not to handle the event. |
| 107 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_LEFT, |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 108 | NSEventModifierFlagCommand)); |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 109 | while (true) { |
| 110 | if (tab_strip->GetActiveWebContents()->GetLastCommittedURL() != test_url) |
| 111 | break; |
| 112 | base::RunLoop().RunUntilIdle(); |
| 113 | } |
| 114 | ASSERT_NE(tab_strip->GetActiveWebContents()->GetLastCommittedURL(), test_url); |
| 115 | } |
| 116 | |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 117 | // Test that common hotkeys for editing the omnibox work. |
| 118 | IN_PROC_BROWSER_TEST_F(GlobalKeyboardShortcutsTest, CopyPasteOmnibox) { |
| 119 | BrowserWindow* window = browser()->window(); |
| 120 | ASSERT_TRUE(window); |
| 121 | LocationBar* location_bar = window->GetLocationBar(); |
| 122 | ASSERT_TRUE(location_bar); |
| 123 | OmniboxView* omnibox_view = location_bar->GetOmniboxView(); |
| 124 | ASSERT_TRUE(omnibox_view); |
| 125 | |
Christopher Cameron | 359c61e2 | 2018-10-18 08:06:20 | [diff] [blame] | 126 | NSWindow* ns_window = |
| 127 | browser()->window()->GetNativeWindow().GetNativeNSWindow(); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 128 | |
| 129 | // Cmd+L focuses the omnibox and selects all the text. |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 130 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_L, |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 131 | NSEventModifierFlagCommand)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 132 | |
| 133 | // The first typed letter overrides the existing contents. |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 134 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_A, |
| 135 | /*flags=*/0)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 136 | // The second typed letter just appends. |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 137 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_B, |
| 138 | /*flags=*/0)); |
Jan Wilken Dörrie | 78e88d82e | 2021-03-23 15:24:22 | [diff] [blame] | 139 | ASSERT_EQ(omnibox_view->GetText(), u"ab"); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 140 | |
| 141 | // Cmd+A selects the contents. |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 142 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_A, |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 143 | NSEventModifierFlagCommand)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 144 | |
| 145 | // Cmd+C copies the contents. |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 146 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_C, |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 147 | NSEventModifierFlagCommand)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 148 | |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 149 | // The first typed letter overrides the existing contents. |
| 150 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_C, |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 151 | /*flags=*/0)); |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 152 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_D, |
| 153 | /*flags=*/0)); |
Jan Wilken Dörrie | 78e88d82e | 2021-03-23 15:24:22 | [diff] [blame] | 154 | ASSERT_EQ(omnibox_view->GetText(), u"cd"); |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 155 | |
| 156 | // Cmd + left arrow moves to the beginning. It should not perform history |
| 157 | // navigation because the firstResponder is not a WebContents.. |
| 158 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_LEFT, |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 159 | NSEventModifierFlagCommand)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 160 | |
| 161 | // Cmd+V pastes the contents. |
erikchen | b706d15 | 2018-06-12 23:21:22 | [diff] [blame] | 162 | SendEvent(SynthesizeKeyEvent(ns_window, /*keydown=*/true, ui::VKEY_V, |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 163 | NSEventModifierFlagCommand)); |
Jan Wilken Dörrie | 78e88d82e | 2021-03-23 15:24:22 | [diff] [blame] | 164 | EXPECT_EQ(omnibox_view->GetText(), u"abcd"); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 165 | } |
| 166 | |
| 167 | // Tests that the shortcut to reopen a previous tab works. |
| 168 | IN_PROC_BROWSER_TEST_F(GlobalKeyboardShortcutsTest, ReopenPreviousTab) { |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 169 | TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 170 | |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 171 | // Set up window with 2 tabs. |
| 172 | chrome::NewTab(browser()); |
| 173 | EXPECT_EQ(2, tab_strip->count()); |
| 174 | |
| 175 | // Navigate the active tab to a dummy URL. |
| 176 | GURL test_url = ui_test_utils::GetTestUrl( |
| 177 | base::FilePath(), base::FilePath(FILE_PATH_LITERAL("title1.html"))); |
| 178 | ui_test_utils::NavigateToURLBlockUntilNavigationsComplete( |
| 179 | browser(), test_url, |
| 180 | /*number_of_navigations=*/1); |
| 181 | ASSERT_EQ(tab_strip->GetActiveWebContents()->GetLastCommittedURL(), test_url); |
| 182 | |
| 183 | // Close a tab. |
Elly Fong-Jones | be3bb27 | 2019-08-21 21:46:01 | [diff] [blame] | 184 | ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync( |
| 185 | browser()->window()->GetNativeWindow(), ui::VKEY_W, false, false, false, |
| 186 | true)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 187 | EXPECT_EQ(1, tab_strip->count()); |
| 188 | ASSERT_NE(tab_strip->GetActiveWebContents()->GetLastCommittedURL(), test_url); |
| 189 | |
| 190 | // Reopen a tab. |
Elly Fong-Jones | be3bb27 | 2019-08-21 21:46:01 | [diff] [blame] | 191 | ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync( |
| 192 | browser()->window()->GetNativeWindow(), ui::VKEY_T, false, true, false, |
| 193 | true)); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 194 | EXPECT_EQ(2, tab_strip->count()); |
| 195 | ASSERT_EQ(tab_strip->GetActiveWebContents()->GetLastCommittedURL(), test_url); |
| 196 | } |
| 197 | |
| 198 | // Checks that manually configured hotkeys in the main menu have higher priority |
| 199 | // than unconfigurable hotkeys not present in the main menu. |
| 200 | IN_PROC_BROWSER_TEST_F(GlobalKeyboardShortcutsTest, MenuCommandPriority) { |
Christopher Cameron | 359c61e2 | 2018-10-18 08:06:20 | [diff] [blame] | 201 | NSWindow* ns_window = |
| 202 | browser()->window()->GetNativeWindow().GetNativeNSWindow(); |
erikchen | 5a8bff8e | 2018-06-11 21:12:17 | [diff] [blame] | 203 | TabStripModel* tab_strip = browser()->tab_strip_model(); |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 204 | |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 205 | // Set up window with 4 tabs. |
| 206 | chrome::NewTab(browser()); |
| 207 | chrome::NewTab(browser()); |
| 208 | chrome::NewTab(browser()); |
| 209 | EXPECT_EQ(4, tab_strip->count()); |
| 210 | EXPECT_TRUE(tab_strip->IsTabSelected(3)); |
| 211 | |
| 212 | // Use the cmd-2 hotkey to switch to the second tab. |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 213 | SendEvent(SynthesizeKeyEvent(ns_window, true, ui::VKEY_2, |
| 214 | NSEventModifierFlagCommand)); |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 215 | EXPECT_TRUE(tab_strip->IsTabSelected(1)); |
| 216 | |
| 217 | // Change the "Select Next Tab" menu item's key equivalent to be cmd-2, to |
| 218 | // simulate what would happen if there was a user key equivalent for it. Note |
| 219 | // that there is a readonly "userKeyEquivalent" property on NSMenuItem, but |
| 220 | // this code can't modify it. |
| 221 | NSMenu* main_menu = [NSApp mainMenu]; |
| 222 | ASSERT_NE(nil, main_menu); |
Elly Fong-Jones | 0d12184 | 2019-08-12 22:03:40 | [diff] [blame] | 223 | NSMenuItem* tab_menu = [main_menu itemWithTitle:@"Tab"]; |
| 224 | ASSERT_NE(nil, tab_menu); |
| 225 | ASSERT_TRUE(tab_menu.hasSubmenu); |
| 226 | NSMenuItem* next_item = [tab_menu.submenu itemWithTag:IDC_SELECT_NEXT_TAB]; |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 227 | ASSERT_NE(nil, next_item); |
| 228 | [next_item setKeyEquivalent:@"2"]; |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 229 | [next_item setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 230 | ASSERT_TRUE([next_item isEnabled]); |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 231 | |
| 232 | // Send cmd-2 again, and ensure the tab switches. |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 233 | SendEvent(SynthesizeKeyEvent(ns_window, true, ui::VKEY_2, |
| 234 | NSEventModifierFlagCommand)); |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 235 | EXPECT_TRUE(tab_strip->IsTabSelected(2)); |
Avi Drissman | 8be8111 | 2022-05-11 01:12:42 | [diff] [blame] | 236 | SendEvent(SynthesizeKeyEvent(ns_window, true, ui::VKEY_2, |
| 237 | NSEventModifierFlagCommand)); |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 238 | EXPECT_TRUE(tab_strip->IsTabSelected(3)); |
| 239 | } |