1// Copyright 2023 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/mac/launch_application.h" 6 7#include <sys/select.h> 8 9#include "base/apple/bridging.h" 10#include "base/apple/foundation_util.h" 11#include "base/base_paths.h" 12#include "base/containers/span.h" 13#include "base/files/file_path.h" 14#include "base/files/file_util.h" 15#include "base/files/scoped_temp_dir.h" 16#include "base/functional/callback_helpers.h" 17#include "base/logging.h" 18#include "base/mac/mac_util.h" 19#include "base/path_service.h" 20#include "base/process/launch.h" 21#include "base/strings/string_util.h" 22#include "base/strings/sys_string_conversions.h" 23#include "base/task/bind_post_task.h" 24#include "base/task/thread_pool.h" 25#include "base/test/bind.h" 26#include "base/test/task_environment.h" 27#include "base/test/test_future.h" 28#include "base/threading/platform_thread.h" 29#include "base/types/expected.h" 30#include "base/uuid.h" 31#include "testing/gmock/include/gmock/gmock.h" 32#include "testing/gtest/include/gtest/gtest.h" 33#import "testing/gtest_mac.h" 34 35namespace base::mac { 36namespace { 37 38// Reads XML encoded property lists from `fifo_path`, calling `callback` for 39// each succesfully parsed dictionary. Loops indefinitely until the string 40// "<!FINISHED>" is read from `fifo_path`. 41void ReadLaunchEventsFromFifo( 42 const FilePath& fifo_path, 43 RepeatingCallback<void(NSDictionary* event)> callback) { 44 File f(fifo_path, File::FLAG_OPEN | File::FLAG_READ); 45 std::string data; 46 while (true) { 47 char buf[4096]; 48 std::optional<size_t> read_count = 49 f.ReadAtCurrentPosNoBestEffort(base::as_writable_byte_span(buf)); 50 ASSERT_TRUE(read_count.has_value()); 51 if (read_count.value()) { 52 data += std::string_view(buf, read_count.value()); 53 // Assume that at any point the beginning of the data buffer is the start 54 // of a plist. Search for the first end, and parse that substring. 55 size_t end_of_plist; 56 while ((end_of_plist = data.find("</plist>")) != std::string::npos) { 57 std::string plist = data.substr(0, end_of_plist + 8); 58 data = data.substr(plist.length()); 59 NSDictionary* event = apple::ObjCCastStrict<NSDictionary>( 60 SysUTF8ToNSString(TrimWhitespaceASCII(plist, TRIM_ALL)) 61 .propertyList); 62 callback.Run(event); 63 } 64 // No more plists found, check if the termination marker was send. 65 if (data.find("<!FINISHED>") != std::string::npos) { 66 break; 67 } 68 } else { 69 // No data was read, wait for the file descriptor to become readable 70 // again. 71 fd_set fds; 72 FD_ZERO(&fds); 73 FD_SET(f.GetPlatformFile(), &fds); 74 select(FD_SETSIZE, &fds, nullptr, nullptr, nullptr); 75 } 76 } 77} 78 79// This test harness creates an app bundle with a random bundle identifier to 80// avoid conflicts with concurrently running other tests. The binary in this app 81// bundle writes various events to a named pipe, allowing tests here to verify 82// that correct events were received by the app. 83class LaunchApplicationTest : public testing::Test { 84 public: 85 void SetUp() override { 86 helper_bundle_id_ = 87 SysUTF8ToNSString("org.chromium.LaunchApplicationTestHelper." + 88 Uuid::GenerateRandomV4().AsLowercaseString()); 89 90 FilePath data_root; 91 ASSERT_TRUE(PathService::Get(DIR_OUT_TEST_DATA_ROOT, &data_root)); 92 const FilePath helper_app_executable = 93 data_root.AppendASCII("launch_application_test_helper"); 94 95 // Put helper app inside home dir, as the default temp location gets special 96 // treatment by launch services, effecting the behavior of some of these 97 // tests. 98 ASSERT_TRUE(temp_dir_.CreateUniqueTempDirUnderPath(base::GetHomeDir())); 99 100 helper_app_bundle_path_ = 101 temp_dir_.GetPath().AppendASCII("launch_application_test_helper.app"); 102 103 const base::FilePath destination_contents_path = 104 helper_app_bundle_path_.AppendASCII("Contents"); 105 const base::FilePath destination_executable_path = 106 destination_contents_path.AppendASCII("MacOS"); 107 108 // First create the .app bundle directory structure. 109 // Use NSFileManager so that the permissions can be set appropriately. The 110 // base::CreateDirectory() routine forces mode 0700. 111 NSError* error = nil; 112 ASSERT_TRUE([NSFileManager.defaultManager 113 createDirectoryAtURL:base::apple::FilePathToNSURL( 114 destination_executable_path) 115 withIntermediateDirectories:YES 116 attributes:@{ 117 NSFilePosixPermissions : @(0755) 118 } 119 error:&error]) 120 << SysNSStringToUTF8(error.description); 121 122 // Copy the executable file. 123 helper_app_executable_path_ = 124 destination_executable_path.Append(helper_app_executable.BaseName()); 125 ASSERT_TRUE( 126 base::CopyFile(helper_app_executable, helper_app_executable_path_)); 127 128 // Write the PkgInfo file. 129 constexpr char kPkgInfoData[] = "APPL????"; 130 ASSERT_TRUE(base::WriteFile( 131 destination_contents_path.AppendASCII("PkgInfo"), kPkgInfoData)); 132 133#if defined(ADDRESS_SANITIZER) 134 const base::FilePath asan_library_path = 135 data_root.AppendASCII("libclang_rt.asan_osx_dynamic.dylib"); 136 ASSERT_TRUE(base::CopyFile( 137 asan_library_path, 138 destination_executable_path.Append(asan_library_path.BaseName()))); 139#endif 140 141#if defined(UNDEFINED_SANITIZER) 142 const base::FilePath ubsan_library_path = 143 data_root.AppendASCII("libclang_rt.ubsan_osx_dynamic.dylib"); 144 ASSERT_TRUE(base::CopyFile( 145 ubsan_library_path, 146 destination_executable_path.Append(ubsan_library_path.BaseName()))); 147#endif 148 149 // Generate the Plist file 150 NSDictionary* plist = @{ 151 @"CFBundleExecutable" : 152 apple::FilePathToNSString(helper_app_executable.BaseName()), 153 @"CFBundleIdentifier" : helper_bundle_id_, 154 }; 155 ASSERT_TRUE([plist 156 writeToURL:apple::FilePathToNSURL( 157 destination_contents_path.AppendASCII("Info.plist")) 158 error:nil]); 159 160 // Register the app with LaunchServices. 161 LSRegisterURL(base::apple::FilePathToCFURL(helper_app_bundle_path_).get(), 162 true); 163 164 // Ensure app was registered with LaunchServices. Sometimes it takes a 165 // little bit of time for this to happen, and some tests might fail if the 166 // app wasn't registered yet. 167 while (true) { 168 NSArray<NSURL*>* apps = nil; 169 if (@available(macOS 12.0, *)) { 170 apps = [[NSWorkspace sharedWorkspace] 171 URLsForApplicationsWithBundleIdentifier:helper_bundle_id_]; 172 } else { 173 apps = 174 apple::CFToNSOwnershipCast(LSCopyApplicationURLsForBundleIdentifier( 175 apple::NSToCFPtrCast(helper_bundle_id_), /*outError=*/nullptr)); 176 } 177 if (apps.count > 0) { 178 break; 179 } 180 PlatformThread::Sleep(Milliseconds(50)); 181 } 182 183 // Setup fifo to receive logs from the helper app. 184 helper_app_fifo_path_ = 185 temp_dir_.GetPath().AppendASCII("launch_application_test_helper.fifo"); 186 ASSERT_EQ(0, mkfifo(helper_app_fifo_path_.value().c_str(), 187 S_IWUSR | S_IRUSR | S_IWGRP | S_IWGRP)); 188 189 // Create array to store received events in, and start listening for events. 190 launch_events_ = [[NSMutableArray alloc] init]; 191 base::ThreadPool::PostTask( 192 FROM_HERE, {MayBlock()}, 193 base::BindOnce( 194 &ReadLaunchEventsFromFifo, helper_app_fifo_path_, 195 BindPostTaskToCurrentDefault(BindRepeating( 196 &LaunchApplicationTest::OnLaunchEvent, Unretained(this))))); 197 } 198 199 void TearDown() override { 200 if (temp_dir_.IsValid()) { 201 // Make sure fifo reading task stops reading/waiting. 202 WriteFile(helper_app_fifo_path_, "<!FINISHED>"); 203 204 // Make sure all apps that were launched by this test are terminated. 205 NSArray<NSRunningApplication*>* apps = 206 NSWorkspace.sharedWorkspace.runningApplications; 207 for (NSRunningApplication* app in apps) { 208 if (temp_dir_.GetPath().IsParent( 209 apple::NSURLToFilePath(app.bundleURL)) || 210 [app.bundleIdentifier isEqualToString:helper_bundle_id_]) { 211 [app forceTerminate]; 212 } 213 } 214 215 // And make sure the temp dir was successfully deleted. 216 EXPECT_TRUE(temp_dir_.Delete()); 217 } 218 } 219 220 // Calls `LaunchApplication` with the given parameters, expecting the launch 221 // to succeed. Returns the `NSRunningApplication*` the callback passed to 222 // `LaunchApplication` was called with. 223 NSRunningApplication* LaunchApplicationSyncExpectSuccess( 224 const FilePath& app_bundle_path, 225 const CommandLineArgs& command_line_args, 226 const std::vector<std::string>& url_specs, 227 LaunchApplicationOptions options) { 228 test::TestFuture<NSRunningApplication*, NSError*> result; 229 LaunchApplication(app_bundle_path, command_line_args, url_specs, options, 230 result.GetCallback()); 231 EXPECT_FALSE(result.Get<1>()); 232 EXPECT_TRUE(result.Get<0>()); 233 return result.Get<0>(); 234 } 235 236 // Similar to the above method, except that this version expects the launch to 237 // fail, returning the error. 238 NSError* LaunchApplicationSyncExpectError( 239 const FilePath& app_bundle_path, 240 const CommandLineArgs& command_line_args, 241 const std::vector<std::string>& url_specs, 242 LaunchApplicationOptions options) { 243 test::TestFuture<NSRunningApplication*, NSError*> result; 244 LaunchApplication(app_bundle_path, command_line_args, url_specs, options, 245 result.GetCallback()); 246 EXPECT_FALSE(result.Get<0>()); 247 EXPECT_TRUE(result.Get<1>()); 248 return result.Get<1>(); 249 } 250 251 // Waits for the total number of received launch events to reach at least 252 // `expected_count`. 253 void WaitForLaunchEvents(unsigned expected_count) { 254 if (LaunchEventCount() >= expected_count) { 255 return; 256 } 257 base::RunLoop loop; 258 launch_event_callback_ = BindLambdaForTesting([&] { 259 if (LaunchEventCount() >= expected_count) { 260 launch_event_callback_ = NullCallback(); 261 loop.Quit(); 262 } 263 }); 264 loop.Run(); 265 } 266 267 unsigned LaunchEventCount() { return launch_events_.count; } 268 NSString* LaunchEventName(unsigned i) { 269 if (i >= launch_events_.count) { 270 return nil; 271 } 272 return apple::ObjCCastStrict<NSString>(launch_events_[i][@"name"]); 273 } 274 NSDictionary* LaunchEventData(unsigned i) { 275 if (i >= launch_events_.count) { 276 return nil; 277 } 278 return apple::ObjCCastStrict<NSDictionary>(launch_events_[i][@"data"]); 279 } 280 281 protected: 282 ScopedTempDir temp_dir_; 283 284 NSString* helper_bundle_id_; 285 FilePath helper_app_bundle_path_; 286 FilePath helper_app_executable_path_; 287 FilePath helper_app_fifo_path_; 288 289 NSMutableArray<NSDictionary*>* launch_events_; 290 RepeatingClosure launch_event_callback_; 291 292 test::TaskEnvironment task_environment_{ 293 test::TaskEnvironment::MainThreadType::UI}; 294 295 private: 296 void OnLaunchEvent(NSDictionary* event) { 297 NSLog(@"Event: %@", event); 298 [launch_events_ addObject:event]; 299 if (launch_event_callback_) { 300 launch_event_callback_.Run(); 301 } 302 } 303}; 304 305TEST_F(LaunchApplicationTest, Basic) { 306 std::vector<std::string> command_line_args; 307 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 308 helper_app_bundle_path_, command_line_args, {}, {}); 309 ASSERT_TRUE(app); 310 EXPECT_NSEQ(app.bundleIdentifier, helper_bundle_id_); 311 EXPECT_EQ(apple::NSURLToFilePath(app.bundleURL), helper_app_bundle_path_); 312 313 WaitForLaunchEvents(1); 314 EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching"); 315 EXPECT_NSEQ(LaunchEventData(0)[@"activationPolicy"], 316 @(NSApplicationActivationPolicyRegular)); 317 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyRegular); 318 EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], 319 (@[ apple::FilePathToNSString(helper_app_executable_path_) ])); 320 EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"], 321 @(app.processIdentifier)); 322} 323 324TEST_F(LaunchApplicationTest, BundleDoesntExist) { 325 std::vector<std::string> command_line_args; 326 NSError* err = LaunchApplicationSyncExpectError( 327 temp_dir_.GetPath().AppendASCII("notexists.app"), command_line_args, {}, 328 {}); 329 ASSERT_TRUE(err); 330 err = LaunchApplicationSyncExpectError( 331 temp_dir_.GetPath().AppendASCII("notexists.app"), command_line_args, {}, 332 {.hidden_in_background = true}); 333 ASSERT_TRUE(err); 334} 335 336TEST_F(LaunchApplicationTest, BundleCorrupt) { 337 base::DeleteFile(helper_app_executable_path_); 338 std::vector<std::string> command_line_args; 339 NSError* err = LaunchApplicationSyncExpectError(helper_app_bundle_path_, 340 command_line_args, {}, {}); 341 ASSERT_TRUE(err); 342 err = LaunchApplicationSyncExpectError(helper_app_bundle_path_, 343 command_line_args, {}, 344 {.hidden_in_background = true}); 345 ASSERT_TRUE(err); 346} 347 348TEST_F(LaunchApplicationTest, CommandLineArgs_StringVector) { 349 std::vector<std::string> command_line_args = {"--foo", "bar", "-v"}; 350 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 351 helper_app_bundle_path_, command_line_args, {}, {}); 352 ASSERT_TRUE(app); 353 354 WaitForLaunchEvents(1); 355 EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching"); 356 EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[ 357 apple::FilePathToNSString(helper_app_executable_path_), 358 @"--foo", @"bar", @"-v" 359 ])); 360} 361 362TEST_F(LaunchApplicationTest, CommandLineArgs_BaseCommandLine) { 363 CommandLine command_line(CommandLine::NO_PROGRAM); 364 command_line.AppendSwitchASCII("foo", "bar"); 365 command_line.AppendSwitch("v"); 366 command_line.AppendSwitchPath("path", FilePath("/tmp")); 367 368 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 369 helper_app_bundle_path_, command_line, {}, {}); 370 ASSERT_TRUE(app); 371 372 WaitForLaunchEvents(1); 373 EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching"); 374 EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[ 375 apple::FilePathToNSString(helper_app_executable_path_), 376 @"--foo=bar", @"--v", @"--path=/tmp" 377 ])); 378} 379 380TEST_F(LaunchApplicationTest, UrlSpecs) { 381 std::vector<std::string> command_line_args; 382 std::vector<std::string> urls = {"https://example.com", 383 "x-chrome-launch://1"}; 384 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 385 helper_app_bundle_path_, command_line_args, urls, {}); 386 ASSERT_TRUE(app); 387 WaitForLaunchEvents(3); 388 389 EXPECT_NSEQ(LaunchEventName(0), @"openURLs"); 390 EXPECT_NSEQ(LaunchEventName(1), @"applicationDidFinishLaunching"); 391 EXPECT_NSEQ(LaunchEventName(2), @"openURLs"); 392 393 if (MacOSMajorVersion() == 11) { 394 // macOS 11 (and only macOS 11) appears to sometimes trigger the openURLs 395 // calls in reverse order. 396 std::vector<std::string> received_urls; 397 for (NSString* url in apple::ObjCCastStrict<NSArray>( 398 LaunchEventData(0)[@"urls"])) { 399 received_urls.push_back(SysNSStringToUTF8(url)); 400 } 401 EXPECT_EQ(received_urls.size(), 1u); 402 for (NSString* url in apple::ObjCCastStrict<NSArray>( 403 LaunchEventData(2)[@"urls"])) { 404 received_urls.push_back(SysNSStringToUTF8(url)); 405 } 406 EXPECT_THAT(received_urls, testing::UnorderedElementsAreArray(urls)); 407 } else { 408 EXPECT_NSEQ(LaunchEventData(0)[@"urls"], @[ @"https://example.com" ]); 409 EXPECT_NSEQ(LaunchEventData(2)[@"urls"], @[ @"x-chrome-launch://1" ]); 410 } 411} 412 413TEST_F(LaunchApplicationTest, CreateNewInstance) { 414 std::vector<std::string> command_line_args; 415 NSRunningApplication* app1 = LaunchApplicationSyncExpectSuccess( 416 helper_app_bundle_path_, command_line_args, {}, 417 {.create_new_instance = false}); 418 WaitForLaunchEvents(1); 419 EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching"); 420 EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"], 421 @(app1.processIdentifier)); 422 423 NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess( 424 helper_app_bundle_path_, command_line_args, {"x-chrome-launch://0"}, 425 {.create_new_instance = false}); 426 EXPECT_NSEQ(app1, app2); 427 EXPECT_EQ(app1.processIdentifier, app2.processIdentifier); 428 WaitForLaunchEvents(2); 429 EXPECT_NSEQ(LaunchEventName(1), @"openURLs"); 430 EXPECT_NSEQ(LaunchEventData(1)[@"processIdentifier"], 431 @(app2.processIdentifier)); 432 433 NSRunningApplication* app3 = LaunchApplicationSyncExpectSuccess( 434 helper_app_bundle_path_, command_line_args, {"x-chrome-launch://1"}, 435 {.create_new_instance = true}); 436 EXPECT_NSNE(app1, app3); 437 EXPECT_NE(app1.processIdentifier, app3.processIdentifier); 438 WaitForLaunchEvents(4); 439 EXPECT_NSEQ(LaunchEventName(2), @"openURLs"); 440 EXPECT_NSEQ(LaunchEventName(3), @"applicationDidFinishLaunching"); 441 EXPECT_NSEQ(LaunchEventData(3)[@"processIdentifier"], 442 @(app3.processIdentifier)); 443} 444 445TEST_F(LaunchApplicationTest, HiddenInBackground) { 446 std::vector<std::string> command_line_args = {"--test", "--foo"}; 447 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 448 helper_app_bundle_path_, command_line_args, {}, 449 {.hidden_in_background = true}); 450 ASSERT_TRUE(app); 451 EXPECT_NSEQ(app.bundleIdentifier, helper_bundle_id_); 452 EXPECT_EQ(helper_app_bundle_path_, apple::NSURLToFilePath(app.bundleURL)); 453 454 WaitForLaunchEvents(1); 455 EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching"); 456 EXPECT_NSEQ(LaunchEventData(0)[@"activationPolicy"], 457 @(NSApplicationActivationPolicyProhibited)); 458 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited); 459 EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[ 460 apple::FilePathToNSString(helper_app_executable_path_), 461 @"--test", @"--foo" 462 ])); 463 EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"], 464 @(app.processIdentifier)); 465 466 NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess( 467 helper_app_bundle_path_, command_line_args, {}, 468 {.create_new_instance = false, .hidden_in_background = true}); 469 EXPECT_NSEQ(app, app2); 470 EXPECT_EQ(app.processIdentifier, app2.processIdentifier); 471 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited); 472 EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyProhibited); 473 // Launching without opening anything should not trigger any launch events. 474 475 // Opening a URL in a new instance, should leave both instances in the 476 // background. 477 NSRunningApplication* app3 = LaunchApplicationSyncExpectSuccess( 478 helper_app_bundle_path_, command_line_args, {"x-chrome-launch://2"}, 479 {.create_new_instance = true, .hidden_in_background = true}); 480 EXPECT_NSNE(app, app3); 481 EXPECT_NE(app.processIdentifier, app3.processIdentifier); 482 WaitForLaunchEvents(3); 483 EXPECT_NSEQ(LaunchEventName(1), @"openURLs"); 484 EXPECT_NSEQ(LaunchEventName(2), @"applicationDidFinishLaunching"); 485 EXPECT_NSEQ(LaunchEventData(2)[@"processIdentifier"], 486 @(app3.processIdentifier)); 487 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited); 488 EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyProhibited); 489 EXPECT_EQ(app3.activationPolicy, NSApplicationActivationPolicyProhibited); 490} 491 492TEST_F(LaunchApplicationTest, 493 HiddenInBackground_OpenUrlChangesActivationPolicy) { 494 std::vector<std::string> command_line_args = {"--test", "--foo"}; 495 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 496 helper_app_bundle_path_, command_line_args, {}, 497 {.hidden_in_background = true}); 498 ASSERT_TRUE(app); 499 EXPECT_NSEQ(app.bundleIdentifier, helper_bundle_id_); 500 EXPECT_EQ(helper_app_bundle_path_, apple::NSURLToFilePath(app.bundleURL)); 501 502 WaitForLaunchEvents(1); 503 EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching"); 504 EXPECT_NSEQ(LaunchEventData(0)[@"activationPolicy"], 505 @(NSApplicationActivationPolicyProhibited)); 506 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited); 507 EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[ 508 apple::FilePathToNSString(helper_app_executable_path_), 509 @"--test", @"--foo" 510 ])); 511 EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"], 512 @(app.processIdentifier)); 513 514 NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess( 515 helper_app_bundle_path_, command_line_args, {"chrome://app-launch/0"}, 516 {.create_new_instance = false, .hidden_in_background = true}); 517 EXPECT_NSEQ(app, app2); 518 EXPECT_EQ(app.processIdentifier, app2.processIdentifier); 519 // Unexpected to me, but opening a URL seems to always change the activation 520 // policy. 521 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyRegular); 522 EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyRegular); 523 WaitForLaunchEvents(3); 524 EXPECT_THAT( 525 std::vector<std::string>({SysNSStringToUTF8(LaunchEventName(1)), 526 SysNSStringToUTF8(LaunchEventName(2))}), 527 testing::UnorderedElementsAre("activationPolicyChanged", "openURLs")); 528} 529 530TEST_F(LaunchApplicationTest, HiddenInBackground_TransitionToForeground) { 531 std::vector<std::string> command_line_args; 532 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 533 helper_app_bundle_path_, command_line_args, {"x-chrome-launch://1"}, 534 {.hidden_in_background = true}); 535 ASSERT_TRUE(app); 536 537 WaitForLaunchEvents(2); 538 EXPECT_NSEQ(LaunchEventName(0), @"openURLs"); 539 EXPECT_NSEQ(LaunchEventName(1), @"applicationDidFinishLaunching"); 540 EXPECT_NSEQ(LaunchEventData(1)[@"activationPolicy"], 541 @(NSApplicationActivationPolicyProhibited)); 542 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited); 543 EXPECT_NSEQ(LaunchEventData(1)[@"processIdentifier"], 544 @(app.processIdentifier)); 545 546 // Second launch with hidden_in_background set to false should cause the first 547 // app to switch activation policy. 548 NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess( 549 helper_app_bundle_path_, command_line_args, {}, 550 {.hidden_in_background = false}); 551 EXPECT_NSEQ(app, app2); 552 WaitForLaunchEvents(3); 553 EXPECT_NSEQ(LaunchEventName(2), @"activationPolicyChanged"); 554 EXPECT_NSEQ(LaunchEventData(2)[@"activationPolicy"], 555 @(NSApplicationActivationPolicyRegular)); 556 EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyRegular); 557} 558 559TEST_F(LaunchApplicationTest, HiddenInBackground_AlreadyInForeground) { 560 std::vector<std::string> command_line_args; 561 NSRunningApplication* app = LaunchApplicationSyncExpectSuccess( 562 helper_app_bundle_path_, command_line_args, {"x-chrome-launch://1"}, 563 {.hidden_in_background = false}); 564 ASSERT_TRUE(app); 565 566 WaitForLaunchEvents(2); 567 EXPECT_NSEQ(LaunchEventName(0), @"openURLs"); 568 EXPECT_NSEQ(LaunchEventName(1), @"applicationDidFinishLaunching"); 569 EXPECT_NSEQ(LaunchEventData(1)[@"activationPolicy"], 570 @(NSApplicationActivationPolicyRegular)); 571 EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyRegular); 572 EXPECT_NSEQ(LaunchEventData(1)[@"processIdentifier"], 573 @(app.processIdentifier)); 574 575 // Second (and third) launch with hidden_in_background set to true should 576 // reuse the existing app and keep it visible. 577 NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess( 578 helper_app_bundle_path_, command_line_args, {}, 579 {.hidden_in_background = true}); 580 EXPECT_NSEQ(app, app2); 581 EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyRegular); 582 NSRunningApplication* app3 = LaunchApplicationSyncExpectSuccess( 583 helper_app_bundle_path_, command_line_args, {"x-chrome-launch://23"}, 584 {.hidden_in_background = true}); 585 EXPECT_NSEQ(app, app3); 586 WaitForLaunchEvents(3); 587 EXPECT_NSEQ(LaunchEventName(2), @"openURLs"); 588 EXPECT_NSEQ(LaunchEventData(2)[@"processIdentifier"], 589 @(app.processIdentifier)); 590 EXPECT_EQ(app3.activationPolicy, NSApplicationActivationPolicyRegular); 591} 592 593} // namespace 594} // namespace base::mac 595