chromium/chrome/browser/app_controller_mac_unittest.mm

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

#import "chrome/browser/app_controller_mac.h"

#import <Cocoa/Cocoa.h>

#include "base/apple/scoped_objc_class_swizzler.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/delete_profile_helper.h"
#include "chrome/browser/profiles/profile_attributes_init_params.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/platform_test.h"
#include "ui/base/l10n/l10n_util_mac.h"

namespace {

id __weak* TargetForAction() {
  static id __weak targetForAction;
  return &targetForAction;
}

}  // namespace

@interface FakeBrowserWindow : NSWindow
@end

@implementation FakeBrowserWindow
@end

// A class providing alternative implementations of various methods.
@interface AppControllerKeyEquivalentTestHelper : NSObject
- (id __weak)targetForAction:(SEL)selector;
- (BOOL)windowHasBrowserTabs:(NSWindow*)window;
@end

@implementation AppControllerKeyEquivalentTestHelper

- (id __weak)targetForAction:(SEL)selector {
  return *TargetForAction();
}

- (BOOL)windowHasBrowserTabs:(NSWindow*)window {
  return [window isKindOfClass:[FakeBrowserWindow class]];
}

@end

class AppControllerTest : public PlatformTest {
 protected:
  AppControllerTest()
      : profile_manager_(TestingBrowserProcess::GetGlobal()),
        profile_(nullptr) {}

  void SetUp() override {
    PlatformTest::SetUp();
    ASSERT_TRUE(profile_manager_.SetUp());
    profile_ = profile_manager_.CreateTestingProfile("New Profile 1");
  }

  void TearDown() override {
    TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
    base::RunLoop().RunUntilIdle();
    PlatformTest::TearDown();
  }

  content::BrowserTaskEnvironment task_environment_;
  TestingProfileManager profile_manager_;
  raw_ptr<TestingProfile, DanglingUntriaged> profile_;
};

class AppControllerKeyEquivalentTest : public PlatformTest {
 protected:
  AppControllerKeyEquivalentTest() = default;

  void SetUp() override {
    PlatformTest::SetUp();

    _nsapp_target_for_action_swizzler =
        std::make_unique<base::apple::ScopedObjCClassSwizzler>(
            [NSApp class], [AppControllerKeyEquivalentTestHelper class],
            @selector(targetForAction:));
    _app_controller_swizzler =
        std::make_unique<base::apple::ScopedObjCClassSwizzler>(
            [AppController class], [AppControllerKeyEquivalentTestHelper class],
            @selector(windowHasBrowserTabs:));

    _app_controller = AppController.sharedController;

    _cmdw_menu_item = [[NSMenuItem alloc] initWithTitle:@""
                                                 action:nullptr
                                          keyEquivalent:@"w"];
    [_app_controller setCmdWMenuItemForTesting:_cmdw_menu_item];

    _shift_cmdw_menu_item = [[NSMenuItem alloc] initWithTitle:@""
                                                       action:nullptr
                                                keyEquivalent:@"W"];
    [_app_controller setShiftCmdWMenuItemForTesting:_shift_cmdw_menu_item];
  }

  void CheckMenuItemsMatchBrowserWindow() {
    ASSERT_EQ([NSApp targetForAction:@selector(performClose:)],
              *TargetForAction());

    [_app_controller updateMenuItemKeyEquivalents];

    EXPECT_FALSE(_shift_cmdw_menu_item.hidden);
    EXPECT_EQ(_shift_cmdw_menu_item.tag, IDC_CLOSE_WINDOW);
    EXPECT_EQ(_shift_cmdw_menu_item.action, @selector(performClose:));
    EXPECT_TRUE([_shift_cmdw_menu_item.title
        isEqualToString:l10n_util::GetNSStringWithFixup(IDS_CLOSE_WINDOW_MAC)]);

    EXPECT_FALSE(_cmdw_menu_item.hidden);
    EXPECT_EQ(_cmdw_menu_item.tag, IDC_CLOSE_TAB);
    EXPECT_EQ(_cmdw_menu_item.action, @selector(commandDispatch:));
    EXPECT_TRUE([_cmdw_menu_item.title
        isEqualToString:l10n_util::GetNSStringWithFixup(IDS_CLOSE_TAB_MAC)]);
  }

  void CheckMenuItemsMatchNonBrowserWindow() {
    ASSERT_EQ([NSApp targetForAction:@selector(performClose:)],
              *TargetForAction());

    [_app_controller updateMenuItemKeyEquivalents];

    EXPECT_TRUE(_shift_cmdw_menu_item.hidden);

    EXPECT_FALSE(_cmdw_menu_item.hidden);
    EXPECT_EQ(_cmdw_menu_item.tag, IDC_CLOSE_WINDOW);
    EXPECT_EQ(_cmdw_menu_item.action, @selector(performClose:));
    EXPECT_TRUE([_cmdw_menu_item.title
        isEqualToString:l10n_util::GetNSStringWithFixup(IDS_CLOSE_WINDOW_MAC)]);
  }

