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 #include <Carbon/Carbon.h> 6 #import <Cocoa/Cocoa.h> 7 8 #include "base/command_line.h" 9 #include "base/debug/debugger.h" 10 #include "base/mac/scoped_nsautorelease_pool.h" 11 #include "base/memory/scoped_ptr.h" 12 #include "base/strings/sys_string_conversions.h" 13 #include "chrome/app/chrome_command_ids.h" // IDC_* 14 #include "chrome/browser/chrome_notification_types.h" 15 #import "chrome/browser/ui/cocoa/browser_window_utils.h" 16 #import "chrome/browser/ui/cocoa/cocoa_profile_test.h" 17 #import "chrome/browser/ui/cocoa/panels/panel_cocoa.h" 18 #import "chrome/browser/ui/cocoa/panels/panel_titlebar_view_cocoa.h" 19 #import "chrome/browser/ui/cocoa/panels/panel_window_controller_cocoa.h" 20 #import "chrome/browser/ui/cocoa/run_loop_testing.h" 21 #include "chrome/browser/ui/panels/panel.h" 22 #include "chrome/browser/ui/panels/panel_manager.h" 23 #include "chrome/common/chrome_switches.h" 24 #include "chrome/test/base/testing_profile.h" 25 #include "content/public/test/test_utils.h" 26 #include "testing/gtest/include/gtest/gtest.h" 27 #include "testing/gtest_mac.h" 28 29 class PanelAnimatedBoundsObserver : 30 public content::WindowedNotificationObserver { 31 public: 32 PanelAnimatedBoundsObserver(Panel* panel) 33 : content::WindowedNotificationObserver( 34 chrome::NOTIFICATION_PANEL_BOUNDS_ANIMATIONS_FINISHED, 35 content::Source<Panel>(panel)) { } 36 virtual ~PanelAnimatedBoundsObserver() { } 37 }; 38 39 // Main test class. 40 class PanelCocoaTest : public CocoaProfileTest { 41 public: 42 virtual void SetUp() { 43 CocoaProfileTest::SetUp(); 44 } 45 46 Panel* CreateTestPanel(const std::string& panel_name) { 47 // Opening panels on a Mac causes NSWindowController of the Panel window 48 // to be autoreleased. We need a pool drained after it's done so the test 49 // can close correctly. 50 base::mac::ScopedNSAutoreleasePool autorelease_pool; 51 52 PanelManager* manager = PanelManager::GetInstance(); 53 int panels_count = manager->num_panels(); 54 55 Panel* panel = manager->CreatePanel(panel_name, profile(), 56 GURL(), gfx::Rect(), 57 PanelManager::CREATE_AS_DOCKED); 58 EXPECT_EQ(panels_count + 1, manager->num_panels()); 59 60 EXPECT_TRUE(panel); 61 EXPECT_TRUE(panel->native_panel()); // Native panel is created right away. 62 PanelCocoa* native_window = 63 static_cast<PanelCocoa*>(panel->native_panel()); 64 EXPECT_EQ(panel, native_window->panel_); // Back pointer initialized. 65 66 PanelAnimatedBoundsObserver bounds_observer(panel); 67 68 // Window should not load before Show(). 69 // Note: Loading the wnidow causes Cocoa to autorelease a few objects. 70 // This is the reason we do this within the scope of the 71 // ScopedNSAutoreleasePool. 72 EXPECT_FALSE([native_window->controller_ isWindowLoaded]); 73 panel->Show(); 74 EXPECT_TRUE([native_window->controller_ isWindowLoaded]); 75 EXPECT_TRUE([native_window->controller_ window]); 76 77 // Wait until bounds animate to their specified values. 78 bounds_observer.Wait(); 79 80 return panel; 81 } 82 83 void VerifyTitlebarLocation(NSView* contentView, NSView* titlebar) { 84 NSRect content_frame = [contentView frame]; 85 NSRect titlebar_frame = [titlebar frame]; 86 // Since contentView and titlebar are both children of window's root view, 87 // we can compare their frames since they are in the same coordinate system. 88 EXPECT_EQ(NSMinX(content_frame), NSMinX(titlebar_frame)); 89 EXPECT_EQ(NSWidth(content_frame), NSWidth(titlebar_frame)); 90 EXPECT_EQ(NSHeight([[titlebar superview] bounds]), NSMaxY(titlebar_frame)); 91 } 92 93 void ClosePanelAndWait(Panel* panel) { 94 EXPECT_TRUE(panel); 95 // Closing a panel may involve several async tasks. Need to use 96 // message pump and wait for the notification. 97 PanelManager* manager = PanelManager::GetInstance(); 98 int panel_count = manager->num_panels(); 99 content::WindowedNotificationObserver signal( 100 chrome::NOTIFICATION_PANEL_CLOSED, 101 content::Source<Panel>(panel)); 102 panel->Close(); 103 signal.Wait(); 104 // Now we have one less panel. 105 EXPECT_EQ(panel_count - 1, manager->num_panels()); 106 } 107 108 NSMenuItem* CreateMenuItem(NSMenu* menu, int command_id) { 109 NSMenuItem* item = 110 [menu addItemWithTitle:@"" 111 action:@selector(commandDispatch:) 112 keyEquivalent:@""]; 113 [item setTag:command_id]; 114 return item; 115 } 116 }; 117 118 TEST_F(PanelCocoaTest, CreateClose) { 119 PanelManager* manager = PanelManager::GetInstance(); 120 EXPECT_EQ(0, manager->num_panels()); // No panels initially. 121 122 Panel* panel = CreateTestPanel("Test Panel"); 123 ASSERT_TRUE(panel); 124 125 gfx::Rect bounds = panel->GetBounds(); 126 EXPECT_TRUE(bounds.width() > 0); 127 EXPECT_TRUE(bounds.height() > 0); 128 129 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 130 ASSERT_TRUE(native_window); 131 // NSWindows created by NSWindowControllers don't have this bit even if 132 // their NIB has it. The controller's lifetime is the window's lifetime. 133 EXPECT_EQ(NO, [[native_window->controller_ window] isReleasedWhenClosed]); 134 135 ClosePanelAndWait(panel); 136 EXPECT_EQ(0, manager->num_panels()); 137 } 138 139 TEST_F(PanelCocoaTest, AssignedBounds) { 140 Panel* panel1 = CreateTestPanel("Test Panel 1"); 141 Panel* panel2 = CreateTestPanel("Test Panel 2"); 142 Panel* panel3 = CreateTestPanel("Test Panel 3"); 143 144 gfx::Rect bounds1 = panel1->GetBounds(); 145 gfx::Rect bounds2 = panel2->GetBounds(); 146 gfx::Rect bounds3 = panel3->GetBounds(); 147 148 // This checks panelManager calculating and assigning bounds right. 149 // Panels should stack on the bottom right to left. 150 EXPECT_LT(bounds3.x() + bounds3.width(), bounds2.x()); 151 EXPECT_LT(bounds2.x() + bounds2.width(), bounds1.x()); 152 EXPECT_EQ(bounds1.y(), bounds2.y()); 153 EXPECT_EQ(bounds2.y(), bounds3.y()); 154 155 // After panel2 is closed, panel3 should take its place. 156 ClosePanelAndWait(panel2); 157 bounds3 = panel3->GetBounds(); 158 EXPECT_EQ(bounds2, bounds3); 159 160 // After panel1 is closed, panel3 should take its place. 161 ClosePanelAndWait(panel1); 162 EXPECT_EQ(bounds1, panel3->GetBounds()); 163 164 ClosePanelAndWait(panel3); 165 } 166 167 // Same test as AssignedBounds, but checks actual bounds on native OS windows. 168 TEST_F(PanelCocoaTest, NativeBounds) { 169 Panel* panel1 = CreateTestPanel("Test Panel 1"); 170 Panel* panel2 = CreateTestPanel("Test Panel 2"); 171 Panel* panel3 = CreateTestPanel("Test Panel 3"); 172 173 PanelCocoa* native_window1 = static_cast<PanelCocoa*>(panel1->native_panel()); 174 PanelCocoa* native_window2 = static_cast<PanelCocoa*>(panel2->native_panel()); 175 PanelCocoa* native_window3 = static_cast<PanelCocoa*>(panel3->native_panel()); 176 177 NSRect bounds1 = [[native_window1->controller_ window] frame]; 178 NSRect bounds2 = [[native_window2->controller_ window] frame]; 179 NSRect bounds3 = [[native_window3->controller_ window] frame]; 180 181 EXPECT_LT(bounds3.origin.x + bounds3.size.width, bounds2.origin.x); 182 EXPECT_LT(bounds2.origin.x + bounds2.size.width, bounds1.origin.x); 183 EXPECT_EQ(bounds1.origin.y, bounds2.origin.y); 184 EXPECT_EQ(bounds2.origin.y, bounds3.origin.y); 185 186 { 187 // After panel2 is closed, panel3 should take its place. 188 PanelAnimatedBoundsObserver bounds_observer(panel3); 189 ClosePanelAndWait(panel2); 190 bounds_observer.Wait(); 191 bounds3 = [[native_window3->controller_ window] frame]; 192 EXPECT_EQ(bounds2.origin.x, bounds3.origin.x); 193 EXPECT_EQ(bounds2.origin.y, bounds3.origin.y); 194 EXPECT_EQ(bounds2.size.width, bounds3.size.width); 195 EXPECT_EQ(bounds2.size.height, bounds3.size.height); 196 } 197 198 { 199 // After panel1 is closed, panel3 should take its place. 200 PanelAnimatedBoundsObserver bounds_observer(panel3); 201 ClosePanelAndWait(panel1); 202 bounds_observer.Wait(); 203 bounds3 = [[native_window3->controller_ window] frame]; 204 EXPECT_EQ(bounds1.origin.x, bounds3.origin.x); 205 EXPECT_EQ(bounds1.origin.y, bounds3.origin.y); 206 EXPECT_EQ(bounds1.size.width, bounds3.size.width); 207 EXPECT_EQ(bounds1.size.height, bounds3.size.height); 208 } 209 210 ClosePanelAndWait(panel3); 211 } 212 213 // Verify the titlebar is being created. 214 TEST_F(PanelCocoaTest, TitlebarViewCreate) { 215 Panel* panel = CreateTestPanel("Test Panel"); 216 217 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 218 219 PanelTitlebarViewCocoa* titlebar = [native_window->controller_ titlebarView]; 220 EXPECT_TRUE(titlebar); 221 EXPECT_EQ(native_window->controller_, [titlebar controller]); 222 223 ClosePanelAndWait(panel); 224 } 225 226 // Verify the sizing of titlebar - should be affixed on top of regular titlebar. 227 TEST_F(PanelCocoaTest, TitlebarViewSizing) { 228 Panel* panel = CreateTestPanel("Test Panel"); 229 230 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 231 PanelTitlebarViewCocoa* titlebar = [native_window->controller_ titlebarView]; 232 233 NSView* contentView = [[native_window->controller_ window] contentView]; 234 VerifyTitlebarLocation(contentView, titlebar); 235 236 // In local coordinate system, width of titlebar should match width of 237 // content view of the window. They both use the same scale factor. 238 EXPECT_EQ(NSWidth([contentView bounds]), NSWidth([titlebar bounds])); 239 240 NSRect oldTitleFrame = [[titlebar title] frame]; 241 NSRect oldIconFrame = [[titlebar icon] frame]; 242 243 // Now resize the Panel, see that titlebar follows. 244 const int kDelta = 153; // random number 245 gfx::Rect bounds = panel->GetBounds(); 246 // Grow panel in a way so that its titlebar moves and grows. 247 bounds.set_x(bounds.x() - kDelta); 248 bounds.set_y(bounds.y() - kDelta); 249 bounds.set_width(bounds.width() + kDelta); 250 bounds.set_height(bounds.height() + kDelta); 251 252 PanelAnimatedBoundsObserver bounds_observer(panel); 253 native_window->SetPanelBounds(bounds); 254 bounds_observer.Wait(); 255 256 // Verify the panel resized. 257 NSRect window_frame = [[native_window->controller_ window] frame]; 258 EXPECT_EQ(NSWidth(window_frame), bounds.width()); 259 EXPECT_EQ(NSHeight(window_frame), bounds.height()); 260 261 // Verify the titlebar is still on top of regular titlebar. 262 VerifyTitlebarLocation(contentView, titlebar); 263 264 // Verify that the title/icon frames were updated. 265 NSRect newTitleFrame = [[titlebar title] frame]; 266 NSRect newIconFrame = [[titlebar icon] frame]; 267 268 EXPECT_EQ(newTitleFrame.origin.x - newIconFrame.origin.x, 269 oldTitleFrame.origin.x - oldIconFrame.origin.x); 270 // Icon and Text should remain at the same left-aligned position. 271 EXPECT_EQ(newTitleFrame.origin.x, oldTitleFrame.origin.x); 272 EXPECT_EQ(newIconFrame.origin.x, oldIconFrame.origin.x); 273 274 ClosePanelAndWait(panel); 275 } 276 277 // Verify closing behavior of titlebar close button. 278 TEST_F(PanelCocoaTest, TitlebarViewClose) { 279 Panel* panel = CreateTestPanel("Test Panel"); 280 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 281 282 PanelTitlebarViewCocoa* titlebar = [native_window->controller_ titlebarView]; 283 EXPECT_TRUE(titlebar); 284 285 PanelManager* manager = PanelManager::GetInstance(); 286 EXPECT_EQ(1, manager->num_panels()); 287 // Simulate clicking Close Button and wait until the Panel closes. 288 content::WindowedNotificationObserver signal( 289 chrome::NOTIFICATION_PANEL_CLOSED, 290 content::Source<Panel>(panel)); 291 [titlebar simulateCloseButtonClick]; 292 signal.Wait(); 293 EXPECT_EQ(0, manager->num_panels()); 294 } 295 296 // Verify some menu items being properly enabled/disabled for panels. 297 TEST_F(PanelCocoaTest, MenuItems) { 298 Panel* panel = CreateTestPanel("Test Panel"); 299 300 base::scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@""]); 301 NSMenuItem* close_tab_menu_item = CreateMenuItem(menu, IDC_CLOSE_TAB); 302 NSMenuItem* new_tab_menu_item = CreateMenuItem(menu, IDC_NEW_TAB); 303 NSMenuItem* new_tab_window_item = CreateMenuItem(menu, IDC_NEW_WINDOW); 304 NSMenuItem* new_tab_incognito_window_item = 305 CreateMenuItem(menu, IDC_NEW_INCOGNITO_WINDOW); 306 NSMenuItem* close_window_menu_item = CreateMenuItem(menu, IDC_CLOSE_WINDOW); 307 NSMenuItem* find_menu_item = CreateMenuItem(menu, IDC_FIND); 308 NSMenuItem* find_previous_menu_item = CreateMenuItem(menu, IDC_FIND_PREVIOUS); 309 NSMenuItem* find_next_menu_item = CreateMenuItem(menu, IDC_FIND_NEXT); 310 NSMenuItem* fullscreen_menu_item = CreateMenuItem(menu, IDC_FULLSCREEN); 311 NSMenuItem* presentation_menu_item = 312 CreateMenuItem(menu, IDC_PRESENTATION_MODE); 313 NSMenuItem* sync_menu_item = CreateMenuItem(menu, IDC_SHOW_SYNC_SETUP); 314 NSMenuItem* dev_tools_item = CreateMenuItem(menu, IDC_DEV_TOOLS); 315 NSMenuItem* dev_tools_console_item = 316 CreateMenuItem(menu, IDC_DEV_TOOLS_CONSOLE); 317 318 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 319 PanelWindowControllerCocoa* panel_controller = native_window->controller_; 320 for (NSMenuItem *item in [menu itemArray]) 321 [item setTarget:panel_controller]; 322 323 [menu update]; // Trigger validation of menu items. 324 EXPECT_FALSE([close_tab_menu_item isEnabled]); 325 EXPECT_TRUE([close_window_menu_item isEnabled]); 326 // No find support. Panels don't have a find bar. 327 EXPECT_FALSE([find_menu_item isEnabled]); 328 EXPECT_FALSE([find_previous_menu_item isEnabled]); 329 EXPECT_FALSE([find_next_menu_item isEnabled]); 330 EXPECT_FALSE([fullscreen_menu_item isEnabled]); 331 EXPECT_FALSE([presentation_menu_item isEnabled]); 332 EXPECT_FALSE([sync_menu_item isEnabled]); 333 // These are not enabled by Panel, so they are expected to be disabled for 334 // this unit_test. In real Chrome app, they are enabled by Chrome NSApp 335 // controller. PanelCocoaBrowsertest.MenuItems verifies that. 336 EXPECT_FALSE([new_tab_menu_item isEnabled]); 337 EXPECT_FALSE([new_tab_window_item isEnabled]); 338 EXPECT_FALSE([new_tab_incognito_window_item isEnabled]); 339 340 EXPECT_TRUE([dev_tools_item isEnabled]); 341 EXPECT_TRUE([dev_tools_console_item isEnabled]); 342 343 // Verify that commandDispatch on an invalid menu item does not crash. 344 [NSApp sendAction:[sync_menu_item action] 345 to:[sync_menu_item target] 346 from:sync_menu_item]; 347 348 ClosePanelAndWait(panel); 349 } 350 351 TEST_F(PanelCocoaTest, KeyEvent) { 352 Panel* panel = CreateTestPanel("Test Panel"); 353 NSEvent* event = [NSEvent keyEventWithType:NSKeyDown 354 location:NSZeroPoint 355 modifierFlags:NSControlKeyMask 356 timestamp:0.0 357 windowNumber:0 358 context:nil 359 characters:@"" 360 charactersIgnoringModifiers:@"" 361 isARepeat:NO 362 keyCode:kVK_Tab]; 363 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 364 [BrowserWindowUtils handleKeyboardEvent:event 365 inWindow:[native_window->controller_ window]]; 366 ClosePanelAndWait(panel); 367 } 368 369 TEST_F(PanelCocoaTest, SetTitle) { 370 NSString *appName = @"Test Panel"; 371 Panel* panel = CreateTestPanel(base::SysNSStringToUTF8(appName)); 372 ASSERT_TRUE(panel); 373 374 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 375 ASSERT_TRUE(native_window); 376 NSString* previousTitle = [[native_window->controller_ window] title]; 377 EXPECT_NSNE(appName, previousTitle); 378 [native_window->controller_ updateTitleBar]; 379 chrome::testing::NSRunLoopRunAllPending(); 380 NSString* currentTitle = [[native_window->controller_ window] title]; 381 EXPECT_NSEQ(appName, currentTitle); 382 EXPECT_NSNE(currentTitle, previousTitle); 383 ClosePanelAndWait(panel); 384 } 385 386 TEST_F(PanelCocoaTest, ActivatePanel) { 387 Panel* panel = CreateTestPanel("Test Panel"); 388 Panel* panel2 = CreateTestPanel("Test Panel 2"); 389 ASSERT_TRUE(panel); 390 ASSERT_TRUE(panel2); 391 392 PanelCocoa* native_window = static_cast<PanelCocoa*>(panel->native_panel()); 393 ASSERT_TRUE(native_window); 394 PanelCocoa* native_window2 = static_cast<PanelCocoa*>(panel2->native_panel()); 395 ASSERT_TRUE(native_window2); 396 397 // No one has a good answer why but apparently windows can't take keyboard 398 // focus outside of interactive UI tests. BrowserWindowController uses the 399 // same way of testing this. 400 native_window->ActivatePanel(); 401 NSWindow* frontmostWindow = [[NSApp orderedWindows] objectAtIndex:0]; 402 EXPECT_NSEQ(frontmostWindow, [native_window->controller_ window]); 403 404 native_window2->ActivatePanel(); 405 frontmostWindow = [[NSApp orderedWindows] objectAtIndex:0]; 406 EXPECT_NSEQ(frontmostWindow, [native_window2->controller_ window]); 407 408 ClosePanelAndWait(panel); 409 ClosePanelAndWait(panel2); 410 } 411