Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2009 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
      6 
      7 #include "base/debug/debugger.h"
      8 #include "base/logging.h"
      9 #include "base/mac/mac_util.h"
     10 #include "base/test/test_timeouts.h"
     11 #import "chrome/browser/chrome_browser_application_mac.h"
     12 
     13 @implementation CocoaTestHelperWindow
     14 
     15 - (id)initWithContentRect:(NSRect)contentRect {
     16   return [self initWithContentRect:contentRect
     17                          styleMask:NSBorderlessWindowMask
     18                            backing:NSBackingStoreBuffered
     19                              defer:NO];
     20 }
     21 
     22 - (id)init {
     23   return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)];
     24 }
     25 
     26 - (void)dealloc {
     27   // Just a good place to put breakpoints when having problems with
     28   // unittests and CocoaTestHelperWindow.
     29   [super dealloc];
     30 }
     31 
     32 - (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder {
     33   EXPECT_TRUE([self makeFirstResponder:responder]);
     34   [self setPretendIsKeyWindow:YES];
     35 }
     36 
     37 - (void)clearPretendKeyWindowAndFirstResponder {
     38   [self setPretendIsKeyWindow:NO];
     39   EXPECT_TRUE([self makeFirstResponder:NSApp]);
     40 }
     41 
     42 - (void)setPretendIsKeyWindow:(BOOL)flag {
     43   pretendIsKeyWindow_ = flag;
     44 }
     45 
     46 - (BOOL)isKeyWindow {
     47   return pretendIsKeyWindow_;
     48 }
     49 
     50 @end
     51 
     52 CocoaTest::CocoaTest() : called_tear_down_(false), test_window_(nil) {
     53   BootstrapCocoa();
     54 
     55   // Set the duration of AppKit-evaluated animations (such as frame changes)
     56   // to zero for testing purposes. That way they take effect immediately.
     57   [[NSAnimationContext currentContext] setDuration:0.0];
     58 
     59   // The above does not affect window-resize time, such as for an
     60   // attached sheet dropping in.  Set that duration for the current
     61   // process (this is not persisted).  Empirically, the value of 0.0
     62   // is ignored.
     63   NSDictionary* dict =
     64       [NSDictionary dictionaryWithObject:@"0.01" forKey:@"NSWindowResizeTime"];
     65   [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
     66 
     67   // Collect the list of windows that were open when the test started so
     68   // that we don't wait for them to close in TearDown. Has to be done
     69   // after BootstrapCocoa is called.
     70   initial_windows_ = ApplicationWindows();
     71 }
     72 
     73 CocoaTest::~CocoaTest() {
     74   // Must call CocoaTest's teardown from your overrides.
     75   DCHECK(called_tear_down_);
     76 }
     77 
     78 void CocoaTest::BootstrapCocoa() {
     79   // Look in the framework bundle for resources.
     80   FilePath path;
     81   PathService::Get(base::DIR_EXE, &path);
     82   path = path.Append(chrome::kFrameworkName);
     83   base::mac::SetOverrideAppBundlePath(path);
     84 
     85   // Bootstrap Cocoa. It's very unhappy without this.
     86   [CrApplication sharedApplication];
     87 }
     88 
     89 void CocoaTest::TearDown() {
     90   called_tear_down_ = true;
     91   // Call close on our test_window to clean it up if one was opened.
     92   [test_window_ close];
     93   test_window_ = nil;
     94 
     95   // Recycle the pool to clean up any stuff that was put on the
     96   // autorelease pool due to window or windowcontroller closures.
     97   pool_.Recycle();
     98 
     99   // Some controls (NSTextFields, NSComboboxes etc) use
    100   // performSelector:withDelay: to clean up drag handlers and other
    101   // things (Radar 5851458 "Closing a window with a NSTextView in it
    102   // should get rid of it immediately").  The event loop must be spun
    103   // to get everything cleaned up correctly.  It normally only takes
    104   // one to two spins through the event loop to see a change.
    105 
    106   // NOTE(shess): Under valgrind, -nextEventMatchingMask:* in one test
    107   // needed to run twice, once taking .2 seconds, the next time .6
    108   // seconds.  The loop exit condition attempts to be scalable.
    109 
    110   // Get the set of windows which weren't present when the test
    111   // started.
    112   std::set<NSWindow*> windows_left(WindowsLeft());
    113 
    114   while (!windows_left.empty()) {
    115     // Cover delayed actions by spinning the loop at least once after
    116     // this timeout.
    117     const NSTimeInterval kCloseTimeoutSeconds =
    118         TestTimeouts::action_timeout_ms() / 1000.0;
    119 
    120     // Cover chains of delayed actions by spinning the loop at least
    121     // this many times.
    122     const int kCloseSpins = 3;
    123 
    124     // Track the set of remaining windows so that everything can be
    125     // reset if progress is made.
    126     std::set<NSWindow*> still_left = windows_left;
    127 
    128     NSDate* start_date = [NSDate date];
    129     bool one_more_time = true;
    130     int spins = 0;
    131     while (still_left.size() == windows_left.size() &&
    132            (spins < kCloseSpins || one_more_time)) {
    133       // Check the timeout before pumping events, so that we'll spin
    134       // the loop once after the timeout.
    135       one_more_time =
    136           ([start_date timeIntervalSinceNow] > -kCloseTimeoutSeconds);
    137 
    138       // Autorelease anything thrown up by the event loop.
    139       {
    140         base::mac::ScopedNSAutoreleasePool pool;
    141         ++spins;
    142         NSEvent *next_event = [NSApp nextEventMatchingMask:NSAnyEventMask
    143                                                  untilDate:nil
    144                                                     inMode:NSDefaultRunLoopMode
    145                                                    dequeue:YES];
    146         [NSApp sendEvent:next_event];
    147         [NSApp updateWindows];
    148       }
    149 
    150       // Refresh the outstanding windows.
    151       still_left = WindowsLeft();
    152     }
    153 
    154     // If no progress is being made, log a failure and continue.
    155     if (still_left.size() == windows_left.size()) {
    156       // NOTE(shess): Failing this expectation means that the test
    157       // opened windows which have not been fully released.  Either
    158       // there is a leak, or perhaps one of |kCloseTimeoutSeconds| or
    159       // |kCloseSpins| needs adjustment.
    160       EXPECT_EQ(0U, windows_left.size());
    161       for (std::set<NSWindow*>::iterator iter = windows_left.begin();
    162            iter != windows_left.end(); ++iter) {
    163         const char* desc = [[*iter description] UTF8String];
    164         LOG(WARNING) << "Didn't close window " << desc;
    165       }
    166       break;
    167     }
    168 
    169     windows_left = still_left;
    170   }
    171   PlatformTest::TearDown();
    172 }
    173 
    174 std::set<NSWindow*> CocoaTest::ApplicationWindows() {
    175   // This must NOT retain the windows it is returning.
    176   std::set<NSWindow*> windows;
    177 
    178   // Must create a pool here because [NSApp windows] has created an array
    179   // with retains on all the windows in it.
    180   base::mac::ScopedNSAutoreleasePool pool;
    181   NSArray *appWindows = [NSApp windows];
    182   for (NSWindow *window in appWindows) {
    183     windows.insert(window);
    184   }
    185   return windows;
    186 }
    187 
    188 std::set<NSWindow*> CocoaTest::WindowsLeft() {
    189   const std::set<NSWindow*> windows(ApplicationWindows());
    190   std::set<NSWindow*> windows_left;
    191   std::set_difference(windows.begin(), windows.end(),
    192                       initial_windows_.begin(), initial_windows_.end(),
    193                       std::inserter(windows_left, windows_left.begin()));
    194   return windows_left;
    195 }
    196 
    197 CocoaTestHelperWindow* CocoaTest::test_window() {
    198   if (!test_window_) {
    199     test_window_ = [[CocoaTestHelperWindow alloc] init];
    200     if (base::debug::BeingDebugged()) {
    201       [test_window_ orderFront:nil];
    202     } else {
    203       [test_window_ orderBack:nil];
    204     }
    205   }
    206   return test_window_;
    207 }
    208