chromium/base/mac/launch_application_unittest.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/mac/launch_application.h"

#include <sys/select.h>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/base_paths.h"
#include "base/compiler_specific.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/threading/platform_thread.h"
#include "base/types/expected.h"
#include "base/uuid.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"

namespace base::mac {
namespace {

// Reads XML encoded property lists from `fifo_path`, calling `callback` for
// each succesfully parsed dictionary. Loops indefinitely until the string
// "<!FINISHED>" is read from `fifo_path`.
void ReadLaunchEventsFromFifo(
    const FilePath& fifo_path,
    RepeatingCallback<void(NSDictionary* event)> callback) {
  File f(fifo_path, File::FLAG_OPEN | File::FLAG_READ);
  std::string data;
  while (true) {
    char buf[4096];
    int read_count =
        UNSAFE_TODO(f.ReadAtCurrentPosNoBestEffort(buf, sizeof buf));
    if (read_count) {
      data += std::string(buf, read_count);
      // Assume that at any point the beginning of the data buffer is the start
      // of a plist. Search for the first end, and parse that substring.
      size_t end_of_plist;
      while ((end_of_plist = data.find("</plist>")) != std::string::npos) {
        std::string plist = data.substr(0, end_of_plist + 8);
        data = data.substr(plist.length());
        NSDictionary* event = apple::ObjCCastStrict<NSDictionary>(
            SysUTF8ToNSString(TrimWhitespaceASCII(plist, TRIM_ALL))
                .propertyList);
        callback.Run(event);
      }
      // No more plists found, check if the termination marker was send.
      if (data.find("<!FINISHED>") != std::string::npos) {
        break;
      }
    } else {
      // No data was read, wait for the file descriptor to become readable
      // again.
      fd_set fds;
      FD_ZERO(&fds);
      FD_SET(f.GetPlatformFile(), &fds);
      select(FD_SETSIZE, &fds, nullptr, nullptr, nullptr);
    }
  }
}

// This test harness creates an app bundle with a random bundle identifier to
// avoid conflicts with concurrently running other tests. The binary in this app
// bundle writes various events to a named pipe, allowing tests here to verify
// that correct events were received by the app.
class LaunchApplicationTest : public testing::Test {
 public:
  void SetUp() override {
    helper_bundle_id_ =
        SysUTF8ToNSString("org.chromium.LaunchApplicationTestHelper." +
                          Uuid::GenerateRandomV4().AsLowercaseString());

    FilePath data_root;
    ASSERT_TRUE(PathService::Get(DIR_OUT_TEST_DATA_ROOT, &data_root));
    const FilePath helper_app_executable =
        data_root.AppendASCII("launch_application_test_helper");

    // Put helper app inside home dir, as the default temp location gets special
    // treatment by launch services, effecting the behavior of some of these
    // tests.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDirUnderPath(base::GetHomeDir()));

    helper_app_bundle_path_ =
        temp_dir_.GetPath().AppendASCII("launch_application_test_helper.app");

    const base::FilePath destination_contents_path =
        helper_app_bundle_path_.AppendASCII("Contents");
    const base::FilePath destination_executable_path =
        destination_contents_path.AppendASCII("MacOS");

    // First create the .app bundle directory structure.
    // Use NSFileManager so that the permissions can be set appropriately. The
    // base::CreateDirectory() routine forces mode 0700.
    NSError* error = nil;
    ASSERT_TRUE([NSFileManager.defaultManager
               createDirectoryAtURL:base::apple::FilePathToNSURL(
                                        destination_executable_path)
        withIntermediateDirectories:YES
                         attributes:@{
                           NSFilePosixPermissions : @(0755)
                         }
                              error:&error])
        << SysNSStringToUTF8(error.description);

    // Copy the executable file.
    helper_app_executable_path_ =
        destination_executable_path.Append(helper_app_executable.BaseName());
    ASSERT_TRUE(
        base::CopyFile(helper_app_executable, helper_app_executable_path_));

    // Write the PkgInfo file.
    constexpr char kPkgInfoData[] = "APPL????";
    ASSERT_TRUE(base::WriteFile(
        destination_contents_path.AppendASCII("PkgInfo"), kPkgInfoData));

#if defined(ADDRESS_SANITIZER)
    const base::FilePath asan_library_path =
        data_root.AppendASCII("libclang_rt.asan_osx_dynamic.dylib");
    ASSERT_TRUE(base::CopyFile(
        asan_library_path,
        destination_executable_path.Append(asan_library_path.BaseName())));
#endif

    // Generate the Plist file
    NSDictionary* plist = @{
      @"CFBundleExecutable" :
          apple::FilePathToNSString(helper_app_executable.BaseName()),
      @"CFBundleIdentifier" : helper_bundle_id_,
    };
    ASSERT_TRUE([plist
        writeToURL:apple::FilePathToNSURL(
                       destination_contents_path.AppendASCII("Info.plist"))
             error:nil]);

    // Register the app with LaunchServices.
    LSRegisterURL(base::apple::FilePathToCFURL(helper_app_bundle_path_).get(),
                  true);

    // Ensure app was registered with LaunchServices. Sometimes it takes a
    // little bit of time for this to happen, and some tests might fail if the
    // app wasn't registered yet.
    while (true) {
      NSArray<NSURL*>* apps = nil;
      if (@available(macOS 12.0, *)) {
        apps = [[NSWorkspace sharedWorkspace]
            URLsForApplicationsWithBundleIdentifier:helper_bundle_id_];
      } else {
        apps =
            apple::CFToNSOwnershipCast(LSCopyApplicationURLsForBundleIdentifier(
                apple::NSToCFPtrCast(helper_bundle_id_), /*outError=*/nullptr));
      }
      if (apps.count > 0) {
        break;
      }
      PlatformThread::Sleep(Milliseconds(50));
    }

    // Setup fifo to receive logs from the helper app.
    helper_app_fifo_path_ =
        temp_dir_.GetPath().AppendASCII("launch_application_test_helper.fifo");
    ASSERT_EQ(0, mkfifo(helper_app_fifo_path_.value().c_str(),
                        S_IWUSR | S_IRUSR | S_IWGRP | S_IWGRP));

    // Create array to store received events in, and start listening for events.
    launch_events_ = [[NSMutableArray alloc] init];
    base::ThreadPool::PostTask(
        FROM_HERE, {MayBlock()},
        base::BindOnce(
            &ReadLaunchEventsFromFifo, helper_app_fifo_path_,
            BindPostTaskToCurrentDefault(BindRepeating(
                &LaunchApplicationTest::OnLaunchEvent, Unretained(this)))));
  }

