blob: dfa356ec7534ed10975522458065b0038e8ff56f [file] [log] [blame]
// 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"
#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/info_bubble_view.h"
#import "chrome/browser/ui/cocoa/info_bubble_window.h"
#import "chrome/browser/ui/cocoa/test/cocoa_test_helper.h"
#import "ui/base/test/menu_test_observer.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
// 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.
//
// Simply post a notification that will cause the controller to call
// |-windowDidResignKey:|.
void SimulateKeyStatusChange() {
NSNotification* notif =
[NSNotification notificationWithName:NSWindowDidResignKeyNotification
object:[controller_ window]];
[[NSNotificationCenter defaultCenter] postNotification: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::kTopLeading];
[[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::kTopTrailing];
[[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::kTopLeading];
[[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::kTopTrailing];
[[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::kTopTrailing, [[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) {
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) {
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:@""];
// Set the menu as the contextual menu of contentView of test_window().
[[test_window() contentView] setMenu:context_menu];
base::scoped_nsobject<MenuTestObserver> menu_observer(
[[MenuTestObserver alloc] initWithMenu:context_menu]);
[menu_observer setCloseAfterOpening:YES];
[menu_observer setOpenCallback:^(MenuTestObserver* observer) {
// Verify bubble's window is closed when contextual menu is open.
EXPECT_TRUE([observer isOpen]);
EXPECT_FALSE([window isVisible]);
}];
// 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());
EXPECT_FALSE([menu_observer isOpen]);
EXPECT_FALSE([menu_observer didOpen]);
[NSApp sendEvent:event];
// When we got here, menu has already run its RunLoop.
EXPECT_FALSE([window isVisible]);
EXPECT_FALSE([menu_observer isOpen]);
EXPECT_TRUE([menu_observer 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) {
[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;
}
// Test that clicking inside a child window of a bubble, does not dismiss the
// bubble, even if shouldCloseOnResignKey is YES.
TEST_F(BaseBubbleControllerTest, MouseDownInChildWindow) {
[controller_ setShouldCloseOnResignKey:YES];
base::scoped_nsobject<NSWindow> child_window([[NSWindow alloc]
initWithContentRect:NSMakeRect(500, 500, 500, 500)
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO]);
[bubble_window_ addChildWindow:child_window ordered:NSWindowAbove];
base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
ASSERT_TRUE([bubble_window_ isVisible]);
NSEvent* event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(10, 10), child_window);
[NSApp sendEvent:event];
EXPECT_TRUE([bubble_window_ isVisible]);
// Clicking outside the bubble on another window should dismiss the bubble.
event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(10, 10), test_window());
[NSApp sendEvent:event];
EXPECT_FALSE([bubble_window_ isVisible]);
}
// Test that clicking outside the bubble window but on a window with a greater
// level, does not dismiss the bubble, even if shouldCloseOnResignKey is YES.
TEST_F(BaseBubbleControllerTest, MouseDownInPopup) {
[controller_ setShouldCloseOnResignKey:YES];
base::scoped_nsobject<NSWindow> other_window([[NSWindow alloc]
initWithContentRect:NSMakeRect(500, 500, 500, 500)
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO]);
[other_window setLevel:NSPopUpMenuWindowLevel];
base::scoped_nsobject<BaseBubbleController> keep_alive = ShowBubble();
ASSERT_TRUE([bubble_window_ isVisible]);
NSEvent* event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(10, 10), other_window);
[NSApp sendEvent:event];
EXPECT_TRUE([bubble_window_ isVisible]);
// Clicking outside the bubble on a normal window should dismiss the bubble.
[other_window setLevel:NSNormalWindowLevel];
ASSERT_GT(NSPopUpMenuWindowLevel, NSNormalWindowLevel);
event = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(10, 10), other_window);
[NSApp sendEvent:event];
EXPECT_FALSE([bubble_window_ isVisible]);
}