• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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