blob: 70461929c6ef4d143c583da30c9ce04140276b4a [file] [log] [blame]
// Copyright 2014 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 "ui/base/test/cocoa_helper.h"
#include "base/debug/debugger.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "base/test/mock_chrome_application_mac.h"
#include "base/test/test_timeouts.h"
namespace {
// Some AppKit function leak intentionally, e.g. for caching purposes.
// Force those leaks here, so there can be a unique calling path, allowing
// to flag intentional leaks without having to suppress all calls to
// potentially leaky functions.
void NOINLINE ForceSystemLeaks() {
// If a test suite hasn't already initialized NSApp, register the mock one
// now.
if (!NSApp)
mock_cr_app::RegisterMockCrApp();
// First NSCursor push always leaks.
[[NSCursor openHandCursor] push];
[NSCursor pop];
}
} // namespace.
@implementation CocoaTestHelperWindow
@synthesize pretendIsKeyWindow = pretendIsKeyWindow_;
@synthesize pretendIsOccluded = pretendIsOccluded_;
@synthesize pretendIsOnActiveSpace = pretendIsOnActiveSpace_;
@synthesize pretendFullKeyboardAccessIsEnabled =
pretendFullKeyboardAccessIsEnabled_;
@synthesize useDefaultConstraints = useDefaultConstraints_;
- (instancetype)initWithContentRect:(NSRect)contentRect {
self = [super initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
if (self) {
useDefaultConstraints_ = YES;
pretendIsOnActiveSpace_ = YES;
}
return self;
}
- (instancetype)init {
return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)];
}
- (void)dealloc {
// Just a good place to put breakpoints when having problems with
// unittests and CocoaTestHelperWindow.
[super dealloc];
}
- (BOOL)isKeyWindow {
return pretendIsKeyWindow_;
}
- (BOOL)isOnActiveSpace {
return pretendIsOnActiveSpace_;
}
- (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder {
EXPECT_TRUE([self makeFirstResponder:responder]);
self.pretendIsKeyWindow = YES;
}
- (void)clearPretendKeyWindowAndFirstResponder {
self.pretendIsKeyWindow = NO;
EXPECT_TRUE([self makeFirstResponder:NSApp]);
}
- (void)setPretendIsOccluded:(BOOL)flag {
pretendIsOccluded_ = flag;
[[NSNotificationCenter defaultCenter]
postNotificationName:NSWindowDidChangeOcclusionStateNotification
object:self];
}
- (void)setPretendIsOnActiveSpace:(BOOL)pretendIsOnActiveSpace {
pretendIsOnActiveSpace_ = pretendIsOnActiveSpace;
[[NSWorkspace sharedWorkspace].notificationCenter
postNotificationName:NSWorkspaceActiveSpaceDidChangeNotification
object:[NSWorkspace sharedWorkspace]];
}
- (void)setPretendFullKeyboardAccessIsEnabled:(BOOL)enabled {
EXPECT_TRUE([NSWindow
instancesRespondToSelector:@selector(_allowsAnyValidResponder)]);
pretendFullKeyboardAccessIsEnabled_ = enabled;
[self recalculateKeyViewLoop];
}
// Override of an undocumented AppKit method which controls call to check if
// full keyboard access is enabled. Its presence is verified in
// -setPretendFullKeyboardAccessIsEnabled:.
- (BOOL)_allowsAnyValidResponder {
return pretendFullKeyboardAccessIsEnabled_;
}
- (NSWindowOcclusionState)occlusionState {
return pretendIsOccluded_ ? 0 : NSWindowOcclusionStateVisible;
}
- (NSArray<NSView*>*)validKeyViews {
NSMutableArray<NSView*>* validKeyViews = [NSMutableArray array];
NSView* contentView = self.contentView;
if (contentView.canBecomeKeyView)
[validKeyViews addObject:contentView];
for (NSView* keyView = contentView.nextValidKeyView;
keyView != nil && ![validKeyViews containsObject:keyView];
keyView = keyView.nextValidKeyView)
[validKeyViews addObject:keyView];
return validKeyViews;
}
- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen*)screen {
if (!useDefaultConstraints_)
return frameRect;
return [super constrainFrameRect:frameRect toScreen:screen];
}
@end
namespace ui {
CocoaTest::CocoaTest() : called_tear_down_(false), test_window_(nil) {
ForceSystemLeaks();
Init();
}
CocoaTest::~CocoaTest() {
// Must call CocoaTest's teardown from your overrides.
DCHECK(called_tear_down_);
}
void CocoaTest::Init() {
// Set the duration of AppKit-evaluated animations (such as frame changes)
// to zero for testing purposes. That way they take effect immediately.
[[NSAnimationContext currentContext] setDuration:0.0];
// The above does not affect window-resize time, such as for an
// attached sheet dropping in. Set that duration for the current
// process (this is not persisted). Empirically, the value of 0.0
// is ignored.
NSDictionary* dict = @{@"NSWindowResizeTime" : @"0.01"};
[[NSUserDefaults standardUserDefaults] registerDefaults:dict];
// Collect the list of windows that were open when the test started so
// that we don't wait for them to close in TearDown. Has to be done
// after BootstrapCocoa is called.
initial_windows_ = ApplicationWindows();
}
void CocoaTest::TearDown() {
called_tear_down_ = true;
// Call close on our test_window to clean it up if one was opened.
[test_window_ clearPretendKeyWindowAndFirstResponder];
[test_window_ close];
test_window_ = nil;
// Recycle the pool to clean up any stuff that was put on the
// autorelease pool due to window or windowcontroller closures.
pool_.Recycle();
// Some controls (NSTextFields, NSComboboxes etc) use
// performSelector:withDelay: to clean up drag handlers and other
// things (Radar 5851458 "Closing a window with a NSTextView in it
// should get rid of it immediately"). The event loop must be spun
// to get everything cleaned up correctly. It normally only takes
// one to two spins through the event loop to see a change.
// NOTE(shess): Under valgrind, -nextEventMatchingMask:* in one test
// needed to run twice, once taking .2 seconds, the next time .6
// seconds. The loop exit condition attempts to be scalable.
// Get the set of windows which weren't present when the test
// started.
std::set<NSWindow*> windows_left(WindowsLeft());
while (!windows_left.empty()) {
// Cover delayed actions by spinning the loop at least once after
// this timeout.
const NSTimeInterval kCloseTimeoutSeconds =
TestTimeouts::action_timeout().InSecondsF();
// Cover chains of delayed actions by spinning the loop at least
// this many times.
const int kCloseSpins = 3;
// Track the set of remaining windows so that everything can be
// reset if progress is made.
std::set<NSWindow*> still_left = windows_left;
NSDate* start_date = [NSDate date];
bool one_more_time = true;
int spins = 0;
while (still_left.size() == windows_left.size() &&
(spins < kCloseSpins || one_more_time)) {
// Check the timeout before pumping events, so that we'll spin
// the loop once after the timeout.
one_more_time =
([start_date timeIntervalSinceNow] > -kCloseTimeoutSeconds);
// Autorelease anything thrown up by the event loop.
{
base::mac::ScopedNSAutoreleasePool pool;
++spins;
NSEvent* next_event = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue:YES];
[NSApp sendEvent:next_event];
[NSApp updateWindows];
}
// Refresh the outstanding windows.
still_left = WindowsLeft();
}
// If no progress is being made, log a failure and continue.
if (still_left.size() == windows_left.size()) {
// NOTE(shess): Failing this expectation means that the test
// opened windows which have not been fully released. Either
// there is a leak, or perhaps one of |kCloseTimeoutSeconds| or
// |kCloseSpins| needs adjustment.
EXPECT_EQ(0U, windows_left.size());
for (std::set<NSWindow*>::iterator iter = windows_left.begin();
iter != windows_left.end(); ++iter) {
LOG(WARNING) << "Didn't close window "
<< base::SysNSStringToUTF8([*iter description]);
}
break;
}
windows_left = still_left;
}
PlatformTest::TearDown();
}
std::set<NSWindow*> CocoaTest::ApplicationWindows() {
// This must NOT retain the windows it is returning.
std::set<NSWindow*> windows;
// Must create a pool here because [NSApp windows] has created an array
// with retains on all the windows in it.
base::mac::ScopedNSAutoreleasePool pool;
NSArray* appWindows = [NSApp windows];
for (NSWindow* window in appWindows) {
windows.insert(window);
}
return windows;
}
std::set<NSWindow*> CocoaTest::WindowsLeft() {
const std::set<NSWindow*> windows(ApplicationWindows());
std::set<NSWindow*> windows_left =
base::STLSetDifference<std::set<NSWindow*>>(windows, initial_windows_);
return windows_left;
}
CocoaTestHelperWindow* CocoaTest::test_window() {
if (!test_window_) {
test_window_ = [[CocoaTestHelperWindow alloc] init];
if (base::debug::BeingDebugged()) {
[test_window_ orderFront:nil];
} else {
[test_window_ orderBack:nil];
}
}
return test_window_;
}
} // namespace ui