  void TearDown() override {
    if (temp_dir_.IsValid()) {
      // Make sure fifo reading task stops reading/waiting.
      WriteFile(helper_app_fifo_path_, "<!FINISHED>");

      // Make sure all apps that were launched by this test are terminated.
      NSArray<NSRunningApplication*>* apps =
          NSWorkspace.sharedWorkspace.runningApplications;
      for (NSRunningApplication* app in apps) {
        if (temp_dir_.GetPath().IsParent(
                apple::NSURLToFilePath(app.bundleURL)) ||
            [app.bundleIdentifier isEqualToString:helper_bundle_id_]) {
          [app forceTerminate];
        }
      }

      // And make sure the temp dir was successfully deleted.
      EXPECT_TRUE(temp_dir_.Delete());
    }
  }

  // Calls `LaunchApplication` with the given parameters, expecting the launch
  // to succeed. Returns the `NSRunningApplication*` the callback passed to
  // `LaunchApplication` was called with.
  NSRunningApplication* LaunchApplicationSyncExpectSuccess(
      const FilePath& app_bundle_path,
      const CommandLineArgs& command_line_args,
      const std::vector<std::string>& url_specs,
      LaunchApplicationOptions options) {
    test::TestFuture<NSRunningApplication*, NSError*> result;
    LaunchApplication(app_bundle_path, command_line_args, url_specs, options,
                      result.GetCallback());
    EXPECT_FALSE(result.Get<1>());
    EXPECT_TRUE(result.Get<0>());
    return result.Get<0>();
  }

  // Similar to the above method, except that this version expects the launch to
  // fail, returning the error.
  NSError* LaunchApplicationSyncExpectError(
      const FilePath& app_bundle_path,
      const CommandLineArgs& command_line_args,
      const std::vector<std::string>& url_specs,
      LaunchApplicationOptions options) {
    test::TestFuture<NSRunningApplication*, NSError*> result;
    LaunchApplication(app_bundle_path, command_line_args, url_specs, options,
                      result.GetCallback());
    EXPECT_FALSE(result.Get<0>());
    EXPECT_TRUE(result.Get<1>());
    return result.Get<1>();
  }

