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.
5 #import "chrome/browser/ui/cocoa/base_bubble_controller.h"
7 #include "base/mac/mac_util.h"
8 #import "base/mac/scoped_nsobject.h"
9 #import "base/mac/scoped_objc_class_swizzler.h"
10 #import "base/mac/sdk_forward_declarations.h"
11 #import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
12 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
13 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
14 #import "ui/events/test/cocoa_test_event_utils.h"
17 const CGFloat kBubbleWindowWidth = 100;
18 const CGFloat kBubbleWindowHeight = 50;
19 const CGFloat kAnchorPointX = 400;
20 const CGFloat kAnchorPointY = 300;
22 NSWindow* g_key_window = nil;
25 @interface ContextMenuController : NSObject<NSMenuDelegate> {
33 - (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window;
37 - (BOOL)isWindowVisible;
39 // NSMenuDelegate methods
40 - (void)menuWillOpen:(NSMenu*)menu;
41 - (void)menuDidClose:(NSMenu*)menu;
45 @implementation ContextMenuController
47 - (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window {
48 if (self = [super init]) {
53 [menu_ setDelegate:self];
66 - (BOOL)isWindowVisible {
68 return [window_ isVisible];
73 - (void)menuWillOpen:(NSMenu*)menu {
77 NSArray* modes = @[NSEventTrackingRunLoopMode, NSDefaultRunLoopMode];
78 [menu_ performSelector:@selector(cancelTracking)
84 - (void)menuDidClose:(NSMenu*)menu {
91 // A helper class to swizzle [NSApplication keyWindow].
92 @interface FakeKeyWindow : NSObject
93 @property(readonly) NSWindow* keyWindow;
96 @implementation FakeKeyWindow
97 - (NSWindow*)keyWindow {
103 class BaseBubbleControllerTest : public CocoaTest {
105 BaseBubbleControllerTest() : controller_(nil) {}
107 void SetUp() override {
108 bubble_window_.reset([[InfoBubbleWindow alloc]
109 initWithContentRect:NSMakeRect(0, 0, kBubbleWindowWidth,
111 styleMask:NSBorderlessWindowMask
112 backing:NSBackingStoreBuffered
114 [bubble_window_ setAllowedAnimations:0];
116 // The bubble controller will release itself when the window closes.
117 controller_ = [[BaseBubbleController alloc]
118 initWithWindow:bubble_window_
119 parentWindow:test_window()
120 anchoredAt:NSMakePoint(kAnchorPointX, kAnchorPointY)];
121 EXPECT_TRUE([controller_ bubble]);
122 EXPECT_EQ(bubble_window_.get(), [controller_ window]);
125 void TearDown() override {
126 // Close our windows.
128 bubble_window_.reset();
129 CocoaTest::TearDown();
132 // Closing the bubble will autorelease the controller. Give callers a keep-
133 // alive to run checks after closing.
134 base::scoped_nsobject<BaseBubbleController> ShowBubble() WARN_UNUSED_RESULT {
135 base::scoped_nsobject<BaseBubbleController> keep_alive(
136 [controller_ retain]);
137 EXPECT_FALSE([bubble_window_ isVisible]);
138 [controller_ showWindow:nil];
139 EXPECT_TRUE([bubble_window_ isVisible]);
143 // Fake the key state notification. Because unit_tests is a "daemon" process
144 // type, its windows can never become key (nor can the app become active).
145 // Instead of the hacks below, one could make a browser_test or transform the
146 // process type, but this seems easiest and is best suited to a unit test.
148 // On Lion and above, which have the event taps, simply post a notification
149 // that will cause the controller to call |-windowDidResignKey:|. Earlier
150 // OSes can call through directly.
151 void SimulateKeyStatusChange() {
152 NSNotification* notif =
153 [NSNotification notificationWithName:NSWindowDidResignKeyNotification
154 object:[controller_ window]];
155 if (base::mac::IsOSLionOrLater())
156 [[NSNotificationCenter defaultCenter] postNotification:notif];
158 [controller_ windowDidResignKey:notif];
162 base::scoped_nsobject<InfoBubbleWindow> bubble_window_;
163 BaseBubbleController* controller_;
166 DISALLOW_COPY_AND_ASSIGN(BaseBubbleControllerTest);
169 // Test that kAlignEdgeToAnchorEdge and a left bubble arrow correctly aligns the
170 // left edge of the buble to the anchor point.
171 TEST_F(BaseBubbleControllerTest, LeftAlign) {
172 [[controller_ bubble] setArrowLocation:info_bubble::kTopLeft];
173 [[controller_ bubble] setAlignment:info_bubble::kAlignEdgeToAnchorEdge];
174 [controller_ showWindow:nil];
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(NSMinX(frame), kAnchorPointX);
182 EXPECT_GE(NSMaxY(frame), kAnchorPointY);
185 // Test that kAlignEdgeToAnchorEdge and a right bubble arrow correctly aligns
186 // the right edge of the buble to the anchor point.
187 TEST_F(BaseBubbleControllerTest, RightAlign) {
188 [[controller_ bubble] setArrowLocation:info_bubble::kTopRight];
189 [[controller_ bubble] setAlignment:info_bubble::kAlignEdgeToAnchorEdge];
190 [controller_ showWindow:nil];
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 is left aligned.
197 EXPECT_EQ(NSMaxX(frame), kAnchorPointX);
198 EXPECT_GE(NSMaxY(frame), kAnchorPointY);
201 // Test that kAlignArrowToAnchor and a left bubble arrow correctly aligns
202 // the bubble arrow to the anchor point.
203 TEST_F(BaseBubbleControllerTest, AnchorAlignLeftArrow) {
204 [[controller_ bubble] setArrowLocation:info_bubble::kTopLeft];
205 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
206 [controller_ showWindow:nil];
208 NSRect frame = [[controller_ window] frame];
209 // Make sure the bubble size hasn't changed.
210 EXPECT_EQ(frame.size.width, kBubbleWindowWidth);
211 EXPECT_EQ(frame.size.height, kBubbleWindowHeight);
212 // Make sure the bubble arrow points to the anchor.
213 EXPECT_EQ(NSMinX(frame) + info_bubble::kBubbleArrowXOffset +
214 roundf(info_bubble::kBubbleArrowWidth / 2.0), kAnchorPointX);
215 EXPECT_GE(NSMaxY(frame), kAnchorPointY);
218 // Test that kAlignArrowToAnchor and a right bubble arrow correctly aligns
219 // the bubble arrow to the anchor point.
220 TEST_F(BaseBubbleControllerTest, AnchorAlignRightArrow) {
221 [[controller_ bubble] setArrowLocation:info_bubble::kTopRight];
222 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
223 [controller_ showWindow:nil];
225 NSRect frame = [[controller_ window] frame];
226 // Make sure the bubble size hasn't changed.
227 EXPECT_EQ(frame.size.width, kBubbleWindowWidth);
228 EXPECT_EQ(frame.size.height, kBubbleWindowHeight);
229 // Make sure the bubble arrow points to the anchor.
230 EXPECT_EQ(NSMaxX(frame) - info_bubble::kBubbleArrowXOffset -
231 floorf(info_bubble::kBubbleArrowWidth / 2.0), kAnchorPointX);
232 EXPECT_GE(NSMaxY(frame), kAnchorPointY);
235 // Test that kAlignArrowToAnchor and a center bubble arrow correctly align
236 // the bubble towards the anchor point.
237 TEST_F(BaseBubbleControllerTest, AnchorAlignCenterArrow) {
238 [[controller_ bubble] setArrowLocation:info_bubble::kTopCenter];
239 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
240 [controller_ showWindow:nil];
242 NSRect frame = [[controller_ window] frame];
243 // Make sure the bubble size hasn't changed.
244 EXPECT_EQ(frame.size.width, kBubbleWindowWidth);
245 EXPECT_EQ(frame.size.height, kBubbleWindowHeight);
246 // Make sure the bubble arrow points to the anchor.
247 EXPECT_EQ(NSMidX(frame), kAnchorPointX);
248 EXPECT_GE(NSMaxY(frame), kAnchorPointY);
251 // Test that the window is given an initial position before being shown. This
252 // ensures offscreen initialization is done using correct screen metrics.
253 TEST_F(BaseBubbleControllerTest, PositionedBeforeShow) {
254 // Verify default alignment settings, used when initialized in SetUp().
255 EXPECT_EQ(info_bubble::kTopRight, [[controller_ bubble] arrowLocation]);
256 EXPECT_EQ(info_bubble::kAlignArrowToAnchor, [[controller_ bubble] alignment]);
258 // Verify the default frame (positioned relative to the test_window() origin).
259 NSRect frame = [[controller_ window] frame];
260 EXPECT_EQ(NSMaxX(frame) - info_bubble::kBubbleArrowXOffset -
261 floorf(info_bubble::kBubbleArrowWidth / 2.0), kAnchorPointX);
262 EXPECT_EQ(NSMaxY(frame), kAnchorPointY);
265 // Tests that when a new window gets key state (and the bubble resigns) that
266 // the key window changes.
267 TEST_F(BaseBubbleControllerTest, ResignKeyCloses) {
268 base::scoped_nsobject<NSWindow> other_window(
269 [[NSWindow alloc] initWithContentRect:NSMakeRect(500, 500, 500, 500)
270 styleMask:NSTitledWindowMask
271 backing:NSBackingStoreBuffered
274 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
275 EXPECT_FALSE([other_window isVisible]);
277 [other_window makeKeyAndOrderFront:nil];
278 SimulateKeyStatusChange();
280 EXPECT_FALSE([bubble_window_ isVisible]);
281 EXPECT_TRUE([other_window isVisible]);
284 // Test that clicking outside the window causes the bubble to close if
285 // shouldCloseOnResignKey is YES.
286 TEST_F(BaseBubbleControllerTest, LionClickOutsideClosesWithoutContextMenu) {
287 // The event tap is only installed on 10.7+.
288 if (!base::mac::IsOSLionOrLater())
291 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
292 NSWindow* window = [controller_ window];
294 EXPECT_TRUE([controller_ shouldCloseOnResignKey]); // Verify default value.
295 [controller_ setShouldCloseOnResignKey:NO];
296 NSEvent* event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
297 NSMakePoint(10, 10), test_window());
298 [NSApp sendEvent:event];
300 EXPECT_TRUE([window isVisible]);
302 event = cocoa_test_event_utils::RightMouseDownAtPointInWindow(
303 NSMakePoint(10, 10), test_window());
304 [NSApp sendEvent:event];
306 EXPECT_TRUE([window isVisible]);
308 [controller_ setShouldCloseOnResignKey:YES];
309 event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
310 NSMakePoint(10, 10), test_window());
311 [NSApp sendEvent:event];
313 EXPECT_FALSE([window isVisible]);
315 [controller_ showWindow:nil]; // Show it again
316 EXPECT_TRUE([window isVisible]);
317 EXPECT_TRUE([controller_ shouldCloseOnResignKey]); // Verify.
319 event = cocoa_test_event_utils::RightMouseDownAtPointInWindow(
320 NSMakePoint(10, 10), test_window());
321 [NSApp sendEvent:event];
323 EXPECT_FALSE([window isVisible]);
326 // Test that right-clicking the window with displaying a context menu causes
327 // the bubble to close.
328 TEST_F(BaseBubbleControllerTest, LionRightClickOutsideClosesWithContextMenu) {
329 // The event tap is only installed on 10.7+.
330 if (!base::mac::IsOSLionOrLater())
333 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
334 NSWindow* window = [controller_ window];
336 base::scoped_nsobject<NSMenu> context_menu(
337 [[NSMenu alloc] initWithTitle:@""]);
338 [context_menu addItemWithTitle:@"ContextMenuTest"
341 base::scoped_nsobject<ContextMenuController> menu_controller(
342 [[ContextMenuController alloc] initWithMenu:context_menu
345 // Set the menu as the contextual menu of contentView of test_window().
346 [[test_window() contentView] setMenu:context_menu];
348 // RightMouseDown in test_window() would close the bubble window and then
349 // dispaly the contextual menu.
350 NSEvent* event = cocoa_test_event_utils::RightMouseDownAtPointInWindow(
351 NSMakePoint(10, 10), test_window());
352 // Verify bubble's window is closed when contextual menu is open.
353 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
354 EXPECT_TRUE([menu_controller isMenuOpen]);
355 EXPECT_FALSE([menu_controller isWindowVisible]);
358 EXPECT_FALSE([menu_controller isMenuOpen]);
359 EXPECT_FALSE([menu_controller didOpen]);
361 [NSApp sendEvent:event];
363 // When we got here, menu has already run its RunLoop.
364 // See -[ContextualMenuController menuWillOpen:].
365 EXPECT_FALSE([window isVisible]);
367 EXPECT_FALSE([menu_controller isMenuOpen]);
368 EXPECT_TRUE([menu_controller didOpen]);
371 // Test that the bubble is not dismissed when it has an attached sheet, or when
372 // a sheet loses key status (since the sheet is not attached when that happens).
373 TEST_F(BaseBubbleControllerTest, BubbleStaysOpenWithSheet) {
374 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
376 // Make a dummy NSPanel for the sheet. Don't use [NSOpenPanel openPanel],
377 // otherwise a stray FI_TFloatingInputWindow is created which the unit test
378 // harness doesn't like.
379 base::scoped_nsobject<NSPanel> panel(
380 [[NSPanel alloc] initWithContentRect:NSMakeRect(0, 0, 100, 50)
381 styleMask:NSTitledWindowMask
382 backing:NSBackingStoreBuffered
384 EXPECT_FALSE([panel isReleasedWhenClosed]); // scoped_nsobject releases it.
386 // With a NSOpenPanel, we would call -[NSSavePanel beginSheetModalForWindow]
387 // here. In 10.9, we would call [NSWindow beginSheet:]. For 10.6, this:
388 [[NSApplication sharedApplication] beginSheet:panel
389 modalForWindow:bubble_window_
394 EXPECT_TRUE([bubble_window_ isVisible]);
395 EXPECT_TRUE([panel isVisible]);
396 // Losing key status while there is an attached window should not close the
398 SimulateKeyStatusChange();
399 EXPECT_TRUE([bubble_window_ isVisible]);
400 EXPECT_TRUE([panel isVisible]);
402 // Closing the attached sheet should not close the bubble.
403 [[NSApplication sharedApplication] endSheet:panel];
406 EXPECT_FALSE([bubble_window_ attachedSheet]);
407 EXPECT_TRUE([bubble_window_ isVisible]);
408 EXPECT_FALSE([panel isVisible]);
410 // Now that the sheet is gone, a key status change should close the bubble.
411 SimulateKeyStatusChange();
412 EXPECT_FALSE([bubble_window_ isVisible]);
415 // Tests that a bubble will close when a window enters fullscreen.
416 TEST_F(BaseBubbleControllerTest, EnterFullscreen) {
417 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
419 EXPECT_TRUE([bubble_window_ isVisible]);
421 // Post the "enter fullscreen" notification.
422 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
423 [center postNotificationName:NSWindowWillEnterFullScreenNotification
424 object:test_window()];
426 EXPECT_FALSE([bubble_window_ isVisible]);
429 // Tests that a bubble will close when a window exits fullscreen.
430 TEST_F(BaseBubbleControllerTest, ExitFullscreen) {
431 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
433 EXPECT_TRUE([bubble_window_ isVisible]);
435 // Post the "exit fullscreen" notification.
436 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
437 [center postNotificationName:NSWindowWillExitFullScreenNotification
438 object:test_window()];
440 EXPECT_FALSE([bubble_window_ isVisible]);
443 // Tests that a bubble will not close when it's becoming a key window.
444 TEST_F(BaseBubbleControllerTest, StayOnFocus) {
445 // The event tap is only installed on 10.7+.
446 if (!base::mac::IsOSLionOrLater())
449 [controller_ setShouldOpenAsKeyWindow:NO];
450 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
452 EXPECT_TRUE([bubble_window_ isVisible]);
453 EXPECT_TRUE([controller_ shouldCloseOnResignKey]); // Verify default value.
455 // Make the bubble a key window.
456 g_key_window = [controller_ window];
457 base::mac::ScopedObjCClassSwizzler swizzler(
458 [NSApplication class], [FakeKeyWindow class], @selector(keyWindow));
460 // Post the "resign key" notification for another window.
461 NSNotification* notif =
462 [NSNotification notificationWithName:NSWindowDidResignKeyNotification
463 object:test_window()];
464 [[NSNotificationCenter defaultCenter] postNotification:notif];
466 EXPECT_TRUE([bubble_window_ isVisible]);