blob: 7f50bb377c8a3eea4c935e8c49df2dea0b87f942 [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/gfx/test/ui_cocoa_test_helper.h"
#include "base/debug/debugger.h"
#include "base/logging.h"
#include "base/stl_util.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() {
// First NSCursor push always leaks.
[[NSCursor openHandCursor] push];
[NSCursor pop];
}
} // namespace.
@implementation CocoaTestHelperWindow
- (id)initWithContentRect:(NSRect)contentRect {
return [self initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
}
- (id)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];
}
- (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder {
EXPECT_TRUE([self makeFirstResponder:responder]);
[self setPretendIsKeyWindow:YES];
}
- (void)clearPretendKeyWindowAndFirstResponder {
[self setPretendIsKeyWindow:NO];
EXPECT_TRUE([self makeFirstResponder:NSApp]);
}
- (void)setPretendIsKeyWindow:(BOOL)flag {
pretendIsKeyWindow_ = flag;
}
- (BOOL)isKeyWindow {
return pretendIsKeyWindow_;
}
@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 =
[NSDictionary dictionaryWithObject:@"0.01" forKey:@"NSWindowResizeTime"];
[[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) {
const char* desc = [[*iter description] UTF8String];
LOG(WARNING) << "Didn't close window " << desc;
}
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