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 "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"
15 const CGFloat kBubbleWindowWidth = 100;
16 const CGFloat kBubbleWindowHeight = 50;
17 const CGFloat kAnchorPointX = 400;
18 const CGFloat kAnchorPointY = 300;
21 @interface ContextMenuController : NSObject<NSMenuDelegate> {
29 - (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window;
33 - (BOOL)isWindowVisible;
35 // NSMenuDelegate methods
36 - (void)menuWillOpen:(NSMenu*)menu;
37 - (void)menuDidClose:(NSMenu*)menu;
41 @implementation ContextMenuController
43 - (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window {
44 if (self = [super init]) {
49 [menu_ setDelegate:self];
62 - (BOOL)isWindowVisible {
64 return [window_ isVisible];
69 - (void)menuWillOpen:(NSMenu*)menu {
73 NSArray* modes = @[NSEventTrackingRunLoopMode, NSDefaultRunLoopMode];
74 [menu_ performSelector:@selector(cancelTracking)
80 - (void)menuDidClose:(NSMenu*)menu {
87 class BaseBubbleControllerTest : public CocoaTest {
89 BaseBubbleControllerTest() : controller_(nil) {}
91 void SetUp() override {
92 bubble_window_.reset([[InfoBubbleWindow alloc]
93 initWithContentRect:NSMakeRect(0, 0, kBubbleWindowWidth,
95 styleMask:NSBorderlessWindowMask
96 backing:NSBackingStoreBuffered
98 [bubble_window_ setAllowedAnimations:0];
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]);
109 void TearDown() override {
110 // Close our windows.
112 bubble_window_.reset();
113 CocoaTest::TearDown();
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]);
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.
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];
142 [controller_ windowDidResignKey:notif];
146 base::scoped_nsobject<InfoBubbleWindow> bubble_window_;
147 BaseBubbleController* controller_;
150 DISALLOW_COPY_AND_ASSIGN(BaseBubbleControllerTest);
153 // Test that kAlignEdgeToAnchorEdge and a left bubble arrow correctly aligns the
154 // left edge of the buble to the anchor point.
155 TEST_F(BaseBubbleControllerTest, LeftAlign) {
156 [[controller_ bubble] setArrowLocation:info_bubble::kTopLeft];
157 [[controller_ bubble] setAlignment:info_bubble::kAlignEdgeToAnchorEdge];
158 [controller_ showWindow:nil];
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);
169 // Test that kAlignEdgeToAnchorEdge and a right bubble arrow correctly aligns
170 // the right edge of the buble to the anchor point.
171 TEST_F(BaseBubbleControllerTest, RightAlign) {
172 [[controller_ bubble] setArrowLocation:info_bubble::kTopRight];
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(NSMaxX(frame), kAnchorPointX);
182 EXPECT_GE(NSMaxY(frame), kAnchorPointY);
185 // Test that kAlignArrowToAnchor and a left bubble arrow correctly aligns
186 // the bubble arrow to the anchor point.
187 TEST_F(BaseBubbleControllerTest, AnchorAlignLeftArrow) {
188 [[controller_ bubble] setArrowLocation:info_bubble::kTopLeft];
189 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
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 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);
202 // Test that kAlignArrowToAnchor and a right bubble arrow correctly aligns
203 // the bubble arrow to the anchor point.
204 TEST_F(BaseBubbleControllerTest, AnchorAlignRightArrow) {
205 [[controller_ bubble] setArrowLocation:info_bubble::kTopRight];
206 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
207 [controller_ showWindow:nil];
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);
219 // Test that kAlignArrowToAnchor and a center bubble arrow correctly align
220 // the bubble towards the anchor point.
221 TEST_F(BaseBubbleControllerTest, AnchorAlignCenterArrow) {
222 [[controller_ bubble] setArrowLocation:info_bubble::kTopCenter];
223 [[controller_ bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
224 [controller_ showWindow:nil];
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);
235 // Test that the window is given an initial position before being shown. This
236 // ensures offscreen initialization is done using correct screen metrics.
237 TEST_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]);
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);
249 // Tests that when a new window gets key state (and the bubble resigns) that
250 // the key window changes.
251 TEST_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
258 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
259 EXPECT_FALSE([other_window isVisible]);
261 [other_window makeKeyAndOrderFront:nil];
262 SimulateKeyStatusChange();
264 EXPECT_FALSE([bubble_window_ isVisible]);
265 EXPECT_TRUE([other_window isVisible]);
268 // Test that clicking outside the window causes the bubble to close if
269 // shouldCloseOnResignKey is YES.
270 TEST_F(BaseBubbleControllerTest, LionClickOutsideClosesWithoutContextMenu) {
271 // The event tap is only installed on 10.7+.
272 if (!base::mac::IsOSLionOrLater())
275 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
276 NSWindow* window = [controller_ window];
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];
284 EXPECT_TRUE([window isVisible]);
286 event = cocoa_test_event_utils::RightMouseDownAtPointInWindow(
287 NSMakePoint(10, 10), test_window());
288 [NSApp sendEvent:event];
290 EXPECT_TRUE([window isVisible]);
292 [controller_ setShouldCloseOnResignKey:YES];
293 event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
294 NSMakePoint(10, 10), test_window());
295 [NSApp sendEvent:event];
297 EXPECT_FALSE([window isVisible]);
299 [controller_ showWindow:nil]; // Show it again
300 EXPECT_TRUE([window isVisible]);
301 EXPECT_TRUE([controller_ shouldCloseOnResignKey]); // Verify.
303 event = cocoa_test_event_utils::RightMouseDownAtPointInWindow(
304 NSMakePoint(10, 10), test_window());
305 [NSApp sendEvent:event];
307 EXPECT_FALSE([window isVisible]);
310 // Test that right-clicking the window with displaying a context menu causes
311 // the bubble to close.
312 TEST_F(BaseBubbleControllerTest, LionRightClickOutsideClosesWithContextMenu) {
313 // The event tap is only installed on 10.7+.
314 if (!base::mac::IsOSLionOrLater())
317 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
318 NSWindow* window = [controller_ window];
320 base::scoped_nsobject<NSMenu> context_menu(
321 [[NSMenu alloc] initWithTitle:@""]);
322 [context_menu addItemWithTitle:@"ContextMenuTest"
325 base::scoped_nsobject<ContextMenuController> menu_controller(
326 [[ContextMenuController alloc] initWithMenu:context_menu
329 // Set the menu as the contextual menu of contentView of test_window().
330 [[test_window() contentView] setMenu:context_menu];
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]);
342 EXPECT_FALSE([menu_controller isMenuOpen]);
343 EXPECT_FALSE([menu_controller didOpen]);
345 [NSApp sendEvent:event];
347 // When we got here, menu has already run its RunLoop.
348 // See -[ContextualMenuController menuWillOpen:].
349 EXPECT_FALSE([window isVisible]);
351 EXPECT_FALSE([menu_controller isMenuOpen]);
352 EXPECT_TRUE([menu_controller didOpen]);
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).
357 TEST_F(BaseBubbleControllerTest, BubbleStaysOpenWithSheet) {
358 base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
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
368 EXPECT_FALSE([panel isReleasedWhenClosed]); // scoped_nsobject releases it.
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_
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
382 SimulateKeyStatusChange();
383 EXPECT_TRUE([bubble_window_ isVisible]);
384 EXPECT_TRUE([panel isVisible]);
386 // Closing the attached sheet should not close the bubble.
387 [[NSApplication sharedApplication] endSheet:panel];
390 EXPECT_FALSE([bubble_window_ attachedSheet]);
391 EXPECT_TRUE([bubble_window_ isVisible]);
392 EXPECT_FALSE([panel isVisible]);
394 // Now that the sheet is gone, a key status change should close the bubble.
395 SimulateKeyStatusChange();
396 EXPECT_FALSE([bubble_window_ isVisible]);