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#import "chrome/browser/ui/cocoa/base_bubble_controller.h" 6 7#include "base/mac/mac_util.h" 8#import "base/mac/scoped_nsobject.h" 9#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" 10#import "chrome/browser/ui/cocoa/info_bubble_view.h" 11#import "chrome/browser/ui/cocoa/info_bubble_window.h" 12#import "ui/events/test/cocoa_test_event_utils.h" 13 14namespace { 15const CGFloat kBubbleWindowWidth = 100; 16const CGFloat kBubbleWindowHeight = 50; 17const CGFloat kAnchorPointX = 400; 18const CGFloat kAnchorPointY = 300; 19} // namespace 20 21@interface ContextMenuController : NSObject<NSMenuDelegate> { 22 @private 23 NSMenu* menu_; 24 NSWindow* window_; 25 BOOL isMenuOpen_; 26 BOOL didOpen_; 27} 28 29- (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window; 30 31- (BOOL)isMenuOpen; 32- (BOOL)didOpen; 33- (BOOL)isWindowVisible; 34 35// NSMenuDelegate methods 36- (void)menuWillOpen:(NSMenu*)menu; 37- (void)menuDidClose:(NSMenu*)menu; 38 39@end 40 41@implementation ContextMenuController 42 43- (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window { 44 if (self = [super init]) { 45 menu_ = menu; 46 window_ = window; 47 isMenuOpen_ = NO; 48 didOpen_ = NO; 49 [menu_ setDelegate:self]; 50 } 51 return self; 52} 53 54- (BOOL)isMenuOpen { 55 return isMenuOpen_; 56} 57 58- (BOOL)didOpen { 59 return didOpen_; 60} 61 62- (BOOL)isWindowVisible { 63 if (window_) { 64 return [window_ isVisible]; 65 } 66 return NO; 67} 68 69- (void)menuWillOpen:(NSMenu*)menu { 70 isMenuOpen_ = YES; 71 didOpen_ = NO; 72 73 NSArray* modes = @[NSEventTrackingRunLoopMode, NSDefaultRunLoopMode]; 74 [menu_ performSelector:@selector(cancelTracking) 75 withObject:nil 76 afterDelay:0.1 77 inModes:modes]; 78} 79 80- (void)menuDidClose:(NSMenu*)menu { 81 isMenuOpen_ = NO; 82 didOpen_ = YES; 83} 84 85@end 86 87class BaseBubbleControllerTest : public CocoaTest { 88 public: 89 BaseBubbleControllerTest() : controller_(nil) {} 90 91 virtual void SetUp() OVERRIDE { 92 bubble_window_.reset([[InfoBubbleWindow alloc] 93 initWithContentRect:NSMakeRect(0, 0, kBubbleWindowWidth, 94 kBubbleWindowHeight) 95 styleMask:NSBorderlessWindowMask 96 backing:NSBackingStoreBuffered 97 defer:YES]); 98 [bubble_window_ setAllowedAnimations:0]; 99 100 // The bubble controller will release itself when the window closes. 101 controller_ = [[BaseBubbleController alloc] 102 initWithWindow:bubble_window_ 103 parentWindow:test_window() 104 anchoredAt:NSMakePoint(kAnchorPointX, kAnchorPointY)]; 105 EXPECT_TRUE([controller_ bubble]); 106 EXPECT_EQ(bubble_window_.get(), [controller_ window]); 107 } 108 109 virtual void TearDown() OVERRIDE { 110 // Close our windows. 111 [controller_ close]; 112 bubble_window_.reset(); 113 CocoaTest::TearDown(); 114 } 115 116 // Closing the bubble will autorelease the controller. Give callers a keep- 117 // alive to run checks after closing. 118 base::scoped_nsobject<BaseBubbleController> ShowBubble() WARN_UNUSED_RESULT { 119 base::scoped_nsobject<BaseBubbleController> keep_alive( 120 [controller_ retain]); 121 EXPECT_FALSE([bubble_window_ isVisible]); 122 [controller_ showWindow:nil]; 123 EXPECT_TRUE([bubble_window_ isVisible]); 124 return keep_alive; 125 } 126 127 // Fake the key state notification. Because unit_tests is a "daemon" process 128 // type, its windows can never become key (nor can the app become active). 129 // Instead of the hacks below, one could make a browser_test or transform the 130 // process type, but this seems easiest and is best suited to a unit test. 131 // 132 // On Lion and above, which have the event taps, simply post a notification 133 // that will cause the controller to call |-windowDidResignKey:|. Earlier 134 // OSes can call through directly. 135 void SimulateKeyStatusChange() { 136 NSNotification* notif = 137 [NSNotification notificationWithName:NSWindowDidResignKeyNotification 138 object:[controller_ window]]; 139 if (base::mac::IsOSLionOrLater()) 140 [[NSNotificationCenter defaultCenter] postNotification:notif]; 141 else 142 [controller_ windowDidResignKey:notif]; 143 } 144 145 protected: 146 base::scoped_nsobject<InfoBubbleWindow> bubble_window_; 147 BaseBubbleController* controller_; 148 149 private: 150 DISALLOW_COPY_AND_ASSIGN(BaseBubbleControllerTest); 151}; 152 153// Test that kAlignEdgeToAnchorEdge and a left bubble arrow correctly aligns the 154// left edge of the buble to the anchor point. 155TEST_F(BaseBubbleControllerTest, LeftAlign) { 156 [[controller_ bubble] setArrowLocation:info_bubble::kTopLeft]; 157 [[controller_ bubble] setAlignment:info_bubble::kAlignEdgeToAnchorEdge]; 158 [controller_ showWindow:nil]; 159 160 NSRect frame = [[controller_ window] frame]; 161 // Make sure the bubble size hasn't changed. 162 EXPECT_EQ(frame.size.width, kBubbleWindowWidth); 163 EXPECT_EQ(frame.size.height, kBubbleWindowHeight); 164 // Make sure the bubble is left aligned. 165 EXPECT_EQ(NSMinX(frame), kAnchorPointX); 166 EXPECT_GE(NSMaxY(frame), kAnchorPointY); 167} 168 169// Test that kAlignEdgeToAnchorEdge and a right bubble arrow correctly aligns 170// the right edge of the buble to the anchor point. 171TEST_F(BaseBubbleControllerTest, RightAlign) { 172 [[controller_ bubble] setArrowLocation:info_bubble::kTopRight]; 173 [[controller_ bubble] setAlignment:info_bubble::kAlignEdgeToAnchorEdge]; 174 [controller_ showWindow:nil]; 175 176 NSRect frame = [[controller_ window] frame]; 177 // Make sure the bubble size hasn't changed. 178 EXPECT_EQ(frame.size.width, kBubbleWindowWidth); 179 EXPECT_EQ(frame.size.height, kBubbleWindowHeight); 180 // Make sure the bubble is left aligned. 181 EXPECT_EQ(NSMaxX(frame), kAnchorPointX); 182 EXPECT_GE(NSMaxY(frame), kAnchorPointY); 183} 184 185// Test that kAlignArrowToAnchor and a left bubble arrow correctly aligns 186// the bubble arrow to the anchor point. 187TEST_F(BaseBubbleControllerTest, AnchorAlignLeftArrow) { 188 [[controller_ bubble] setArrowLocation:info_bubble::kTopLeft]; 189 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor]; 190 [controller_ showWindow:nil]; 191 192 NSRect frame = [[controller_ window] frame]; 193 // Make sure the bubble size hasn't changed. 194 EXPECT_EQ(frame.size.width, kBubbleWindowWidth); 195 EXPECT_EQ(frame.size.height, kBubbleWindowHeight); 196 // Make sure the bubble arrow points to the anchor. 197 EXPECT_EQ(NSMinX(frame) + info_bubble::kBubbleArrowXOffset + 198 roundf(info_bubble::kBubbleArrowWidth / 2.0), kAnchorPointX); 199 EXPECT_GE(NSMaxY(frame), kAnchorPointY); 200} 201 202// Test that kAlignArrowToAnchor and a right bubble arrow correctly aligns 203// the bubble arrow to the anchor point. 204TEST_F(BaseBubbleControllerTest, AnchorAlignRightArrow) { 205 [[controller_ bubble] setArrowLocation:info_bubble::kTopRight]; 206 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor]; 207 [controller_ showWindow:nil]; 208 209 NSRect frame = [[controller_ window] frame]; 210 // Make sure the bubble size hasn't changed. 211 EXPECT_EQ(frame.size.width, kBubbleWindowWidth); 212 EXPECT_EQ(frame.size.height, kBubbleWindowHeight); 213 // Make sure the bubble arrow points to the anchor. 214 EXPECT_EQ(NSMaxX(frame) - info_bubble::kBubbleArrowXOffset - 215 floorf(info_bubble::kBubbleArrowWidth / 2.0), kAnchorPointX); 216 EXPECT_GE(NSMaxY(frame), kAnchorPointY); 217} 218 219// Test that kAlignArrowToAnchor and a center bubble arrow correctly align 220// the bubble towards the anchor point. 221TEST_F(BaseBubbleControllerTest, AnchorAlignCenterArrow) { 222 [[controller_ bubble] setArrowLocation:info_bubble::kTopCenter]; 223 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor]; 224 [controller_ showWindow:nil]; 225 226 NSRect frame = [[controller_ window] frame]; 227 // Make sure the bubble size hasn't changed. 228 EXPECT_EQ(frame.size.width, kBubbleWindowWidth); 229 EXPECT_EQ(frame.size.height, kBubbleWindowHeight); 230 // Make sure the bubble arrow points to the anchor. 231 EXPECT_EQ(NSMidX(frame), kAnchorPointX); 232 EXPECT_GE(NSMaxY(frame), kAnchorPointY); 233} 234 235// Test that the window is given an initial position before being shown. This 236// ensures offscreen initialization is done using correct screen metrics. 237TEST_F(BaseBubbleControllerTest, PositionedBeforeShow) { 238 // Verify default alignment settings, used when initialized in SetUp(). 239 EXPECT_EQ(info_bubble::kTopRight, [[controller_ bubble] arrowLocation]); 240 EXPECT_EQ(info_bubble::kAlignArrowToAnchor, [[controller_ bubble] alignment]); 241 242 // Verify the default frame (positioned relative to the test_window() origin). 243 NSRect frame = [[controller_ window] frame]; 244 EXPECT_EQ(NSMaxX(frame) - info_bubble::kBubbleArrowXOffset - 245 floorf(info_bubble::kBubbleArrowWidth / 2.0), kAnchorPointX); 246 EXPECT_EQ(NSMaxY(frame), kAnchorPointY); 247} 248 249// Tests that when a new window gets key state (and the bubble resigns) that 250// the key window changes. 251TEST_F(BaseBubbleControllerTest, ResignKeyCloses) { 252 base::scoped_nsobject<NSWindow> other_window( 253 [[NSWindow alloc] initWithContentRect:NSMakeRect(500, 500, 500, 500) 254 styleMask:NSTitledWindowMask 255 backing:NSBackingStoreBuffered 256 defer:YES]); 257 258 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble(); 259 EXPECT_FALSE([other_window isVisible]); 260 261 [other_window makeKeyAndOrderFront:nil]; 262 SimulateKeyStatusChange(); 263 264 EXPECT_FALSE([bubble_window_ isVisible]); 265 EXPECT_TRUE([other_window isVisible]); 266} 267 268// Test that clicking outside the window causes the bubble to close if 269// shouldCloseOnResignKey is YES. 270TEST_F(BaseBubbleControllerTest, LionClickOutsideClosesWithoutContextMenu) { 271 // The event tap is only installed on 10.7+. 272 if (!base::mac::IsOSLionOrLater()) 273 return; 274 275 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble(); 276 NSWindow* window = [controller_ window]; 277 278 EXPECT_TRUE([controller_ shouldCloseOnResignKey]); // Verify default value. 279 [controller_ setShouldCloseOnResignKey:NO]; 280 NSEvent* event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( 281 NSMakePoint(10, 10), test_window()); 282 [NSApp sendEvent:event]; 283 284 EXPECT_TRUE([window isVisible]); 285 286 event = cocoa_test_event_utils::RightMouseDownAtPointInWindow( 287 NSMakePoint(10, 10), test_window()); 288 [NSApp sendEvent:event]; 289 290 EXPECT_TRUE([window isVisible]); 291 292 [controller_ setShouldCloseOnResignKey:YES]; 293 event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( 294 NSMakePoint(10, 10), test_window()); 295 [NSApp sendEvent:event]; 296 297 EXPECT_FALSE([window isVisible]); 298 299 [controller_ showWindow:nil]; // Show it again 300 EXPECT_TRUE([window isVisible]); 301 EXPECT_TRUE([controller_ shouldCloseOnResignKey]); // Verify. 302 303 event = cocoa_test_event_utils::RightMouseDownAtPointInWindow( 304 NSMakePoint(10, 10), test_window()); 305 [NSApp sendEvent:event]; 306 307 EXPECT_FALSE([window isVisible]); 308} 309 310// Test that right-clicking the window with displaying a context menu causes 311// the bubble to close. 312TEST_F(BaseBubbleControllerTest, LionRightClickOutsideClosesWithContextMenu) { 313 // The event tap is only installed on 10.7+. 314 if (!base::mac::IsOSLionOrLater()) 315 return; 316 317 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble(); 318 NSWindow* window = [controller_ window]; 319 320 base::scoped_nsobject<NSMenu> context_menu( 321 [[NSMenu alloc] initWithTitle:@""]); 322 [context_menu addItemWithTitle:@"ContextMenuTest" 323 action:nil 324 keyEquivalent:@""]; 325 base::scoped_nsobject<ContextMenuController> menu_controller( 326 [[ContextMenuController alloc] initWithMenu:context_menu 327 andWindow:window]); 328 329 // Set the menu as the contextual menu of contentView of test_window(). 330 [[test_window() contentView] setMenu:context_menu]; 331 332 // RightMouseDown in test_window() would close the bubble window and then 333 // dispaly the contextual menu. 334 NSEvent* event = cocoa_test_event_utils::RightMouseDownAtPointInWindow( 335 NSMakePoint(10, 10), test_window()); 336 // Verify bubble's window is closed when contextual menu is open. 337 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{ 338 EXPECT_TRUE([menu_controller isMenuOpen]); 339 EXPECT_FALSE([menu_controller isWindowVisible]); 340 }); 341 342 EXPECT_FALSE([menu_controller isMenuOpen]); 343 EXPECT_FALSE([menu_controller didOpen]); 344 345 [NSApp sendEvent:event]; 346 347 // When we got here, menu has already run its RunLoop. 348 // See -[ContextualMenuController menuWillOpen:]. 349 EXPECT_FALSE([window isVisible]); 350 351 EXPECT_FALSE([menu_controller isMenuOpen]); 352 EXPECT_TRUE([menu_controller didOpen]); 353} 354 355// Test that the bubble is not dismissed when it has an attached sheet, or when 356// a sheet loses key status (since the sheet is not attached when that happens). 357TEST_F(BaseBubbleControllerTest, BubbleStaysOpenWithSheet) { 358 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble(); 359 360 // Make a dummy NSPanel for the sheet. Don't use [NSOpenPanel openPanel], 361 // otherwise a stray FI_TFloatingInputWindow is created which the unit test 362 // harness doesn't like. 363 base::scoped_nsobject<NSPanel> panel( 364 [[NSPanel alloc] initWithContentRect:NSMakeRect(0, 0, 100, 50) 365 styleMask:NSTitledWindowMask 366 backing:NSBackingStoreBuffered 367 defer:YES]); 368 EXPECT_FALSE([panel isReleasedWhenClosed]); // scoped_nsobject releases it. 369 370 // With a NSOpenPanel, we would call -[NSSavePanel beginSheetModalForWindow] 371 // here. In 10.9, we would call [NSWindow beginSheet:]. For 10.6, this: 372 [[NSApplication sharedApplication] beginSheet:panel 373 modalForWindow:bubble_window_ 374 modalDelegate:nil 375 didEndSelector:NULL 376 contextInfo:NULL]; 377 378 EXPECT_TRUE([bubble_window_ isVisible]); 379 EXPECT_TRUE([panel isVisible]); 380 // Losing key status while there is an attached window should not close the 381 // bubble. 382 SimulateKeyStatusChange(); 383 EXPECT_TRUE([bubble_window_ isVisible]); 384 EXPECT_TRUE([panel isVisible]); 385 386 // Closing the attached sheet should not close the bubble. 387 [[NSApplication sharedApplication] endSheet:panel]; 388 [panel close]; 389 390 EXPECT_FALSE([bubble_window_ attachedSheet]); 391 EXPECT_TRUE([bubble_window_ isVisible]); 392 EXPECT_FALSE([panel isVisible]); 393 394 // Now that the sheet is gone, a key status change should close the bubble. 395 SimulateKeyStatusChange(); 396 EXPECT_FALSE([bubble_window_ isVisible]); 397} 398