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