| // Copyright 2014 The Chromium Authors |
| // 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 <objc/message.h> |
| #include <objc/runtime.h> |
| |
| #include <set> |
| #include <vector> |
| |
| #include "base/debug/debugger.h" |
| #include "base/logging.h" |
| #include "base/stl_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/test/mock_chrome_application_mac.h" |
| #include "base/test/test_timeouts.h" |
| |
| @implementation CocoaTestHelperWindow |
| |
| @synthesize pretendIsKeyWindow = _pretendIsKeyWindow; |
| @synthesize pretendIsOnActiveSpace = _pretendIsOnActiveSpace; |
| @synthesize pretendFullKeyboardAccessIsEnabled = |
| _pretendFullKeyboardAccessIsEnabled; |
| @synthesize useDefaultConstraints = _useDefaultConstraints; |
| |
| - (instancetype)initWithContentRect:(NSRect)contentRect { |
| self = [super initWithContentRect:contentRect |
| styleMask:NSWindowStyleMaskBorderless |
| backing:NSBackingStoreBuffered |
| defer:NO]; |
| if (self) { |
| _useDefaultConstraints = YES; |
| _pretendIsOnActiveSpace = YES; |
| self.releasedWhenClosed = NO; |
| } |
| return self; |
| } |
| |
| - (instancetype)init { |
| return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)]; |
| } |
| |
| // Enables users to write debugging code to help diagnose failures to close |
| // CocoaTestHelperWindow. Debugging code is not usually committed in Chromium, |
| // but because it's difficult to correctly override retain/release in ARC, this |
| // is left in. |
| #if 0 |
| |
| + (void)initialize { |
| if (self == [CocoaTestHelperWindow self]) { |
| Class test_class = [CocoaTestHelperWindow class]; |
| |
| Method method = class_getInstanceMethod(test_class, @selector(debugRetain)); |
| ASSERT_TRUE(method); |
| ASSERT_TRUE(class_addMethod(test_class, sel_registerName("retain"), |
| method_getImplementation(method), |
| method_getTypeEncoding(method))); |
| |
| method = class_getInstanceMethod(test_class, @selector(debugRelease)); |
| ASSERT_TRUE(method); |
| ASSERT_TRUE(class_addMethod(test_class, sel_registerName("release"), |
| method_getImplementation(method), |
| method_getTypeEncoding(method))); |
| } |
| } |
| |
| - (void)dealloc { |
| // Insert debugging code here. |
| } |
| |
| - (instancetype)debugRetain { |
| // Insert debugging code here. |
| |
| struct objc_super mySuper = {.receiver = self, |
| .super_class = [self superclass]}; |
| using retainSendSuper = id (*)(struct objc_super*, SEL); |
| retainSendSuper sendSuper = |
| reinterpret_cast<retainSendSuper>(objc_msgSendSuper); |
| return sendSuper(&mySuper, _cmd); |
| } |
| |
| - (oneway void)debugRelease { |
| // Insert debugging code here. |
| |
| struct objc_super mySuper = {.receiver = self, |
| .super_class = [self superclass]}; |
| using releaseSendSuper = void (*)(struct objc_super*, SEL); |
| releaseSendSuper sendSuper = |
| reinterpret_cast<releaseSendSuper>(objc_msgSendSuper); |
| return sendSuper(&mySuper, _cmd); |
| } |
| |
| #endif |
| |
| - (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)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; |
| } |
| |
| - (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 { |
| |
| CocoaTestHelper::CocoaTestHelper() { |
| // If a test suite hasn't already initialized NSApp, register the mock one |
| // now. |
| if (!NSApp) { |
| mock_cr_app::RegisterMockCrApp(); |
| } |
| |
| // Set the duration of AppKit-evaluated animations (such as frame changes) |
| // to zero for testing purposes. That way they take effect immediately. |
| NSAnimationContext.currentContext.duration = 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]; |
| |
| MarkCurrentWindowsAsInitial(); |
| } |
| |
| CocoaTestHelper::~CocoaTestHelper() { |
| // Call close on the 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 window controller 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. |
| WeakWindowVector 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. |
| WeakWindowVector 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. |
| @autoreleasepool { |
| ++spins; |
| NSEvent* next_event = [NSApp nextEventMatchingMask:NSEventMaskAny |
| 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 (NSWindow* __weak window : windows_left) { |
| LOG(WARNING) << "Didn't close window " |
| << base::SysNSStringToUTF8(window.description); |
| } |
| break; |
| } |
| |
| windows_left = still_left; |
| } |
| } |
| |
| void CocoaTestHelper::MarkCurrentWindowsAsInitial() { |
| // 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(); |
| } |
| |
| CocoaTestHelperWindow* CocoaTestHelper::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_; |
| } |
| |
| // Returns a vector of currently open windows. |
| CocoaTestHelper::WeakWindowVector CocoaTestHelper::ApplicationWindows() { |
| WeakWindowVector windows; |
| |
| // Must create a pool here because [NSApp windows] has created an array which |
| // retains all the windows in it. |
| @autoreleasepool { |
| for (NSWindow* window in NSApp.windows) { |
| windows.push_back(window); |
| } |
| return windows; |
| } |
| } |
| |
| CocoaTestHelper::WeakWindowVector CocoaTestHelper::WindowsLeft() { |
| // Window pointers can go nil only when the run loop is going, so it's safe to |
| // use sets within this function, just not outside it. |
| using WeakWindowSet = std::set<NSWindow * __weak>; |
| |
| WeakWindowVector windows = ApplicationWindows(); |
| WeakWindowSet windows_set(windows.begin(), windows.end()); |
| |
| // Ignore TextInputUIMacHelper.framework created TUINSWindow. We have no |
| // control or documentation about these windows, ignoring them seems like the |
| // best approach. |
| std::erase_if(windows_set, [](NSWindow* __weak set_window) { |
| return [set_window isKindOfClass:NSClassFromString(@"TUINSWindow")]; |
| }); |
| |
| // Subtract away the initial windows. The current window set will not have any |
| // nil values, as it was just obtained, so subtracting away the nil from any |
| // initial windows that have been closed is safe. |
| WeakWindowSet initial_windows_set(initial_windows_.begin(), |
| initial_windows_.end()); |
| |
| WeakWindowSet windows_left_set = |
| base::STLSetDifference<WeakWindowSet>(windows_set, initial_windows_set); |
| return std::vector(windows_left_set.begin(), windows_left_set.end()); |
| } |
| |
| CocoaTest::CocoaTest() : helper_(std::make_unique<CocoaTestHelper>()) {} |
| CocoaTest::~CocoaTest() { |
| CHECK(!helper_); |
| } |
| |
| void CocoaTest::TearDown() { |
| helper_.reset(); |
| PlatformTest::TearDown(); |
| } |
| |
| void CocoaTest::MarkCurrentWindowsAsInitial() { |
| helper_->MarkCurrentWindowsAsInitial(); |
| } |
| |
| } // namespace ui |