Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / base_bubble_controller_unittest.mm
blob91b837843e125389084e3f31c75cd21be4705807
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"
16 namespace {
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;
23 }  // namespace
25 @interface ContextMenuController : NSObject<NSMenuDelegate> {
26  @private
27   NSMenu* menu_;
28   NSWindow* window_;
29   BOOL isMenuOpen_;
30   BOOL didOpen_;
33 - (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window;
35 - (BOOL)isMenuOpen;
36 - (BOOL)didOpen;
37 - (BOOL)isWindowVisible;
39 // NSMenuDelegate methods
40 - (void)menuWillOpen:(NSMenu*)menu;
41 - (void)menuDidClose:(NSMenu*)menu;
43 @end
45 @implementation ContextMenuController
47 - (id)initWithMenu:(NSMenu*)menu andWindow:(NSWindow*)window {
48   if (self = [super init]) {
49     menu_ = menu;
50     window_ = window;
51     isMenuOpen_ = NO;
52     didOpen_ = NO;
53     [menu_ setDelegate:self];
54   }
55   return self;
58 - (BOOL)isMenuOpen {
59   return isMenuOpen_;
62 - (BOOL)didOpen {
63   return didOpen_;
66 - (BOOL)isWindowVisible {
67   if (window_) {
68     return [window_ isVisible];
69   }
70   return NO;
73 - (void)menuWillOpen:(NSMenu*)menu {
74   isMenuOpen_ = YES;
75   didOpen_ = NO;
77   NSArray* modes = @[NSEventTrackingRunLoopMode, NSDefaultRunLoopMode];
78   [menu_ performSelector:@selector(cancelTracking)
79               withObject:nil
80               afterDelay:0.1
81                  inModes:modes];
84 - (void)menuDidClose:(NSMenu*)menu {
85   isMenuOpen_ = NO;
86   didOpen_ = YES;
89 @end
91 // A helper class to swizzle [NSApplication keyWindow].
92 @interface FakeKeyWindow : NSObject
93 @property(readonly) NSWindow* keyWindow;
94 @end
96 @implementation FakeKeyWindow
97 - (NSWindow*)keyWindow {
98   return g_key_window;
100 @end
103 class BaseBubbleControllerTest : public CocoaTest {
104  public:
105   BaseBubbleControllerTest() : controller_(nil) {}
107   void SetUp() override {
108     bubble_window_.reset([[InfoBubbleWindow alloc]
109         initWithContentRect:NSMakeRect(0, 0, kBubbleWindowWidth,
110                                        kBubbleWindowHeight)
111                   styleMask:NSBorderlessWindowMask
112                     backing:NSBackingStoreBuffered
113                       defer:NO]);
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]);
123   }
125   void TearDown() override {
126     // Close our windows.
127     [controller_ close];
128     bubble_window_.reset();
129     CocoaTest::TearDown();
130   }
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]);
140     return keep_alive;
141   }
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.
147   //
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];
157     else
158       [controller_ windowDidResignKey:notif];
159   }
161  protected:
162   base::scoped_nsobject<InfoBubbleWindow> bubble_window_;
163   BaseBubbleController* controller_;
165  private:
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
272                                       defer:NO]);
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())
289     return;
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())
331     return;
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"
339                           action:nil
340                    keyEquivalent:@""];
341   base::scoped_nsobject<ContextMenuController> menu_controller(
342       [[ContextMenuController alloc] initWithMenu:context_menu
343                                         andWindow:window]);
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]);
356   });
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
383                                      defer:NO]);
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_
390                                   modalDelegate:nil
391                                  didEndSelector:NULL
392                                     contextInfo:NULL];
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
397   // bubble.
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];
404   [panel close];
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())
447     return;
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]);
467   g_key_window = nil;