  // Waits for the total number of received launch events to reach at least
  // `expected_count`.
  void WaitForLaunchEvents(unsigned expected_count) {
    if (LaunchEventCount() >= expected_count) {
      return;
    }
    base::RunLoop loop;
    launch_event_callback_ = BindLambdaForTesting([&]() {
      if (LaunchEventCount() >= expected_count) {
        launch_event_callback_ = NullCallback();
        loop.Quit();
      }
    });
    loop.Run();
  }

  unsigned LaunchEventCount() { return launch_events_.count; }
  NSString* LaunchEventName(unsigned i) {
    if (i >= launch_events_.count) {
      return nil;
    }
    return apple::ObjCCastStrict<NSString>(launch_events_[i][@"name"]);
  }
  NSDictionary* LaunchEventData(unsigned i) {
    if (i >= launch_events_.count) {
      return nil;
    }
    return apple::ObjCCastStrict<NSDictionary>(launch_events_[i][@"data"]);
  }

 protected:
  ScopedTempDir temp_dir_;

  NSString* helper_bundle_id_;
  FilePath helper_app_bundle_path_;
  FilePath helper_app_executable_path_;
  FilePath helper_app_fifo_path_;

  NSMutableArray<NSDictionary*>* launch_events_;
  RepeatingClosure launch_event_callback_;

  test::TaskEnvironment task_environment_{
      test::TaskEnvironment::MainThreadType::UI};

 private:
  void OnLaunchEvent(NSDictionary* event) {
    NSLog(@"Event: %@", event);
    [launch_events_ addObject:event];
    if (launch_event_callback_) {
      launch_event_callback_.Run();
    }
  }
};

TEST_F(LaunchApplicationTest, Basic) {
  std::vector<std::string> command_line_args;
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {}, {});
  ASSERT_TRUE(app);
  EXPECT_NSEQ(app.bundleIdentifier, helper_bundle_id_);
  EXPECT_EQ(apple::NSURLToFilePath(app.bundleURL), helper_app_bundle_path_);

  WaitForLaunchEvents(1);
  EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(0)[@"activationPolicy"],
              @(NSApplicationActivationPolicyRegular));
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyRegular);
  EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"],
              (@[ apple::FilePathToNSString(helper_app_executable_path_) ]));
  EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"],
              @(app.processIdentifier));
}

TEST_F(LaunchApplicationTest, BundleDoesntExist) {
  std::vector<std::string> command_line_args;
  NSError* err = LaunchApplicationSyncExpectError(
      temp_dir_.GetPath().AppendASCII("notexists.app"), command_line_args, {},
      {});
  ASSERT_TRUE(err);
  err = LaunchApplicationSyncExpectError(
      temp_dir_.GetPath().AppendASCII("notexists.app"), command_line_args, {},
      {.hidden_in_background = true});
  ASSERT_TRUE(err);
}

TEST_F(LaunchApplicationTest, BundleCorrupt) {
  base::DeleteFile(helper_app_executable_path_);
  std::vector<std::string> command_line_args;
  NSError* err = LaunchApplicationSyncExpectError(helper_app_bundle_path_,
                                                  command_line_args, {}, {});
  ASSERT_TRUE(err);
  err = LaunchApplicationSyncExpectError(helper_app_bundle_path_,
                                         command_line_args, {},
                                         {.hidden_in_background = true});
  ASSERT_TRUE(err);
}

TEST_F(LaunchApplicationTest, CommandLineArgs_StringVector) {
  std::vector<std::string> command_line_args = {"--foo", "bar", "-v"};
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {}, {});
  ASSERT_TRUE(app);

  WaitForLaunchEvents(1);
  EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[
                apple::FilePathToNSString(helper_app_executable_path_),
                @"--foo", @"bar", @"-v"
              ]));
}

TEST_F(LaunchApplicationTest, CommandLineArgs_BaseCommandLine) {
  CommandLine command_line(CommandLine::NO_PROGRAM);
  command_line.AppendSwitchASCII("foo", "bar");
  command_line.AppendSwitch("v");
  command_line.AppendSwitchPath("path", FilePath("/tmp"));

  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line, {}, {});
  ASSERT_TRUE(app);

  WaitForLaunchEvents(1);
  EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[
                apple::FilePathToNSString(helper_app_executable_path_),
                @"--foo=bar", @"--v", @"--path=/tmp"
              ]));
}

