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