| // 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 |