| // 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_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); | 
 |  | 
 |   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_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(); | 
 | } |