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