xref: /aosp_15_r20/external/cronet/base/mac/launch_application_unittest.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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