  void TearDown() override {
    PlatformTest::TearDown();

    [_app_controller setCmdWMenuItemForTesting:nil];
    [_app_controller setShiftCmdWMenuItemForTesting:nil];
    *TargetForAction() = nil;
  }

 private:
  std::unique_ptr<base::apple::ScopedObjCClassSwizzler>
      _nsapp_target_for_action_swizzler;
  std::unique_ptr<base::apple::ScopedObjCClassSwizzler>
      _app_controller_swizzler;
  AppController* __strong _app_controller;
  NSMenuItem* __strong _cmdw_menu_item;
  NSMenuItem* __strong _shift_cmdw_menu_item;
};

TEST_F(AppControllerTest, DockMenuProfileNotLoaded) {
  AppController* app_controller = AppController.sharedController;
  NSMenu* menu = [app_controller applicationDockMenu:NSApp];
  // Incognito item is hidden when the profile is not loaded.
  EXPECT_EQ(nil, [app_controller lastProfileIfLoaded]);
  EXPECT_EQ(-1, [menu indexOfItemWithTag:IDC_NEW_INCOGNITO_WINDOW]);
}

TEST_F(AppControllerTest, DockMenu) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  NSMenu* menu = [app_controller applicationDockMenu:NSApp];
  NSMenuItem* item;

  EXPECT_TRUE(menu);
  EXPECT_NE(-1, [menu indexOfItemWithTag:IDC_NEW_WINDOW]);

  // Incognito item is shown when the profile is loaded.
  EXPECT_EQ(profile_, [app_controller lastProfileIfLoaded]);
  EXPECT_NE(-1, [menu indexOfItemWithTag:IDC_NEW_INCOGNITO_WINDOW]);

  for (item in [menu itemArray]) {
    EXPECT_EQ(app_controller, [item target]);
    EXPECT_EQ(@selector(commandFromDock:), [item action]);
  }
}

TEST_F(AppControllerTest, LastProfileIfLoaded) {
  // Create a second profile.
  base::FilePath dest_path1 = profile_->GetPath();
  base::FilePath dest_path2 =
      profile_manager_.CreateTestingProfile("New Profile 2")->GetPath();
  ASSERT_EQ(2U, profile_manager_.profile_manager()->GetNumberOfProfiles());
  ASSERT_EQ(2U, profile_manager_.profile_manager()->GetLoadedProfiles().size());

  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         dest_path1.BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;

  // Delete the active profile.
  profile_manager_.profile_manager()
      ->GetDeleteProfileHelper()
      .MaybeScheduleProfileForDeletion(
          dest_path1, base::DoNothing(),
          ProfileMetrics::DELETE_PROFILE_USER_MANAGER);

  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(dest_path2, app_controller.lastProfileIfLoaded->GetPath());
}

// Tests key equivalents for Close Window when target is a child window (like a
// bubble).
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForBubbleWindow) {
  // Set up the "bubble" and main window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* child_window =
      [[NSWindow alloc] initWithContentRect:kContentRect
                                  styleMask:NSWindowStyleMaskClosable
                                    backing:NSBackingStoreBuffered
                                      defer:YES];
  child_window.releasedWhenClosed = NO;
  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];
  browser_window.releasedWhenClosed = NO;

  [browser_window addChildWindow:child_window ordered:NSWindowAbove];

  *TargetForAction() = child_window;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is an NSPopOver.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForPopover) {
  // Set up the popover and main window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSPopover* popover = [[NSPopover alloc] init];
  NSWindow* popover_window =
      [[NSWindow alloc] initWithContentRect:kContentRect
                                  styleMask:NSWindowStyleMaskClosable
                                    backing:NSBackingStoreBuffered
                                      defer:YES];
  popover_window.releasedWhenClosed = NO;

  [popover setContentViewController:[[NSViewController alloc] init]];
  [[popover contentViewController] setView:[popover_window contentView]];

  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];
  browser_window.releasedWhenClosed = NO;
  [browser_window addChildWindow:popover_window ordered:NSWindowAbove];

  *TargetForAction() = popover;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is a browser window.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForBrowserWindow) {
  // Set up the browser window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];

  *TargetForAction() = browser_window;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is a descendant of a
// browser window.
TEST_F(AppControllerKeyEquivalentTest,
       UpdateMenuItemsForBrowserWindowDescendant) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(features::kImmersiveFullscreen);

  // Set up the browser window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];

  // Set up descendants.
  NSWindow* child_window = [[NSWindow alloc] init];
  [browser_window addChildWindow:child_window ordered:NSWindowAbove];
  NSWindow* child_child_window = [[NSWindow alloc] init];
  [child_window addChildWindow:child_child_window ordered:NSWindowAbove];

  *TargetForAction() = child_child_window;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is not a browser window.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForNonBrowserWindow) {
  // Set up the window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* main_window =
      [[NSWindow alloc] initWithContentRect:kContentRect
                                  styleMask:NSWindowStyleMaskClosable
                                    backing:NSBackingStoreBuffered
                                      defer:YES];

  *TargetForAction() = main_window;

  CheckMenuItemsMatchNonBrowserWindow();
}

