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