cocoa_helper.mm 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. // Copyright 2014 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. #import "ui/base/test/cocoa_helper.h"
  5. #include "base/debug/debugger.h"
  6. #include "base/logging.h"
  7. #include "base/stl_util.h"
  8. #include "base/test/mock_chrome_application_mac.h"
  9. #include "base/test/test_timeouts.h"
  10. namespace {
  11. // Some AppKit function leak intentionally, e.g. for caching purposes.
  12. // Force those leaks here, so there can be a unique calling path, allowing
  13. // to flag intentional leaks without having to suppress all calls to
  14. // potentially leaky functions.
  15. void NOINLINE ForceSystemLeaks() {
  16. // If a test suite hasn't already initialized NSApp, register the mock one
  17. // now.
  18. if (!NSApp)
  19. mock_cr_app::RegisterMockCrApp();
  20. // First NSCursor push always leaks.
  21. [[NSCursor openHandCursor] push];
  22. [NSCursor pop];
  23. }
  24. } // namespace.
  25. @implementation CocoaTestHelperWindow
  26. @synthesize pretendIsKeyWindow = _pretendIsKeyWindow;
  27. @synthesize pretendIsOnActiveSpace = _pretendIsOnActiveSpace;
  28. @synthesize pretendFullKeyboardAccessIsEnabled =
  29. _pretendFullKeyboardAccessIsEnabled;
  30. @synthesize useDefaultConstraints = _useDefaultConstraints;
  31. - (instancetype)initWithContentRect:(NSRect)contentRect {
  32. self = [super initWithContentRect:contentRect
  33. styleMask:NSWindowStyleMaskBorderless
  34. backing:NSBackingStoreBuffered
  35. defer:NO];
  36. if (self) {
  37. _useDefaultConstraints = YES;
  38. _pretendIsOnActiveSpace = YES;
  39. }
  40. return self;
  41. }
  42. - (instancetype)init {
  43. return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)];
  44. }
  45. - (void)dealloc {
  46. // Just a good place to put breakpoints when having problems with
  47. // unittests and CocoaTestHelperWindow.
  48. [super dealloc];
  49. }
  50. - (BOOL)isKeyWindow {
  51. return _pretendIsKeyWindow;
  52. }
  53. - (BOOL)isOnActiveSpace {
  54. return _pretendIsOnActiveSpace;
  55. }
  56. - (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder {
  57. EXPECT_TRUE([self makeFirstResponder:responder]);
  58. self.pretendIsKeyWindow = YES;
  59. }
  60. - (void)clearPretendKeyWindowAndFirstResponder {
  61. self.pretendIsKeyWindow = NO;
  62. EXPECT_TRUE([self makeFirstResponder:NSApp]);
  63. }
  64. - (void)setPretendIsOnActiveSpace:(BOOL)pretendIsOnActiveSpace {
  65. _pretendIsOnActiveSpace = pretendIsOnActiveSpace;
  66. [[NSWorkspace sharedWorkspace].notificationCenter
  67. postNotificationName:NSWorkspaceActiveSpaceDidChangeNotification
  68. object:[NSWorkspace sharedWorkspace]];
  69. }
  70. - (void)setPretendFullKeyboardAccessIsEnabled:(BOOL)enabled {
  71. EXPECT_TRUE([NSWindow
  72. instancesRespondToSelector:@selector(_allowsAnyValidResponder)]);
  73. _pretendFullKeyboardAccessIsEnabled = enabled;
  74. [self recalculateKeyViewLoop];
  75. }
  76. // Override of an undocumented AppKit method which controls call to check if
  77. // full keyboard access is enabled. Its presence is verified in
  78. // -setPretendFullKeyboardAccessIsEnabled:.
  79. - (BOOL)_allowsAnyValidResponder {
  80. return _pretendFullKeyboardAccessIsEnabled;
  81. }
  82. - (NSArray<NSView*>*)validKeyViews {
  83. NSMutableArray<NSView*>* validKeyViews = [NSMutableArray array];
  84. NSView* contentView = self.contentView;
  85. if (contentView.canBecomeKeyView)
  86. [validKeyViews addObject:contentView];
  87. for (NSView* keyView = contentView.nextValidKeyView;
  88. keyView != nil && ![validKeyViews containsObject:keyView];
  89. keyView = keyView.nextValidKeyView)
  90. [validKeyViews addObject:keyView];
  91. return validKeyViews;
  92. }
  93. - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen*)screen {
  94. if (!_useDefaultConstraints)
  95. return frameRect;
  96. return [super constrainFrameRect:frameRect toScreen:screen];
  97. }
  98. @end
  99. namespace ui {
  100. CocoaTestHelper::CocoaTestHelper() {
  101. ForceSystemLeaks();
  102. // Set the duration of AppKit-evaluated animations (such as frame changes)
  103. // to zero for testing purposes. That way they take effect immediately.
  104. [[NSAnimationContext currentContext] setDuration:0.0];
  105. // The above does not affect window-resize time, such as for an
  106. // attached sheet dropping in. Set that duration for the current
  107. // process (this is not persisted). Empirically, the value of 0.0
  108. // is ignored.
  109. NSDictionary* dict = @{@"NSWindowResizeTime" : @"0.01"};
  110. [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
  111. MarkCurrentWindowsAsInitial();
  112. }
  113. CocoaTestHelper::~CocoaTestHelper() {
  114. // Call close on our test_window to clean it up if one was opened.
  115. [test_window_ clearPretendKeyWindowAndFirstResponder];
  116. [test_window_ close];
  117. test_window_ = nil;
  118. // Recycle the pool to clean up any stuff that was put on the
  119. // autorelease pool due to window or windowcontroller closures.
  120. pool_.Recycle();
  121. // Some controls (NSTextFields, NSComboboxes etc) use
  122. // performSelector:withDelay: to clean up drag handlers and other
  123. // things (Radar 5851458 "Closing a window with a NSTextView in it
  124. // should get rid of it immediately"). The event loop must be spun
  125. // to get everything cleaned up correctly. It normally only takes
  126. // one to two spins through the event loop to see a change.
  127. // NOTE(shess): Under valgrind, -nextEventMatchingMask:* in one test
  128. // needed to run twice, once taking .2 seconds, the next time .6
  129. // seconds. The loop exit condition attempts to be scalable.
  130. // Get the set of windows which weren't present when the test
  131. // started.
  132. std::set<NSWindow*> windows_left(WindowsLeft());
  133. while (!windows_left.empty()) {
  134. // Cover delayed actions by spinning the loop at least once after
  135. // this timeout.
  136. const NSTimeInterval kCloseTimeoutSeconds =
  137. TestTimeouts::action_timeout().InSecondsF();
  138. // Cover chains of delayed actions by spinning the loop at least
  139. // this many times.
  140. const int kCloseSpins = 3;
  141. // Track the set of remaining windows so that everything can be
  142. // reset if progress is made.
  143. std::set<NSWindow*> still_left = windows_left;
  144. NSDate* start_date = [NSDate date];
  145. bool one_more_time = true;
  146. int spins = 0;
  147. while (still_left.size() == windows_left.size() &&
  148. (spins < kCloseSpins || one_more_time)) {
  149. // Check the timeout before pumping events, so that we'll spin
  150. // the loop once after the timeout.
  151. one_more_time =
  152. ([start_date timeIntervalSinceNow] > -kCloseTimeoutSeconds);
  153. // Autorelease anything thrown up by the event loop.
  154. @autoreleasepool {
  155. ++spins;
  156. NSEvent* next_event = [NSApp nextEventMatchingMask:NSEventMaskAny
  157. untilDate:nil
  158. inMode:NSDefaultRunLoopMode
  159. dequeue:YES];
  160. [NSApp sendEvent:next_event];
  161. [NSApp updateWindows];
  162. }
  163. // Refresh the outstanding windows.
  164. still_left = WindowsLeft();
  165. }
  166. // If no progress is being made, log a failure and continue.
  167. if (still_left.size() == windows_left.size()) {
  168. // NOTE(shess): Failing this expectation means that the test
  169. // opened windows which have not been fully released. Either
  170. // there is a leak, or perhaps one of |kCloseTimeoutSeconds| or
  171. // |kCloseSpins| needs adjustment.
  172. EXPECT_EQ(0U, windows_left.size());
  173. for (std::set<NSWindow*>::iterator iter = windows_left.begin();
  174. iter != windows_left.end(); ++iter) {
  175. LOG(WARNING) << "Didn't close window "
  176. << base::SysNSStringToUTF8([*iter description]);
  177. }
  178. break;
  179. }
  180. windows_left = still_left;
  181. }
  182. }
  183. void CocoaTestHelper::MarkCurrentWindowsAsInitial() {
  184. // Collect the list of windows that were open when the test started so
  185. // that we don't wait for them to close in TearDown. Has to be done
  186. // after BootstrapCocoa is called.
  187. initial_windows_ = ApplicationWindows();
  188. }
  189. std::set<NSWindow*> CocoaTestHelper::ApplicationWindows() {
  190. // This must NOT retain the windows it is returning.
  191. std::set<NSWindow*> windows;
  192. // Must create a pool here because [NSApp windows] has created an array
  193. // with retains on all the windows in it.
  194. @autoreleasepool {
  195. NSArray* appWindows = [NSApp windows];
  196. for (NSWindow* window in appWindows) {
  197. windows.insert(window);
  198. }
  199. return windows;
  200. }
  201. }
  202. std::set<NSWindow*> CocoaTestHelper::WindowsLeft() {
  203. const std::set<NSWindow*> windows(ApplicationWindows());
  204. std::set<NSWindow*> windows_left =
  205. base::STLSetDifference<std::set<NSWindow*>>(windows, initial_windows_);
  206. return windows_left;
  207. }
  208. CocoaTestHelperWindow* CocoaTestHelper::test_window() {
  209. if (!test_window_) {
  210. test_window_ = [[CocoaTestHelperWindow alloc] init];
  211. if (base::debug::BeingDebugged()) {
  212. [test_window_ orderFront:nil];
  213. } else {
  214. [test_window_ orderBack:nil];
  215. }
  216. }
  217. return test_window_;
  218. }
  219. CocoaTest::CocoaTest() : helper_(std::make_unique<CocoaTestHelper>()) {}
  220. CocoaTest::~CocoaTest() {
  221. CHECK(!helper_);
  222. }
  223. void CocoaTest::TearDown() {
  224. helper_.reset();
  225. PlatformTest::TearDown();
  226. }
  227. void CocoaTest::MarkCurrentWindowsAsInitial() {
  228. helper_->MarkCurrentWindowsAsInitial();
  229. }
  230. } // namespace ui