1// Copyright 2017 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/message_loop/message_pump_mac.h" 6 7#include "base/cancelable_callback.h" 8#include "base/functional/bind.h" 9#include "base/mac/scoped_cftyperef.h" 10#import "base/mac/scoped_nsobject.h" 11#include "base/task/current_thread.h" 12#include "base/task/single_thread_task_runner.h" 13#include "base/test/bind.h" 14#include "base/test/task_environment.h" 15#include "testing/gtest/include/gtest/gtest.h" 16 17@interface TestModalAlertCloser : NSObject 18- (void)runTestThenCloseAlert:(NSAlert*)alert; 19@end 20 21namespace { 22 23// Internal constants from message_pump_mac.mm. 24constexpr int kAllModesMask = 0xf; 25constexpr int kNSApplicationModalSafeModeMask = 0x3; 26 27} // namespace 28 29namespace base { 30 31namespace { 32 33// PostedTasks are only executed while the message pump has a delegate. That is, 34// when a base::RunLoop is running, so in order to test whether posted tasks 35// are run by CFRunLoopRunInMode and *not* by the regular RunLoop, we need to 36// be inside a task that is also calling CFRunLoopRunInMode. 37// This function posts |task| and runs the given |mode|. 38void RunTaskInMode(CFRunLoopMode mode, OnceClosure task) { 39 // Since this task is "ours" rather than a system task, allow nesting. 40 CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow; 41 CancelableOnceClosure cancelable(std::move(task)); 42 SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE, 43 cancelable.callback()); 44 while (CFRunLoopRunInMode(mode, 0, true) == kCFRunLoopRunHandledSource) 45 ; 46} 47 48} // namespace 49 50// Tests the correct behavior of ScopedPumpMessagesInPrivateModes. 51TEST(MessagePumpMacTest, ScopedPumpMessagesInPrivateModes) { 52 test::SingleThreadTaskEnvironment task_environment( 53 test::SingleThreadTaskEnvironment::MainThreadType::UI); 54 55 CFRunLoopMode kRegular = kCFRunLoopDefaultMode; 56 CFRunLoopMode kPrivate = CFSTR("NSUnhighlightMenuRunLoopMode"); 57 58 // Work is seen when running in the default mode. 59 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 60 FROM_HERE, 61 BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE))); 62 EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); 63 64 // But not seen when running in a private mode. 65 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 66 FROM_HERE, 67 BindOnce(&RunTaskInMode, kPrivate, MakeExpectedNotRunClosure(FROM_HERE))); 68 EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); 69 70 { 71 ScopedPumpMessagesInPrivateModes allow_private; 72 // Now the work should be seen. 73 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 74 FROM_HERE, 75 BindOnce(&RunTaskInMode, kPrivate, MakeExpectedRunClosure(FROM_HERE))); 76 EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); 77 78 // The regular mode should also work the same. 79 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 80 FROM_HERE, 81 BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE))); 82 EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); 83 } 84 85 // And now the scoper is out of scope, private modes should no longer see it. 86 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 87 FROM_HERE, 88 BindOnce(&RunTaskInMode, kPrivate, MakeExpectedNotRunClosure(FROM_HERE))); 89 EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); 90 91 // Only regular modes see it. 92 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 93 FROM_HERE, 94 BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE))); 95 EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); 96} 97 98// Tests that private message loop modes are not pumped while a modal dialog is 99// present. 100TEST(MessagePumpMacTest, ScopedPumpMessagesAttemptWithModalDialog) { 101 test::SingleThreadTaskEnvironment task_environment( 102 test::SingleThreadTaskEnvironment::MainThreadType::UI); 103 104 { 105 base::ScopedPumpMessagesInPrivateModes allow_private; 106 // No modal window, so all modes should be pumped. 107 EXPECT_EQ(kAllModesMask, allow_private.GetModeMaskForTest()); 108 } 109 110 base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]); 111 [alert addButtonWithTitle:@"OK"]; 112 base::scoped_nsobject<TestModalAlertCloser> closer( 113 [[TestModalAlertCloser alloc] init]); 114 [closer performSelector:@selector(runTestThenCloseAlert:) 115 withObject:alert 116 afterDelay:0 117 inModes:@[ NSModalPanelRunLoopMode ]]; 118 NSInteger result = [alert runModal]; 119 EXPECT_EQ(NSAlertFirstButtonReturn, result); 120} 121 122TEST(MessagePumpMacTest, QuitWithModalWindow) { 123 test::SingleThreadTaskEnvironment task_environment( 124 test::SingleThreadTaskEnvironment::MainThreadType::UI); 125 NSWindow* window = 126 [[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100) 127 styleMask:NSWindowStyleMaskBorderless 128 backing:NSBackingStoreBuffered 129 defer:NO] autorelease]; 130 131 // Check that quitting the run loop while a modal window is shown applies to 132 // |run_loop| rather than the internal NSApplication modal run loop. 133 RunLoop run_loop; 134 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 135 FROM_HERE, base::BindLambdaForTesting([&] { 136 CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow; 137 ScopedPumpMessagesInPrivateModes pump_private; 138 [NSApp runModalForWindow:window]; 139 })); 140 SingleThreadTaskRunner::GetCurrentDefault()->PostTask( 141 FROM_HERE, base::BindLambdaForTesting([&] { 142 [NSApp stopModal]; 143 run_loop.Quit(); 144 })); 145 146 EXPECT_NO_FATAL_FAILURE(run_loop.Run()); 147} 148 149} // namespace base 150 151@implementation TestModalAlertCloser 152 153- (void)runTestThenCloseAlert:(NSAlert*)alert { 154 EXPECT_TRUE([NSApp modalWindow]); 155 { 156 base::ScopedPumpMessagesInPrivateModes allow_private; 157 // With a modal window, only safe modes should be pumped. 158 EXPECT_EQ(kNSApplicationModalSafeModeMask, 159 allow_private.GetModeMaskForTest()); 160 } 161 [[alert buttons][0] performClick:nil]; 162} 163 164@end 165