blob: f3d5c71fcfed224fab91fa5aa2a4d4332991b651 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/cocoa/tab_group_menu_bridge.h"
#import <Cocoa/Cocoa.h>
#include <memory>
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/uuid.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/saved_tab_groups/test_support/fake_tab_group_sync_service.h"
#include "components/tab_groups/tab_group_color.h"
#include "content/public/browser/web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
class TabGroupMenuBridgeTest : public BrowserWithTestWindowTest {
public:
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
service_ = std::make_unique<tab_groups::FakeTabGroupSyncService>();
// Create a dummy main menu.
main_menu_ = [[NSMenu alloc] init];
NSMenuItem* app_menu_item = [[NSMenuItem alloc] init];
[main_menu_ addItem:app_menu_item];
[NSApp setMainMenu:main_menu_];
// Create the "Saved Tab Groups" menu that the bridge will use.
tab_groups_menu_root_ = [[NSMenuItem alloc] initWithTitle:@"Tab Groups"
action:nil
keyEquivalent:@""];
tab_groups_menu_root_.tag = IDC_SAVED_TAB_GROUPS_MENU;
tab_groups_menu_ = [[NSMenu alloc] initWithTitle:@"Tab Groups"];
tab_groups_menu_root_.submenu = tab_groups_menu_;
[main_menu_ addItem:tab_groups_menu_root_];
// Add the static "New Tab Group" item.
NSMenuItem* new_group_item =
[[NSMenuItem alloc] initWithTitle:@"New Tab Group"
action:nil
keyEquivalent:@""];
new_group_item.tag = IDC_CREATE_NEW_TAB_GROUP;
[tab_groups_menu_ addItem:new_group_item];
}
void TearDown() override {
[NSApp setMainMenu:nil];
BrowserWithTestWindowTest::TearDown();
}
protected:
NSMenu* menu() { return tab_groups_menu_; }
tab_groups::TabGroupSyncService* service() { return service_.get(); }
// Helper to add a group to the service.
void AddGroup(const std::u16string& title,
const tab_groups::TabGroupColorId& color,
const std::vector<GURL>& urls) {
tab_groups::SavedTabGroup group(title, color, {}, std::nullopt);
for (const auto& url : urls) {
tab_groups::SavedTabGroupTab tab(url, u"Tab Title", group.saved_guid(),
/*position=*/std::nullopt);
group.AddTabLocally(std::move(tab));
}
service()->AddGroup(std::move(group));
}
void ExpectGroupTitlesInMenu(const std::vector<std::string>& titles) {
std::vector<std::string> actual_titles;
bool found_separator = false;
for (NSMenuItem* item in [menu() itemArray]) {
if (item.tag == IDC_CREATE_NEW_TAB_GROUP) {
continue;
}
if ([item isSeparatorItem]) {
found_separator = true;
continue;
}
if (item.hasSubmenu) {
actual_titles.push_back(base::SysNSStringToUTF8(item.title));
}
}
EXPECT_EQ(!titles.empty(), found_separator);
std::sort(actual_titles.begin(), actual_titles.end());
std::vector<std::string> sorted_titles = titles;
std::sort(sorted_titles.begin(), sorted_titles.end());
ASSERT_EQ(sorted_titles.size(), actual_titles.size());
for (size_t i = 0; i < sorted_titles.size(); ++i) {
EXPECT_EQ(sorted_titles[i], actual_titles[i]);
}
}
std::unique_ptr<tab_groups::TabGroupSyncService> service_;
NSMenu* __strong main_menu_;
NSMenuItem* __strong tab_groups_menu_root_;
NSMenu* __strong tab_groups_menu_;
};
TEST_F(TabGroupMenuBridgeTest, CreatesBlankMenu) {
TabGroupMenuBridge bridge(profile(), service());
bridge.BuildMenu();
// Only the static "New Tab Group" item should be present.
EXPECT_EQ(1, menu().numberOfItems);
ExpectGroupTitlesInMenu({});
}
TEST_F(TabGroupMenuBridgeTest, TracksGroupUpdates) {
TabGroupMenuBridge bridge(profile(), service());
bridge.BuildMenu();
AddGroup(u"Group 1", tab_groups::TabGroupColorId::kGrey,
{GURL("https://a.com")});
ExpectGroupTitlesInMenu({"Group 1"});
AddGroup(u"Group 2", tab_groups::TabGroupColorId::kBlue,
{GURL("https://b.com")});
ExpectGroupTitlesInMenu({"Group 1", "Group 2"});
const auto& groups_before_remove = service()->GetAllGroups();
ASSERT_EQ(2u, groups_before_remove.size());
base::Uuid group1_uuid = groups_before_remove[0].saved_guid();
if (groups_before_remove[0].title() != u"Group 1") {
group1_uuid = groups_before_remove[1].saved_guid();
}
service()->RemoveGroup(group1_uuid);
ExpectGroupTitlesInMenu({"Group 2"});
}
TEST_F(TabGroupMenuBridgeTest, SubmenuHasCorrectItems) {
TabGroupMenuBridge bridge(profile(), service());
AddGroup(u"Group 1", tab_groups::TabGroupColorId::kGrey,
{GURL("https://a.com"), GURL("https://b.com")});
bridge.BuildMenu();
ExpectGroupTitlesInMenu({"Group 1"});
NSMenuItem* group_item = [menu() itemWithTitle:@"Group 1"];
ASSERT_TRUE(group_item);
ASSERT_TRUE(group_item.hasSubmenu);
NSMenu* submenu = group_item.submenu;
// Expected items:
// 0: Open in Browser
// 1: Open/Move to New Window
// 2: Pin/Unpin
// 3: Delete/Leave
// 4: --- separator ---
// 5: Tab 1
// 6: Tab 2
EXPECT_EQ(7, submenu.numberOfItems);
EXPECT_EQ(base::SysNSStringToUTF16([[submenu itemAtIndex:0] title]),
l10n_util::GetStringUTF16(IDS_OPEN_GROUP_IN_BROWSER_MENU));
EXPECT_EQ(base::SysNSStringToUTF16([[submenu itemAtIndex:1] title]),
l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_OPEN_GROUP_IN_NEW_WINDOW));
EXPECT_EQ(base::SysNSStringToUTF16([[submenu itemAtIndex:2] title]),
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_PIN_GROUP));
EXPECT_EQ(
base::SysNSStringToUTF16([[submenu itemAtIndex:3] title]),
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP));
EXPECT_TRUE([[submenu itemAtIndex:4] isSeparatorItem]);
EXPECT_EQ(base::SysNSStringToUTF8([[submenu itemAtIndex:5] title]),
"Tab Title");
EXPECT_EQ(base::SysNSStringToUTF8([[submenu itemAtIndex:6] title]),
"Tab Title");
}
TEST_F(TabGroupMenuBridgeTest, ClickingTabOpensUrl) {
TabGroupMenuBridge bridge(profile(), service());
bridge.SetActiveBrowser(browser());
AddGroup(u"Group 1", tab_groups::TabGroupColorId::kGrey,
{GURL("https://a.com")});
NSMenuItem* group_item = [menu() itemWithTitle:@"Group 1"];
ASSERT_TRUE(group_item);
NSMenu* submenu = group_item.submenu;
NSMenuItem* tab_item = nil;
for (NSMenuItem* item in [submenu itemArray]) {
if ([item.title isEqualToString:@"Tab Title"]) {
tab_item = item;
break;
}
}
ASSERT_TRUE(tab_item);
EXPECT_EQ(0, browser()->tab_strip_model()->count());
[submenu performActionForItemAtIndex:[submenu indexOfItem:tab_item]];
EXPECT_EQ(1, browser()->tab_strip_model()->count());
content::WebContents* new_tab =
browser()->tab_strip_model()->GetWebContentsAt(0);
EXPECT_EQ(GURL("https://a.com"), new_tab->GetVisibleURL());
}