TEST_F(LaunchApplicationTest, UrlSpecs) {
  std::vector<std::string> command_line_args;
  std::vector<std::string> urls = {"https://example.com",
                                   "x-chrome-launch://1"};
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, urls, {});
  ASSERT_TRUE(app);
  WaitForLaunchEvents(3);

  EXPECT_NSEQ(LaunchEventName(0), @"openURLs");
  EXPECT_NSEQ(LaunchEventName(1), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventName(2), @"openURLs");

  if (MacOSMajorVersion() == 11) {
    // macOS 11 (and only macOS 11) appears to sometimes trigger the openURLs
    // calls in reverse order.
    std::vector<std::string> received_urls;
    for (NSString* url in apple::ObjCCastStrict<NSArray>(
             LaunchEventData(0)[@"urls"])) {
      received_urls.push_back(SysNSStringToUTF8(url));
    }
    EXPECT_EQ(received_urls.size(), 1u);
    for (NSString* url in apple::ObjCCastStrict<NSArray>(
             LaunchEventData(2)[@"urls"])) {
      received_urls.push_back(SysNSStringToUTF8(url));
    }
    EXPECT_THAT(received_urls, testing::UnorderedElementsAreArray(urls));
  } else {
    EXPECT_NSEQ(LaunchEventData(0)[@"urls"], @[ @"https://example.com" ]);
    EXPECT_NSEQ(LaunchEventData(2)[@"urls"], @[ @"x-chrome-launch://1" ]);
  }
}

TEST_F(LaunchApplicationTest, CreateNewInstance) {
  std::vector<std::string> command_line_args;
  NSRunningApplication* app1 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {},
      {.create_new_instance = false});
  WaitForLaunchEvents(1);
  EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"],
              @(app1.processIdentifier));

  NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"x-chrome-launch://0"},
      {.create_new_instance = false});
  EXPECT_NSEQ(app1, app2);
  EXPECT_EQ(app1.processIdentifier, app2.processIdentifier);
  WaitForLaunchEvents(2);
  EXPECT_NSEQ(LaunchEventName(1), @"openURLs");
  EXPECT_NSEQ(LaunchEventData(1)[@"processIdentifier"],
              @(app2.processIdentifier));

  NSRunningApplication* app3 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"x-chrome-launch://1"},
      {.create_new_instance = true});
  EXPECT_NSNE(app1, app3);
  EXPECT_NE(app1.processIdentifier, app3.processIdentifier);
  WaitForLaunchEvents(4);
  EXPECT_NSEQ(LaunchEventName(2), @"openURLs");
  EXPECT_NSEQ(LaunchEventName(3), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(3)[@"processIdentifier"],
              @(app3.processIdentifier));
}

TEST_F(LaunchApplicationTest, HiddenInBackground) {
  std::vector<std::string> command_line_args = {"--test", "--foo"};
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {},
      {.hidden_in_background = true});
  ASSERT_TRUE(app);
  EXPECT_NSEQ(app.bundleIdentifier, helper_bundle_id_);
  EXPECT_EQ(helper_app_bundle_path_, apple::NSURLToFilePath(app.bundleURL));

  WaitForLaunchEvents(1);
  EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(0)[@"activationPolicy"],
              @(NSApplicationActivationPolicyProhibited));
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited);
  EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[
                apple::FilePathToNSString(helper_app_executable_path_),
                @"--test", @"--foo"
              ]));
  EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"],
              @(app.processIdentifier));

  NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {},
      {.create_new_instance = false, .hidden_in_background = true});
  EXPECT_NSEQ(app, app2);
  EXPECT_EQ(app.processIdentifier, app2.processIdentifier);
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited);
  EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyProhibited);
  // Launching without opening anything should not trigger any launch events.

  // Opening a URL in a new instance, should leave both instances in the
  // background.
  NSRunningApplication* app3 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"x-chrome-launch://2"},
      {.create_new_instance = true, .hidden_in_background = true});
  EXPECT_NSNE(app, app3);
  EXPECT_NE(app.processIdentifier, app3.processIdentifier);
  WaitForLaunchEvents(3);
  EXPECT_NSEQ(LaunchEventName(1), @"openURLs");
  EXPECT_NSEQ(LaunchEventName(2), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(2)[@"processIdentifier"],
              @(app3.processIdentifier));
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited);
  EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyProhibited);
  EXPECT_EQ(app3.activationPolicy, NSApplicationActivationPolicyProhibited);
}

