1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #import <Cocoa/Cocoa.h> 6 7 #include "base/basictypes.h" 8 #include "base/memory/scoped_nsobject.h" 9 #include "base/utf_string_conversions.h" 10 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h" 11 #include "chrome/browser/ui/cocoa/browser_test_helper.h" 12 #include "chrome/browser/ui/cocoa/browser_window_controller.h" 13 #import "chrome/browser/ui/cocoa/cocoa_test_helper.h" 14 #import "chrome/browser/ui/cocoa/info_bubble_window.h" 15 #include "content/common/notification_service.h" 16 #include "testing/gtest/include/gtest/gtest.h" 17 #import "testing/gtest_mac.h" 18 #include "testing/platform_test.h" 19 20 // Watch for bookmark pulse notifications so we can confirm they were sent. 21 @interface BookmarkPulseObserver : NSObject { 22 int notifications_; 23 } 24 @property (assign, nonatomic) int notifications; 25 @end 26 27 28 @implementation BookmarkPulseObserver 29 30 @synthesize notifications = notifications_; 31 32 - (id)init { 33 if ((self = [super init])) { 34 [[NSNotificationCenter defaultCenter] 35 addObserver:self 36 selector:@selector(pulseBookmarkNotification:) 37 name:bookmark_button::kPulseBookmarkButtonNotification 38 object:nil]; 39 } 40 return self; 41 } 42 43 - (void)pulseBookmarkNotification:(NSNotificationCenter *)notification { 44 notifications_++; 45 } 46 47 - (void)dealloc { 48 [[NSNotificationCenter defaultCenter] removeObserver:self]; 49 [super dealloc]; 50 } 51 52 @end 53 54 55 namespace { 56 57 class BookmarkBubbleControllerTest : public CocoaTest { 58 public: 59 static int edits_; 60 BrowserTestHelper helper_; 61 BookmarkBubbleController* controller_; 62 63 BookmarkBubbleControllerTest() : controller_(nil) { 64 edits_ = 0; 65 } 66 67 virtual void TearDown() { 68 [controller_ close]; 69 CocoaTest::TearDown(); 70 } 71 72 // Returns a controller but ownership not transferred. 73 // Only one of these will be valid at a time. 74 BookmarkBubbleController* ControllerForNode(const BookmarkNode* node) { 75 if (controller_ && !IsWindowClosing()) { 76 [controller_ close]; 77 controller_ = nil; 78 } 79 controller_ = [[BookmarkBubbleController alloc] 80 initWithParentWindow:test_window() 81 model:helper_.profile()->GetBookmarkModel() 82 node:node 83 alreadyBookmarked:YES]; 84 EXPECT_TRUE([controller_ window]); 85 // The window must be gone or we'll fail a unit test with windows left open. 86 [static_cast<InfoBubbleWindow*>([controller_ window]) setDelayOnClose:NO]; 87 [controller_ showWindow:nil]; 88 return controller_; 89 } 90 91 BookmarkModel* GetBookmarkModel() { 92 return helper_.profile()->GetBookmarkModel(); 93 } 94 95 bool IsWindowClosing() { 96 return [static_cast<InfoBubbleWindow*>([controller_ window]) isClosing]; 97 } 98 }; 99 100 // static 101 int BookmarkBubbleControllerTest::edits_; 102 103 // Confirm basics about the bubble window (e.g. that it is inside the 104 // parent window) 105 TEST_F(BookmarkBubbleControllerTest, TestBubbleWindow) { 106 BookmarkModel* model = GetBookmarkModel(); 107 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 108 0, 109 ASCIIToUTF16("Bookie markie title"), 110 GURL("http://www.google.com")); 111 BookmarkBubbleController* controller = ControllerForNode(node); 112 EXPECT_TRUE(controller); 113 NSWindow* window = [controller window]; 114 EXPECT_TRUE(window); 115 EXPECT_TRUE(NSContainsRect([test_window() frame], 116 [window frame])); 117 } 118 119 // Test that we can handle closing the parent window 120 TEST_F(BookmarkBubbleControllerTest, TestClosingParentWindow) { 121 BookmarkModel* model = GetBookmarkModel(); 122 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 123 0, 124 ASCIIToUTF16("Bookie markie title"), 125 GURL("http://www.google.com")); 126 BookmarkBubbleController* controller = ControllerForNode(node); 127 EXPECT_TRUE(controller); 128 NSWindow* window = [controller window]; 129 EXPECT_TRUE(window); 130 base::mac::ScopedNSAutoreleasePool pool; 131 [test_window() performClose:NSApp]; 132 } 133 134 135 // Confirm population of folder list 136 TEST_F(BookmarkBubbleControllerTest, TestFillInFolder) { 137 // Create some folders, including a nested folder 138 BookmarkModel* model = GetBookmarkModel(); 139 EXPECT_TRUE(model); 140 const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); 141 EXPECT_TRUE(bookmarkBarNode); 142 const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0, 143 ASCIIToUTF16("one")); 144 EXPECT_TRUE(node1); 145 const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 1, 146 ASCIIToUTF16("two")); 147 EXPECT_TRUE(node2); 148 const BookmarkNode* node3 = model->AddFolder(bookmarkBarNode, 2, 149 ASCIIToUTF16("three")); 150 EXPECT_TRUE(node3); 151 const BookmarkNode* node4 = model->AddFolder(node2, 0, ASCIIToUTF16("sub")); 152 EXPECT_TRUE(node4); 153 const BookmarkNode* node5 = model->AddURL(node1, 0, ASCIIToUTF16("title1"), 154 GURL("http://www.google.com")); 155 EXPECT_TRUE(node5); 156 const BookmarkNode* node6 = model->AddURL(node3, 0, ASCIIToUTF16("title2"), 157 GURL("http://www.google.com")); 158 EXPECT_TRUE(node6); 159 const BookmarkNode* node7 = model->AddURL( 160 node4, 0, ASCIIToUTF16("title3"), GURL("http://www.google.com/reader")); 161 EXPECT_TRUE(node7); 162 163 BookmarkBubbleController* controller = ControllerForNode(node4); 164 EXPECT_TRUE(controller); 165 166 NSArray* titles = 167 [[[controller folderPopUpButton] itemArray] valueForKey:@"title"]; 168 EXPECT_TRUE([titles containsObject:@"one"]); 169 EXPECT_TRUE([titles containsObject:@"two"]); 170 EXPECT_TRUE([titles containsObject:@"three"]); 171 EXPECT_TRUE([titles containsObject:@"sub"]); 172 EXPECT_FALSE([titles containsObject:@"title1"]); 173 EXPECT_FALSE([titles containsObject:@"title2"]); 174 } 175 176 // Confirm ability to handle folders with blank name. 177 TEST_F(BookmarkBubbleControllerTest, TestFolderWithBlankName) { 178 // Create some folders, including a nested folder 179 BookmarkModel* model = GetBookmarkModel(); 180 EXPECT_TRUE(model); 181 const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); 182 EXPECT_TRUE(bookmarkBarNode); 183 const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0, 184 ASCIIToUTF16("one")); 185 EXPECT_TRUE(node1); 186 const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 1, 187 ASCIIToUTF16("")); 188 EXPECT_TRUE(node2); 189 const BookmarkNode* node3 = model->AddFolder(bookmarkBarNode, 2, 190 ASCIIToUTF16("three")); 191 EXPECT_TRUE(node3); 192 const BookmarkNode* node2_1 = model->AddURL(node2, 0, ASCIIToUTF16("title1"), 193 GURL("http://www.google.com")); 194 EXPECT_TRUE(node2_1); 195 196 BookmarkBubbleController* controller = ControllerForNode(node1); 197 EXPECT_TRUE(controller); 198 199 // One of the items should be blank and its node should be node2. 200 NSArray* items = [[controller folderPopUpButton] itemArray]; 201 EXPECT_GT([items count], 4U); 202 BOOL blankFolderFound = NO; 203 for (NSMenuItem* item in [[controller folderPopUpButton] itemArray]) { 204 if ([[item title] length] == 0 && 205 static_cast<const BookmarkNode*>([[item representedObject] 206 pointerValue]) == node2) { 207 blankFolderFound = YES; 208 break; 209 } 210 } 211 EXPECT_TRUE(blankFolderFound); 212 } 213 214 215 // Click on edit; bubble gets closed. 216 TEST_F(BookmarkBubbleControllerTest, TestEdit) { 217 BookmarkModel* model = GetBookmarkModel(); 218 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 219 0, 220 ASCIIToUTF16("Bookie markie title"), 221 GURL("http://www.google.com")); 222 BookmarkBubbleController* controller = ControllerForNode(node); 223 EXPECT_TRUE(controller); 224 225 EXPECT_EQ(edits_, 0); 226 EXPECT_FALSE(IsWindowClosing()); 227 [controller edit:controller]; 228 EXPECT_EQ(edits_, 1); 229 EXPECT_TRUE(IsWindowClosing()); 230 } 231 232 // CallClose; bubble gets closed. 233 // Also confirm pulse notifications get sent. 234 TEST_F(BookmarkBubbleControllerTest, TestClose) { 235 BookmarkModel* model = GetBookmarkModel(); 236 const BookmarkNode* node = model->AddURL( 237 model->GetBookmarkBarNode(), 0, ASCIIToUTF16("Bookie markie title"), 238 GURL("http://www.google.com")); 239 EXPECT_EQ(edits_, 0); 240 241 scoped_nsobject<BookmarkPulseObserver> observer([[BookmarkPulseObserver alloc] 242 init]); 243 EXPECT_EQ([observer notifications], 0); 244 BookmarkBubbleController* controller = ControllerForNode(node); 245 EXPECT_TRUE(controller); 246 EXPECT_FALSE(IsWindowClosing()); 247 EXPECT_EQ([observer notifications], 1); 248 [controller ok:controller]; 249 EXPECT_EQ(edits_, 0); 250 EXPECT_TRUE(IsWindowClosing()); 251 EXPECT_EQ([observer notifications], 2); 252 } 253 254 // User changes title and parent folder in the UI 255 TEST_F(BookmarkBubbleControllerTest, TestUserEdit) { 256 BookmarkModel* model = GetBookmarkModel(); 257 EXPECT_TRUE(model); 258 const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); 259 EXPECT_TRUE(bookmarkBarNode); 260 const BookmarkNode* node = model->AddURL(bookmarkBarNode, 261 0, 262 ASCIIToUTF16("short-title"), 263 GURL("http://www.google.com")); 264 const BookmarkNode* grandma = model->AddFolder(bookmarkBarNode, 0, 265 ASCIIToUTF16("grandma")); 266 EXPECT_TRUE(grandma); 267 const BookmarkNode* grandpa = model->AddFolder(bookmarkBarNode, 0, 268 ASCIIToUTF16("grandpa")); 269 EXPECT_TRUE(grandpa); 270 271 BookmarkBubbleController* controller = ControllerForNode(node); 272 EXPECT_TRUE(controller); 273 274 // simulate a user edit 275 [controller setTitle:@"oops" parentFolder:grandma]; 276 [controller edit:controller]; 277 278 // Make sure bookmark has changed 279 EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("oops")); 280 EXPECT_EQ(node->parent()->GetTitle(), ASCIIToUTF16("grandma")); 281 } 282 283 // Confirm happiness with parent nodes that have the same name. 284 TEST_F(BookmarkBubbleControllerTest, TestNewParentSameName) { 285 BookmarkModel* model = GetBookmarkModel(); 286 EXPECT_TRUE(model); 287 const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); 288 EXPECT_TRUE(bookmarkBarNode); 289 for (int i=0; i<2; i++) { 290 const BookmarkNode* node = model->AddURL(bookmarkBarNode, 291 0, 292 ASCIIToUTF16("short-title"), 293 GURL("http://www.google.com")); 294 EXPECT_TRUE(node); 295 const BookmarkNode* folder = model->AddFolder(bookmarkBarNode, 0, 296 ASCIIToUTF16("NAME")); 297 EXPECT_TRUE(folder); 298 folder = model->AddFolder(bookmarkBarNode, 0, ASCIIToUTF16("NAME")); 299 EXPECT_TRUE(folder); 300 folder = model->AddFolder(bookmarkBarNode, 0, ASCIIToUTF16("NAME")); 301 EXPECT_TRUE(folder); 302 BookmarkBubbleController* controller = ControllerForNode(node); 303 EXPECT_TRUE(controller); 304 305 // simulate a user edit 306 [controller setParentFolderSelection:bookmarkBarNode->GetChild(i)]; 307 [controller edit:controller]; 308 309 // Make sure bookmark has changed, and that the parent is what we 310 // expect. This proves nobody did searching based on name. 311 EXPECT_EQ(node->parent(), bookmarkBarNode->GetChild(i)); 312 } 313 } 314 315 // Confirm happiness with nodes with the same Name 316 TEST_F(BookmarkBubbleControllerTest, TestDuplicateNodeNames) { 317 BookmarkModel* model = GetBookmarkModel(); 318 const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); 319 EXPECT_TRUE(bookmarkBarNode); 320 const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0, 321 ASCIIToUTF16("NAME")); 322 EXPECT_TRUE(node1); 323 const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 0, 324 ASCIIToUTF16("NAME")); 325 EXPECT_TRUE(node2); 326 BookmarkBubbleController* controller = ControllerForNode(bookmarkBarNode); 327 EXPECT_TRUE(controller); 328 329 NSPopUpButton* button = [controller folderPopUpButton]; 330 [controller setParentFolderSelection:node1]; 331 NSMenuItem* item = [button selectedItem]; 332 id itemObject = [item representedObject]; 333 EXPECT_NSEQ([NSValue valueWithPointer:node1], itemObject); 334 [controller setParentFolderSelection:node2]; 335 item = [button selectedItem]; 336 itemObject = [item representedObject]; 337 EXPECT_NSEQ([NSValue valueWithPointer:node2], itemObject); 338 } 339 340 // Click the "remove" button 341 TEST_F(BookmarkBubbleControllerTest, TestRemove) { 342 BookmarkModel* model = GetBookmarkModel(); 343 GURL gurl("http://www.google.com"); 344 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 345 0, 346 ASCIIToUTF16("Bookie markie title"), 347 gurl); 348 BookmarkBubbleController* controller = ControllerForNode(node); 349 EXPECT_TRUE(controller); 350 EXPECT_TRUE(model->IsBookmarked(gurl)); 351 352 [controller remove:controller]; 353 EXPECT_FALSE(model->IsBookmarked(gurl)); 354 EXPECT_TRUE(IsWindowClosing()); 355 } 356 357 // Confirm picking "choose another folder" caused edit: to be called. 358 TEST_F(BookmarkBubbleControllerTest, PopUpSelectionChanged) { 359 BookmarkModel* model = GetBookmarkModel(); 360 GURL gurl("http://www.google.com"); 361 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 362 0, ASCIIToUTF16("super-title"), 363 gurl); 364 BookmarkBubbleController* controller = ControllerForNode(node); 365 EXPECT_TRUE(controller); 366 367 NSPopUpButton* button = [controller folderPopUpButton]; 368 [button selectItemWithTitle:[[controller class] chooseAnotherFolderString]]; 369 EXPECT_EQ(edits_, 0); 370 [button sendAction:[button action] to:[button target]]; 371 EXPECT_EQ(edits_, 1); 372 } 373 374 // Create a controller that simulates the bookmark just now being created by 375 // the user clicking the star, then sending the "cancel" command to represent 376 // them pressing escape. The bookmark should not be there. 377 TEST_F(BookmarkBubbleControllerTest, EscapeRemovesNewBookmark) { 378 BookmarkModel* model = GetBookmarkModel(); 379 GURL gurl("http://www.google.com"); 380 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 381 0, 382 ASCIIToUTF16("Bookie markie title"), 383 gurl); 384 BookmarkBubbleController* controller = 385 [[BookmarkBubbleController alloc] 386 initWithParentWindow:test_window() 387 model:helper_.profile()->GetBookmarkModel() 388 node:node 389 alreadyBookmarked:NO]; // The last param is the key difference. 390 EXPECT_TRUE([controller window]); 391 // Calls release on controller. 392 [controller cancel:nil]; 393 EXPECT_FALSE(model->IsBookmarked(gurl)); 394 } 395 396 // Create a controller where the bookmark already existed prior to clicking 397 // the star and test that sending a cancel command doesn't change the state 398 // of the bookmark. 399 TEST_F(BookmarkBubbleControllerTest, EscapeDoesntTouchExistingBookmark) { 400 BookmarkModel* model = GetBookmarkModel(); 401 GURL gurl("http://www.google.com"); 402 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 403 0, 404 ASCIIToUTF16("Bookie markie title"), 405 gurl); 406 BookmarkBubbleController* controller = ControllerForNode(node); 407 EXPECT_TRUE(controller); 408 409 [(id)controller cancel:nil]; 410 EXPECT_TRUE(model->IsBookmarked(gurl)); 411 } 412 413 // Confirm indentation of items in pop-up menu 414 TEST_F(BookmarkBubbleControllerTest, TestMenuIndentation) { 415 // Create some folders, including a nested folder 416 BookmarkModel* model = GetBookmarkModel(); 417 EXPECT_TRUE(model); 418 const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); 419 EXPECT_TRUE(bookmarkBarNode); 420 const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0, 421 ASCIIToUTF16("one")); 422 EXPECT_TRUE(node1); 423 const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 1, 424 ASCIIToUTF16("two")); 425 EXPECT_TRUE(node2); 426 const BookmarkNode* node2_1 = model->AddFolder(node2, 0, 427 ASCIIToUTF16("two dot one")); 428 EXPECT_TRUE(node2_1); 429 const BookmarkNode* node3 = model->AddFolder(bookmarkBarNode, 2, 430 ASCIIToUTF16("three")); 431 EXPECT_TRUE(node3); 432 433 BookmarkBubbleController* controller = ControllerForNode(node1); 434 EXPECT_TRUE(controller); 435 436 // Compare the menu item indents against expectations. 437 static const int kExpectedIndent[] = {0, 1, 1, 2, 1, 0}; 438 NSArray* items = [[controller folderPopUpButton] itemArray]; 439 ASSERT_GE([items count], 6U); 440 for(int itemNo = 0; itemNo < 6; itemNo++) { 441 NSMenuItem* item = [items objectAtIndex:itemNo]; 442 EXPECT_EQ(kExpectedIndent[itemNo], [item indentationLevel]) 443 << "Unexpected indent for menu item #" << itemNo; 444 } 445 } 446 447 // Confirm bubble goes away when a new tab is created. 448 TEST_F(BookmarkBubbleControllerTest, BubbleGoesAwayOnNewTab) { 449 450 BookmarkModel* model = GetBookmarkModel(); 451 const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), 452 0, 453 ASCIIToUTF16("Bookie markie title"), 454 GURL("http://www.google.com")); 455 EXPECT_EQ(edits_, 0); 456 457 BookmarkBubbleController* controller = ControllerForNode(node); 458 EXPECT_TRUE(controller); 459 EXPECT_FALSE(IsWindowClosing()); 460 461 // We can't actually create a new tab here, e.g. 462 // helper_.browser()->AddTabWithURL(...); 463 // Many of our browser objects (Browser, Profile, RequestContext) 464 // are "just enough" to run tests without being complete. Instead 465 // we fake the notification that would be triggered by a tab 466 // creation. 467 NotificationService::current()->Notify( 468 NotificationType::TAB_CONTENTS_CONNECTED, 469 Source<TabContentsDelegate>(NULL), 470 Details<TabContents>(NULL)); 471 472 // Confirm bubble going bye-bye. 473 EXPECT_TRUE(IsWindowClosing()); 474 } 475 476 477 } // namespace 478 479 @implementation NSApplication (BookmarkBubbleUnitTest) 480 // Add handler for the editBookmarkNode: action to NSApp for testing purposes. 481 // Normally this would be sent up the responder tree correctly, but since 482 // tests run in the background, key window and main window are never set on 483 // NSApplication. Adding it to NSApplication directly removes the need for 484 // worrying about what the current window with focus is. 485 - (void)editBookmarkNode:(id)sender { 486 EXPECT_TRUE([sender respondsToSelector:@selector(node)]); 487 BookmarkBubbleControllerTest::edits_++; 488 } 489 490 @end 491