// Tests key equivalents for Close Window when target is not a window.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForNonWindow) {
  NSObject* non_window_object = [[NSObject alloc] init];
  *TargetForAction() = non_window_object;

  CheckMenuItemsMatchNonBrowserWindow();
}

// Tests key equivalents for Close Window and Close Tab when we shift from one
// browser window to no browser windows, and then back to one browser window.
TEST_F(AppControllerKeyEquivalentTest, MenuItemsUpdateWithWindowChanges) {
  // Set up the browser window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];

  *TargetForAction() = browser_window;

  CheckMenuItemsMatchBrowserWindow();

  // "Close" it.
  NSObject* non_window_object = [[NSObject alloc] init];
  *TargetForAction() = non_window_object;

  CheckMenuItemsMatchNonBrowserWindow();

  // "New" window.
  *TargetForAction() = browser_window;

  CheckMenuItemsMatchBrowserWindow();
}

class AppControllerSafeProfileTest : public AppControllerTest {
 protected:
  AppControllerSafeProfileTest() = default;
  ~AppControllerSafeProfileTest() override = default;
};

// Tests that RunInLastProfileSafely() works with an already-loaded
// profile.
TEST_F(AppControllerSafeProfileTest, LastProfileLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  base::RunLoop run_loop;
  app_controller_mac::RunInLastProfileSafely(
      base::BindLambdaForTesting([&](Profile* profile) {
        EXPECT_EQ(profile, profile_.get());
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInLastProfileSafely() re-loads the profile from disk if
// it's not currently in memory.
TEST_F(AppControllerSafeProfileTest, LastProfileNotLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed, "New Profile 2");

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(nil, app_controller.lastProfileIfLoaded);

  base::RunLoop run_loop;
  app_controller_mac::RunInLastProfileSafely(
      base::BindLambdaForTesting([&](Profile* profile) {
        EXPECT_NE(profile, nullptr);
        EXPECT_NE(profile, profile_.get());
        EXPECT_EQ(profile->GetBaseName().MaybeAsASCII(), "New Profile 2");
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInProfileInSafeProfileHelper::RunInProfile() works with an
// already-loaded profile.
TEST_F(AppControllerSafeProfileTest, SpecificProfileLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  TestingProfile* profile2 =
      profile_manager_.CreateTestingProfile("New Profile 2");

  base::RunLoop run_loop;
  app_controller_mac::RunInProfileSafely(
      profile_manager_.profiles_dir().AppendASCII("New Profile 2"),
      base::BindLambdaForTesting([&](Profile* profile) {
        // This should run with the specific profile we asked for, rather than
        // the last-used profile.
        EXPECT_EQ(profile, profile2);
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInProfileSafely() re-loads the profile from
// disk if it's not currently in memory.
TEST_F(AppControllerSafeProfileTest, SpecificProfileNotLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  // Add a profile in the cache (simulate another profile on disk).
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  ProfileAttributesStorage* profile_storage =
      &profile_manager->GetProfileAttributesStorage();
  const base::FilePath profile_path =
      profile_manager->GenerateNextProfileDirectoryPath();
  ProfileAttributesInitParams params;
  params.profile_path = profile_path;
  params.profile_name = u"New Profile 2";
  profile_storage->AddProfile(std::move(params));

  base::RunLoop run_loop;
  app_controller_mac::RunInProfileSafely(
      profile_path, base::BindLambdaForTesting([&](Profile* profile) {
        // This should run with the specific profile we asked for, rather than
        // the last-used profile.
        EXPECT_NE(profile, nullptr);
        EXPECT_NE(profile, profile_.get());
        EXPECT_EQ(profile->GetPath(), profile_path);
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInProfileSafely() returns nullptr if a profle doesn't exist.
TEST_F(AppControllerSafeProfileTest, SpecificProfileDoesNotExist) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  base::RunLoop run_loop;
  app_controller_mac::RunInProfileSafely(
      profile_manager_.profiles_dir().AppendASCII("Non-existent Profile"),
      base::BindLambdaForTesting([&](Profile* profile) {
        EXPECT_EQ(profile, nullptr);
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}