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