TEST_F(LaunchApplicationTest,
       HiddenInBackground_OpenUrlChangesActivationPolicy) {
  std::vector<std::string> command_line_args = {"--test", "--foo"};
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {},
      {.hidden_in_background = true});
  ASSERT_TRUE(app);
  EXPECT_NSEQ(app.bundleIdentifier, helper_bundle_id_);
  EXPECT_EQ(helper_app_bundle_path_, apple::NSURLToFilePath(app.bundleURL));

  WaitForLaunchEvents(1);
  EXPECT_NSEQ(LaunchEventName(0), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(0)[@"activationPolicy"],
              @(NSApplicationActivationPolicyProhibited));
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited);
  EXPECT_NSEQ(LaunchEventData(0)[@"commandLine"], (@[
                apple::FilePathToNSString(helper_app_executable_path_),
                @"--test", @"--foo"
              ]));
  EXPECT_NSEQ(LaunchEventData(0)[@"processIdentifier"],
              @(app.processIdentifier));

  NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"chrome://app-launch/0"},
      {.create_new_instance = false, .hidden_in_background = true});
  EXPECT_NSEQ(app, app2);
  EXPECT_EQ(app.processIdentifier, app2.processIdentifier);
  // Unexpected to me, but opening a URL seems to always change the activation
  // policy.
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyRegular);
  EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyRegular);
  WaitForLaunchEvents(3);
  EXPECT_THAT(
      std::vector<std::string>({SysNSStringToUTF8(LaunchEventName(1)),
                                SysNSStringToUTF8(LaunchEventName(2))}),
      testing::UnorderedElementsAre("activationPolicyChanged", "openURLs"));
}

TEST_F(LaunchApplicationTest, HiddenInBackground_TransitionToForeground) {
  std::vector<std::string> command_line_args;
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"x-chrome-launch://1"},
      {.hidden_in_background = true});
  ASSERT_TRUE(app);

  WaitForLaunchEvents(2);
  EXPECT_NSEQ(LaunchEventName(0), @"openURLs");
  EXPECT_NSEQ(LaunchEventName(1), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(1)[@"activationPolicy"],
              @(NSApplicationActivationPolicyProhibited));
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyProhibited);
  EXPECT_NSEQ(LaunchEventData(1)[@"processIdentifier"],
              @(app.processIdentifier));

  // Second launch with hidden_in_background set to false should cause the first
  // app to switch activation policy.
  NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {},
      {.hidden_in_background = false});
  EXPECT_NSEQ(app, app2);
  WaitForLaunchEvents(3);
  EXPECT_NSEQ(LaunchEventName(2), @"activationPolicyChanged");
  EXPECT_NSEQ(LaunchEventData(2)[@"activationPolicy"],
              @(NSApplicationActivationPolicyRegular));
  EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyRegular);
}

TEST_F(LaunchApplicationTest, HiddenInBackground_AlreadyInForeground) {
  std::vector<std::string> command_line_args;
  NSRunningApplication* app = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"x-chrome-launch://1"},
      {.hidden_in_background = false});
  ASSERT_TRUE(app);

  WaitForLaunchEvents(2);
  EXPECT_NSEQ(LaunchEventName(0), @"openURLs");
  EXPECT_NSEQ(LaunchEventName(1), @"applicationDidFinishLaunching");
  EXPECT_NSEQ(LaunchEventData(1)[@"activationPolicy"],
              @(NSApplicationActivationPolicyRegular));
  EXPECT_EQ(app.activationPolicy, NSApplicationActivationPolicyRegular);
  EXPECT_NSEQ(LaunchEventData(1)[@"processIdentifier"],
              @(app.processIdentifier));

  // Second (and third) launch with hidden_in_background set to true should
  // reuse the existing app and keep it visible.
  NSRunningApplication* app2 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {},
      {.hidden_in_background = true});
  EXPECT_NSEQ(app, app2);
  EXPECT_EQ(app2.activationPolicy, NSApplicationActivationPolicyRegular);
  NSRunningApplication* app3 = LaunchApplicationSyncExpectSuccess(
      helper_app_bundle_path_, command_line_args, {"x-chrome-launch://23"},
      {.hidden_in_background = true});
  EXPECT_NSEQ(app, app3);
  WaitForLaunchEvents(3);
  EXPECT_NSEQ(LaunchEventName(2), @"openURLs");
  EXPECT_NSEQ(LaunchEventData(2)[@"processIdentifier"],
              @(app.processIdentifier));
  EXPECT_EQ(app3.activationPolicy, NSApplicationActivationPolicyRegular);
}

}  // namespace
}  // namespace base::mac