// 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/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
#import <AppKit/AppKit.h>
#import <string>
#import "base/strings/string_util.h"
#import "base/strings/utf_string_conversions.h"
#import "base/uuid.h"
#import "chrome/app/chrome_command_ids.h"
#import "chrome/browser/bookmarks/bookmark_model_factory.h"
#import "chrome/browser/bookmarks/managed_bookmark_service_factory.h"
#import "chrome/browser/ui/cocoa/test/cocoa_test_helper.h"
#import "chrome/test/base/browser_with_test_window_test.h"
#import "chrome/test/base/testing_profile.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/common/bookmark_metrics.h"
#import "components/bookmarks/test/bookmark_test_helpers.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
using base::ASCIIToUTF16;
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
class BookmarkMenuBridgeTest : public BrowserWithTestWindowTest {
public:
BookmarkMenuBridgeTest() = default;
BookmarkMenuBridgeTest(const BookmarkMenuBridgeTest&) = delete;
BookmarkMenuBridgeTest& operator=(const BookmarkMenuBridgeTest&) = delete;
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
bookmarks::test::WaitForBookmarkModelToLoad(
BookmarkModelFactory::GetForBrowserContext(profile()));
menu_ = [[NSMenu alloc] initWithTitle:@"test"];
bridge_ = std::make_unique<BookmarkMenuBridge>(profile(), menu_);
}
void TearDown() override {
bridge_ = nullptr;
BrowserWithTestWindowTest::TearDown();
}
TestingProfile::TestingFactories GetTestingFactories() override {
return {TestingProfile::TestingFactory{
BookmarkModelFactory::GetInstance(),
BookmarkModelFactory::GetDefaultFactory()},
TestingProfile::TestingFactory{
ManagedBookmarkServiceFactory::GetInstance(),
ManagedBookmarkServiceFactory::GetDefaultFactory()}};
}
void UpdateRootMenu() {
bridge_->UpdateMenu(menu_, nullptr, /*recurse=*/false);
}
// We are a friend of BookmarkMenuBridge (and have access to
// protected methods), but none of the classes generated by TEST_F()
// are. This (and AddNodeToMenu()) are simple wrappers to let
// derived test classes have access to protected methods.
void ClearBookmarkMenu() { bridge_->ClearBookmarkMenu(); }
void InvalidateMenu() { bridge_->InvalidateMenu(); }
bool menu_is_valid() { return bridge_->IsMenuValid(); }
void AddNodeToMenu(BookmarkMenuBridge* bridge,
const BookmarkNode* root,
NSMenu* menu) {
bridge->AddNodeToMenu(root, menu, /*recurse=*/false);
}
NSMenuItem* MenuItemForNode(BookmarkMenuBridge* bridge,
const BookmarkNode* node) {
return bridge->MenuItemForNode(node);
}
NSMenuItem* AddTestMenuItem(NSMenu *menu, NSString *title, SEL selector) {
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
action:nullptr
keyEquivalent:@""];
if (selector)
[item setAction:selector];
[menu addItem:item];
return item;
}
protected:
NSMenu* __strong menu_;
std::unique_ptr<BookmarkMenuBridge> bridge_;
private:
CocoaTestHelper cocoa_test_helper_;
};
TEST_F(BookmarkMenuBridgeTest, TestBookmarkMenuAutoSeparator) {
BookmarkModel* model = bridge_->GetBookmarkModel();
bridge_->BookmarkModelLoaded(false);
UpdateRootMenu();
// The bare menu after loading used to have a separator and an
// "Other Bookmarks" submenu, but we no longer show those items if the
// "Other Bookmarks" submenu would be empty.
EXPECT_EQ(0, [menu_ numberOfItems]);
// Add a bookmark and reload and there should be 2 items:
// a new separator and the new bookmark.
const BookmarkNode* parent = model->bookmark_bar_node();
const char* url = "http://www.zim-bop-a-dee.com/";
model->AddURL(parent, 0, u"Bookmark", GURL(url));
UpdateRootMenu();
EXPECT_EQ(2, [menu_ numberOfItems]);
// Remove the new bookmark and reload and we should have 0 items again
// because the separator should have been removed as well.
model->Remove(parent->children().front().get(),
bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
UpdateRootMenu();
EXPECT_EQ(0, [menu_ numberOfItems]);
}
// Test that ClearBookmarkMenu() removes all bookmark menus.
TEST_F(BookmarkMenuBridgeTest, TestClearBookmarkMenu) {
AddTestMenuItem(menu_, @"hi mom", nil);
AddTestMenuItem(menu_, @"not", @selector(openBookmarkMenuItem:));
NSMenuItem* test_item = AddTestMenuItem(menu_, @"hi mom", nil);
[test_item setSubmenu:[[NSMenu alloc] initWithTitle:@"bar"]];
AddTestMenuItem(menu_, @"not", @selector(openBookmarkMenuItem:));
AddTestMenuItem(menu_, @"zippy", @selector(length));
[menu_ addItem:[NSMenuItem separatorItem]];
ClearBookmarkMenu();
// Make sure all bookmark items are removed, all items with
// submenus removed, and all separator items are gone.
EXPECT_EQ(2, [menu_ numberOfItems]);
for (NSMenuItem* item in [menu_ itemArray]) {
EXPECT_NSNE(@"not", [item title]);
}
}
// Test invalidation
TEST_F(BookmarkMenuBridgeTest, TestInvalidation) {
BookmarkModel* model = bridge_->GetBookmarkModel();
model->AddURL(model->bookmark_bar_node(), 0, u"Google",
GURL("https://google.com"));
bridge_->BookmarkModelLoaded(false);
EXPECT_FALSE(menu_is_valid());
UpdateRootMenu();
EXPECT_TRUE(menu_is_valid());
InvalidateMenu();
EXPECT_FALSE(menu_is_valid());
InvalidateMenu();
EXPECT_FALSE(menu_is_valid());
UpdateRootMenu();
EXPECT_TRUE(menu_is_valid());
UpdateRootMenu();
EXPECT_TRUE(menu_is_valid());
const BookmarkNode* parent = model->bookmark_bar_node();
const char* url = "http://www.zim-bop-a-dee.com/";
model->AddURL(parent, 0, u"Bookmark", GURL(url));
EXPECT_FALSE(menu_is_valid());
UpdateRootMenu();
EXPECT_TRUE(menu_is_valid());
}
// Test that AddNodeToMenu() properly adds bookmark nodes as menus,
// including the recursive case.
TEST_F(BookmarkMenuBridgeTest, TestAddNodeToMenu) {
std::u16string empty;
BookmarkModel* model = bridge_->GetBookmarkModel();
const BookmarkNode* root = model->bookmark_bar_node();
EXPECT_TRUE(model && root);
const char* short_url = "http://foo/";
const char* long_url = "http://super-duper-long-url--."
"that.cannot.possibly.fit.even-in-80-columns"
"or.be.reasonably-displayed-in-a-menu"
"without.looking-ridiculous.com/"; // 140 chars total
// 3 nodes; middle one has a child, last one has a HUGE URL
// Set their titles to be the same as the URLs
const BookmarkNode* node = nullptr;
model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url));
UpdateRootMenu();
int prev_count = [menu_ numberOfItems] - 1; // "extras" added at this point
node = model->AddFolder(root, 1, empty);
model->AddURL(root, 2, ASCIIToUTF16(long_url), GURL(long_url));
// And the submenu fo the middle one
model->AddURL(node, 0, empty, GURL("http://sub"));
UpdateRootMenu();
EXPECT_EQ((NSInteger)(prev_count + 3), [menu_ numberOfItems]);
// Verify the 1st one is there with the right action.
NSMenuItem* item = [menu_ itemWithTitle:@(short_url)];
EXPECT_TRUE(item);
EXPECT_EQ(@selector(openBookmarkMenuItem:), [item action]);
EXPECT_EQ(NO, [item hasSubmenu]);
NSMenuItem* short_item = item;
NSMenuItem* long_item = nil;
// Now confirm we have 1 submenu (the one we added, and not "other")
int subs = 0;
for (item in [menu_ itemArray]) {
if ([item hasSubmenu])
subs++;
}
EXPECT_EQ(1, subs);
for (item in [menu_ itemArray]) {
if ([[item title] hasPrefix:@"http://super-duper"]) {
long_item = item;
break;
}
}
EXPECT_TRUE(long_item);
// Make sure a short title looks fine
NSString* s = [short_item title];
EXPECT_NSEQ(@(short_url), s);
// Long titles are shortened, but only once drawn by AppKit.
s = [long_item title];
EXPECT_NSEQ(@(long_url), s);
// Confirm tooltips and confirm they are not trimmed (like the item
// name might be). Add tolerance for URL fixer-upping;
// e.g. http://foo becomes http://foo/)
EXPECT_GE([[short_item toolTip] length], strlen(short_url) - 3);
EXPECT_GE([[long_item toolTip] length], strlen(long_url) - 3);
// Make sure the favicon is non-nil (should be either the default site
// icon or a favicon, if present).
EXPECT_TRUE([short_item image]);
EXPECT_TRUE([long_item image]);
}
// Makes sure our internal map of BookmarkNode to NSMenuItem works.
TEST_F(BookmarkMenuBridgeTest, TestGetMenuItemForNode) {
std::u16string empty;
BookmarkModel* model = bridge_->GetBookmarkModel();
EXPECT_TRUE(model);
const BookmarkNode* bookmark_bar = model->bookmark_bar_node();
UpdateRootMenu();
EXPECT_EQ(0u, [menu_ numberOfItems]);
const BookmarkNode* folder = model->AddFolder(bookmark_bar, 0, empty);
EXPECT_TRUE(folder);
UpdateRootMenu();
EXPECT_EQ(2u, [menu_ numberOfItems]);
NSMenu* submenu = [[menu_ itemAtIndex:1] submenu];
EXPECT_TRUE(submenu);
EXPECT_TRUE([submenu delegate]);
EXPECT_EQ(0u, [submenu numberOfItems]);
bridge_->UpdateMenu(submenu, folder, /*recurse=*/false);
// Updating the menu clears the delegate to prevent further updates.
EXPECT_FALSE([submenu delegate]);
// Since the folder is currently empty, a single node is added saying (empty).
EXPECT_NSEQ(@"(empty)", [[submenu itemAtIndex:0] title]);
model->AddURL(folder, 0, u"Test Item", GURL("http://test"));
UpdateRootMenu();
// There will be a new submenu each time, Cocoa will update it if needed.
bridge_->UpdateMenu([[menu_ itemAtIndex:1] submenu], folder,
/*recurse=*/false);
EXPECT_TRUE(MenuItemForNode(bridge_.get(), folder->children().front().get()));
model->AddURL(folder, 1, u"Test 2", GURL("http://second-test"));
UpdateRootMenu();
NSMenu* old_menu = [[menu_ itemAtIndex:1] submenu];
EXPECT_TRUE([old_menu delegate]);
// If the menu was never built, ensure UpdateRootMenu() also clears delegates
// from unbuilt submenus, since they will no longer be reachable.
InvalidateMenu();
UpdateRootMenu();
EXPECT_NE(old_menu, [[menu_ itemAtIndex:1] submenu]);
EXPECT_FALSE([old_menu delegate]);
bridge_->UpdateMenu([[menu_ itemAtIndex:1] submenu], folder,
/*recurse=*/false);
EXPECT_TRUE(MenuItemForNode(bridge_.get(), folder->children()[0].get()));
EXPECT_TRUE(MenuItemForNode(bridge_.get(), folder->children()[1].get()));
const BookmarkNode* removed_node = folder->children()[0].get();
EXPECT_EQ(2u, folder->children().size());
model->Remove(folder->children()[0].get(),
bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
EXPECT_EQ(1u, folder->children().size());
EXPECT_FALSE(menu_is_valid());
UpdateRootMenu();
// Initially both will be false, but the submenu corresponding to the folder
// will have a delegate set again, allowing it to be updated on demand.
EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node));
EXPECT_FALSE(MenuItemForNode(bridge_.get(), folder->children()[0].get()));
UpdateRootMenu();
bridge_->UpdateMenu([[menu_ itemAtIndex:1] submenu], folder,
/*recurse=*/false);
EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node));
EXPECT_TRUE(MenuItemForNode(bridge_.get(), folder->children()[0].get()));
const BookmarkNode empty_node(/*id=*/0, base::Uuid::GenerateRandomV4(),
GURL("http://no-where/"));
EXPECT_FALSE(MenuItemForNode(bridge_.get(), &empty_node));
EXPECT_FALSE(MenuItemForNode(bridge_.get(), nullptr));
}
// Test that Loaded() adds both the bookmark bar nodes and the "other" nodes, as
// lazily loadable submenus.
TEST_F(BookmarkMenuBridgeTest, TestAddNodeToOther) {
BookmarkModel* model = bridge_->GetBookmarkModel();
const BookmarkNode* other_root = model->other_node();
EXPECT_TRUE(model && other_root);
const char* short_url = "http://foo/";
model->AddURL(other_root, 0, ASCIIToUTF16(short_url), GURL(short_url));
UpdateRootMenu();
ASSERT_GT([menu_ numberOfItems], 0);
NSMenuItem* other = [menu_ itemAtIndex:([menu_ numberOfItems] - 1)];
EXPECT_TRUE(other);
EXPECT_TRUE([other hasSubmenu]);
// The "other" submenu is loaded lazily.
EXPECT_EQ(0u, [[other submenu] numberOfItems]);
bridge_->UpdateMenu([other submenu], model -> other_node(),
/*recurse=*/false);
ASSERT_GT([[other submenu] numberOfItems], 0);
EXPECT_NSEQ(@"http://foo/", [[[other submenu] itemAtIndex:0] title]);
}
TEST_F(BookmarkMenuBridgeTest, TestFaviconLoading) {
BookmarkModel* model = bridge_->GetBookmarkModel();
const BookmarkNode* root = model->bookmark_bar_node();
EXPECT_TRUE(model && root);
const BookmarkNode* node =
model->AddURL(root, 0, u"Test Item", GURL("http://favicon-test"));
UpdateRootMenu();
NSMenuItem* item = [menu_ itemWithTitle:@"Test Item"];
EXPECT_TRUE([item image]);
[item setImage:nil];
bridge_->BookmarkNodeFaviconChanged(node);
EXPECT_TRUE([item image]);
}
TEST_F(BookmarkMenuBridgeTest, TestChangeTitle) {
BookmarkModel* model = bridge_->GetBookmarkModel();
const BookmarkNode* root = model->bookmark_bar_node();
EXPECT_TRUE(model && root);
const BookmarkNode* node =
model->AddURL(root, 0, u"Test Item", GURL("http://title-test"));
UpdateRootMenu();
NSMenuItem* item = [menu_ itemWithTitle:@"Test Item"];
EXPECT_TRUE([item image]);
model->SetTitle(node, u"New Title",
bookmarks::metrics::BookmarkEditSource::kOther);
item = [menu_ itemWithTitle:@"Test Item"];
EXPECT_FALSE(item);
item = [menu_ itemWithTitle:@"New Title"];
EXPECT_TRUE(item);
}
TEST_F(BookmarkMenuBridgeTest, BuildMenuRecursivelyBeforeProfileDestruction) {
BookmarkModel* model = bridge_->GetBookmarkModel();
const BookmarkNode* root = model->bookmark_bar_node();
EXPECT_TRUE(model && root);
// root
// + Item 1
// + Folder 1
// + Folder 2
// + Item 2
const BookmarkNode* item1 =
model->AddURL(root, 0, u"Item 1", GURL("http://item-1/"));
base::Uuid item1_guid = item1->uuid();
const BookmarkNode* folder1 = model->AddFolder(root, 1, u"Folder 1");
base::Uuid folder1_guid = folder1->uuid();
const BookmarkNode* folder2 = model->AddFolder(folder1, 0, u"Folder 2");
base::Uuid folder2_guid = folder2->uuid();
const BookmarkNode* item2 =
model->AddURL(folder2, 0, u"Item 2", GURL("http://item-2/"));
base::Uuid item2_guid = item2->uuid();
// We didn't show the menu or any submenus, so it shouldn't contain these
// items.
NSMenuItem* item = [menu_ itemWithTitle:@"Item 1"];
EXPECT_FALSE(item);
item = [menu_ itemWithTitle:@"Folder 1"];
EXPECT_FALSE(item);
bridge_->OnProfileWillBeDestroyed();
EXPECT_EQ(nullptr, bridge_->GetProfile());
// OnProfileWillBeDestroyed() should've recursively populated the menu.
item = [menu_ itemWithTitle:@"Item 1"];
EXPECT_TRUE(item);
EXPECT_EQ(item1_guid, bridge_->TagToGUID([item tag]));
item = [menu_ itemWithTitle:@"Folder 1"];
EXPECT_TRUE(item);
EXPECT_EQ(folder1_guid, bridge_->TagToGUID([item tag]));
EXPECT_TRUE([item hasSubmenu]);
item = [[item submenu] itemWithTitle:@"Folder 2"];
EXPECT_TRUE(item);
EXPECT_EQ(folder2_guid, bridge_->TagToGUID([item tag]));
EXPECT_TRUE([item hasSubmenu]);
item = [[item submenu] itemWithTitle:@"Item 2"];
EXPECT_TRUE(item);
EXPECT_EQ(item2_guid, bridge_->TagToGUID([item tag]));
}