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