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 "chrome/browser/autocomplete/autocomplete_popup_view_mac.h" 6 7#include "base/memory/scoped_ptr.h" 8#include "base/sys_string_conversions.h" 9#include "base/utf_string_conversions.h" 10#include "chrome/browser/autocomplete/autocomplete.h" 11#include "testing/platform_test.h" 12#include "ui/base/text/text_elider.h" 13 14namespace { 15 16const float kLargeWidth = 10000; 17 18class AutocompletePopupViewMacTest : public PlatformTest { 19 public: 20 AutocompletePopupViewMacTest() {} 21 22 virtual void SetUp() { 23 PlatformTest::SetUp(); 24 25 // These are here because there is no autorelease pool for the 26 // constructor. 27 color_ = [NSColor blackColor]; 28 dimColor_ = [NSColor darkGrayColor]; 29 font_ = gfx::Font( 30 base::SysNSStringToUTF16([[NSFont userFontOfSize:12] fontName]), 12); 31 } 32 33 // Returns the length of the run starting at |location| for which 34 // |attributeName| remains the same. 35 static NSUInteger RunLengthForAttribute(NSAttributedString* string, 36 NSUInteger location, 37 NSString* attributeName) { 38 const NSRange fullRange = NSMakeRange(0, [string length]); 39 NSRange range; 40 [string attribute:attributeName 41 atIndex:location longestEffectiveRange:&range inRange:fullRange]; 42 43 // In order to signal when the run doesn't start exactly at 44 // location, return a weirdo length. This causes the incorrect 45 // expectation to manifest at the calling location, which is more 46 // useful than an EXPECT_EQ() would be here. 47 if (range.location != location) { 48 return -1; 49 } 50 51 return range.length; 52 } 53 54 // Return true if the run starting at |location| has |color| for 55 // attribute NSForegroundColorAttributeName. 56 static bool RunHasColor(NSAttributedString* string, 57 NSUInteger location, NSColor* color) { 58 const NSRange fullRange = NSMakeRange(0, [string length]); 59 NSRange range; 60 NSColor* runColor = [string attribute:NSForegroundColorAttributeName 61 atIndex:location 62 longestEffectiveRange:&range inRange:fullRange]; 63 64 // According to one "Ali Ozer", you can compare objects within the 65 // same color space using -isEqual:. Converting color spaces 66 // seems too heavyweight for these tests. 67 // http://lists.apple.com/archives/cocoa-dev/2005/May/msg00186.html 68 return [runColor isEqual:color] ? true : false; 69 } 70 71 // Return true if the run starting at |location| has the font 72 // trait(s) in |mask| font in NSFontAttributeName. 73 static bool RunHasFontTrait(NSAttributedString* string, NSUInteger location, 74 NSFontTraitMask mask) { 75 const NSRange fullRange = NSMakeRange(0, [string length]); 76 NSRange range; 77 NSFont* runFont = [string attribute:NSFontAttributeName 78 atIndex:location 79 longestEffectiveRange:&range inRange:fullRange]; 80 NSFontManager* fontManager = [NSFontManager sharedFontManager]; 81 if (runFont && (mask == ([fontManager traitsOfFont:runFont]&mask))) { 82 return true; 83 } 84 return false; 85 } 86 87 // AutocompleteMatch doesn't really have the right constructor for our 88 // needs. Fake one for us to use. 89 static AutocompleteMatch MakeMatch(const string16 &contents, 90 const string16 &description) { 91 AutocompleteMatch m(NULL, 1, true, AutocompleteMatch::URL_WHAT_YOU_TYPED); 92 m.contents = contents; 93 m.description = description; 94 return m; 95 } 96 97 NSColor* color_; // weak 98 NSColor* dimColor_; // weak 99 gfx::Font font_; 100}; 101 102// Simple inputs with no matches should result in styled output who's 103// text matches the input string, with the passed-in color, and 104// nothing bolded. 105TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringNoMatch) { 106 NSString* const string = @"This is a test"; 107 AutocompleteMatch::ACMatchClassifications classifications; 108 109 NSAttributedString* decorated = 110 AutocompletePopupViewMac::DecorateMatchedString( 111 base::SysNSStringToUTF16(string), classifications, 112 color_, dimColor_, font_); 113 114 // Result has same characters as the input. 115 EXPECT_EQ([decorated length], [string length]); 116 EXPECT_TRUE([[decorated string] isEqualToString:string]); 117 118 // Our passed-in color for the entire string. 119 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 120 NSForegroundColorAttributeName), 121 [string length]); 122 EXPECT_TRUE(RunHasColor(decorated, 0U, color_)); 123 124 // An unbolded font for the entire string. 125 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 126 NSFontAttributeName), [string length]); 127 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 128} 129 130// Identical to DecorateMatchedStringNoMatch, except test that URL 131// style gets a different color than we passed in. 132TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringURLNoMatch) { 133 NSString* const string = @"This is a test"; 134 AutocompleteMatch::ACMatchClassifications classifications; 135 136 classifications.push_back( 137 ACMatchClassification(0, ACMatchClassification::URL)); 138 139 NSAttributedString* decorated = 140 AutocompletePopupViewMac::DecorateMatchedString( 141 base::SysNSStringToUTF16(string), classifications, 142 color_, dimColor_, font_); 143 144 // Result has same characters as the input. 145 EXPECT_EQ([decorated length], [string length]); 146 EXPECT_TRUE([[decorated string] isEqualToString:string]); 147 148 // One color for the entire string, and it's not the one we passed in. 149 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 150 NSForegroundColorAttributeName), 151 [string length]); 152 EXPECT_FALSE(RunHasColor(decorated, 0U, color_)); 153 154 // An unbolded font for the entire string. 155 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 156 NSFontAttributeName), [string length]); 157 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 158} 159 160// Test that DIM works as expected. 161TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringDimNoMatch) { 162 NSString* const string = @"This is a test"; 163 // Dim "is". 164 const NSUInteger runLength1 = 5, runLength2 = 2, runLength3 = 7; 165 // Make sure nobody messed up the inputs. 166 EXPECT_EQ(runLength1 + runLength2 + runLength3, [string length]); 167 168 // Push each run onto classifications. 169 AutocompleteMatch::ACMatchClassifications classifications; 170 classifications.push_back( 171 ACMatchClassification(0, ACMatchClassification::NONE)); 172 classifications.push_back( 173 ACMatchClassification(runLength1, ACMatchClassification::DIM)); 174 classifications.push_back( 175 ACMatchClassification(runLength1 + runLength2, 176 ACMatchClassification::NONE)); 177 178 NSAttributedString* decorated = 179 AutocompletePopupViewMac::DecorateMatchedString( 180 base::SysNSStringToUTF16(string), classifications, 181 color_, dimColor_, font_); 182 183 // Result has same characters as the input. 184 EXPECT_EQ([decorated length], [string length]); 185 EXPECT_TRUE([[decorated string] isEqualToString:string]); 186 187 // Should have three font runs, normal, dim, normal. 188 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 189 NSForegroundColorAttributeName), 190 runLength1); 191 EXPECT_TRUE(RunHasColor(decorated, 0U, color_)); 192 193 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1, 194 NSForegroundColorAttributeName), 195 runLength2); 196 EXPECT_TRUE(RunHasColor(decorated, runLength1, dimColor_)); 197 198 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2, 199 NSForegroundColorAttributeName), 200 runLength3); 201 EXPECT_TRUE(RunHasColor(decorated, runLength1 + runLength2, color_)); 202 203 // An unbolded font for the entire string. 204 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 205 NSFontAttributeName), [string length]); 206 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 207} 208 209// Test that the matched run gets bold-faced, but keeps the same 210// color. 211TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringMatch) { 212 NSString* const string = @"This is a test"; 213 // Match "is". 214 const NSUInteger runLength1 = 5, runLength2 = 2, runLength3 = 7; 215 // Make sure nobody messed up the inputs. 216 EXPECT_EQ(runLength1 + runLength2 + runLength3, [string length]); 217 218 // Push each run onto classifications. 219 AutocompleteMatch::ACMatchClassifications classifications; 220 classifications.push_back( 221 ACMatchClassification(0, ACMatchClassification::NONE)); 222 classifications.push_back( 223 ACMatchClassification(runLength1, ACMatchClassification::MATCH)); 224 classifications.push_back( 225 ACMatchClassification(runLength1 + runLength2, 226 ACMatchClassification::NONE)); 227 228 NSAttributedString* decorated = 229 AutocompletePopupViewMac::DecorateMatchedString( 230 base::SysNSStringToUTF16(string), classifications, 231 color_, dimColor_, font_); 232 233 // Result has same characters as the input. 234 EXPECT_EQ([decorated length], [string length]); 235 EXPECT_TRUE([[decorated string] isEqualToString:string]); 236 237 // Our passed-in color for the entire string. 238 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 239 NSForegroundColorAttributeName), 240 [string length]); 241 EXPECT_TRUE(RunHasColor(decorated, 0U, color_)); 242 243 // Should have three font runs, not bold, bold, then not bold again. 244 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 245 NSFontAttributeName), runLength1); 246 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 247 248 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1, 249 NSFontAttributeName), runLength2); 250 EXPECT_TRUE(RunHasFontTrait(decorated, runLength1, NSBoldFontMask)); 251 252 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2, 253 NSFontAttributeName), runLength3); 254 EXPECT_FALSE(RunHasFontTrait(decorated, runLength1 + runLength2, 255 NSBoldFontMask)); 256} 257 258// Just like DecorateMatchedStringURLMatch, this time with URL style. 259TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringURLMatch) { 260 NSString* const string = @"http://hello.world/"; 261 // Match "hello". 262 const NSUInteger runLength1 = 7, runLength2 = 5, runLength3 = 7; 263 // Make sure nobody messed up the inputs. 264 EXPECT_EQ(runLength1 + runLength2 + runLength3, [string length]); 265 266 // Push each run onto classifications. 267 AutocompleteMatch::ACMatchClassifications classifications; 268 classifications.push_back( 269 ACMatchClassification(0, ACMatchClassification::URL)); 270 const int kURLMatch = ACMatchClassification::URL|ACMatchClassification::MATCH; 271 classifications.push_back(ACMatchClassification(runLength1, kURLMatch)); 272 classifications.push_back( 273 ACMatchClassification(runLength1 + runLength2, 274 ACMatchClassification::URL)); 275 276 NSAttributedString* decorated = 277 AutocompletePopupViewMac::DecorateMatchedString( 278 base::SysNSStringToUTF16(string), classifications, 279 color_, dimColor_, font_); 280 281 // Result has same characters as the input. 282 EXPECT_EQ([decorated length], [string length]); 283 EXPECT_TRUE([[decorated string] isEqualToString:string]); 284 285 // One color for the entire string, and it's not the one we passed in. 286 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 287 NSForegroundColorAttributeName), 288 [string length]); 289 EXPECT_FALSE(RunHasColor(decorated, 0U, color_)); 290 291 // Should have three font runs, not bold, bold, then not bold again. 292 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 293 NSFontAttributeName), runLength1); 294 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 295 296 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1, 297 NSFontAttributeName), runLength2); 298 EXPECT_TRUE(RunHasFontTrait(decorated, runLength1, NSBoldFontMask)); 299 300 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2, 301 NSFontAttributeName), runLength3); 302 EXPECT_FALSE(RunHasFontTrait(decorated, runLength1 + runLength2, 303 NSBoldFontMask)); 304} 305 306// Check that matches with both contents and description come back 307// with contents at the beginning, description at the end, and 308// something separating them. Not being specific about the separator 309// on purpose, in case it changes. 310TEST_F(AutocompletePopupViewMacTest, MatchText) { 311 NSString* const contents = @"contents"; 312 NSString* const description = @"description"; 313 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents), 314 base::SysNSStringToUTF16(description)); 315 316 NSAttributedString* decorated = 317 AutocompletePopupViewMac::MatchText(m, font_, kLargeWidth); 318 319 // Result contains the characters of the input in the right places. 320 EXPECT_GT([decorated length], [contents length] + [description length]); 321 EXPECT_TRUE([[decorated string] hasPrefix:contents]); 322 EXPECT_TRUE([[decorated string] hasSuffix:description]); 323 324 // Check that the description is a different color from the 325 // contents. 326 const NSUInteger descriptionLocation = 327 [decorated length] - [description length]; 328 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 329 NSForegroundColorAttributeName), 330 descriptionLocation); 331 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation, 332 NSForegroundColorAttributeName), 333 [description length]); 334 335 // Same font all the way through, nothing bold. 336 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 337 NSFontAttributeName), [decorated length]); 338 EXPECT_FALSE(RunHasFontTrait(decorated, 0, NSBoldFontMask)); 339} 340 341// Check that MatchText() styles content matches as expected. 342TEST_F(AutocompletePopupViewMacTest, MatchTextContentsMatch) { 343 NSString* const contents = @"This is a test"; 344 // Match "is". 345 const NSUInteger runLength1 = 5, runLength2 = 2, runLength3 = 7; 346 // Make sure nobody messed up the inputs. 347 EXPECT_EQ(runLength1 + runLength2 + runLength3, [contents length]); 348 349 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents), 350 string16()); 351 352 // Push each run onto contents classifications. 353 m.contents_class.push_back( 354 ACMatchClassification(0, ACMatchClassification::NONE)); 355 m.contents_class.push_back( 356 ACMatchClassification(runLength1, ACMatchClassification::MATCH)); 357 m.contents_class.push_back( 358 ACMatchClassification(runLength1 + runLength2, 359 ACMatchClassification::NONE)); 360 361 NSAttributedString* decorated = 362 AutocompletePopupViewMac::MatchText(m, font_, kLargeWidth); 363 364 // Result has same characters as the input. 365 EXPECT_EQ([decorated length], [contents length]); 366 EXPECT_TRUE([[decorated string] isEqualToString:contents]); 367 368 // Result is all one color. 369 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 370 NSForegroundColorAttributeName), 371 [contents length]); 372 373 // Should have three font runs, not bold, bold, then not bold again. 374 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 375 NSFontAttributeName), runLength1); 376 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 377 378 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1, 379 NSFontAttributeName), runLength2); 380 EXPECT_TRUE(RunHasFontTrait(decorated, runLength1, NSBoldFontMask)); 381 382 EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2, 383 NSFontAttributeName), runLength3); 384 EXPECT_FALSE(RunHasFontTrait(decorated, runLength1 + runLength2, 385 NSBoldFontMask)); 386} 387 388// Check that MatchText() styles description matches as expected. 389TEST_F(AutocompletePopupViewMacTest, MatchTextDescriptionMatch) { 390 NSString* const contents = @"This is a test"; 391 NSString* const description = @"That was a test"; 392 // Match "That was". 393 const NSUInteger runLength1 = 8, runLength2 = 7; 394 // Make sure nobody messed up the inputs. 395 EXPECT_EQ(runLength1 + runLength2, [description length]); 396 397 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents), 398 base::SysNSStringToUTF16(description)); 399 400 // Push each run onto contents classifications. 401 m.description_class.push_back( 402 ACMatchClassification(0, ACMatchClassification::MATCH)); 403 m.description_class.push_back( 404 ACMatchClassification(runLength1, ACMatchClassification::NONE)); 405 406 NSAttributedString* decorated = 407 AutocompletePopupViewMac::MatchText(m, font_, kLargeWidth); 408 409 // Result contains the characters of the input. 410 EXPECT_GT([decorated length], [contents length] + [description length]); 411 EXPECT_TRUE([[decorated string] hasPrefix:contents]); 412 EXPECT_TRUE([[decorated string] hasSuffix:description]); 413 414 // Check that the description is a different color from the 415 // contents. 416 const NSUInteger descriptionLocation = 417 [decorated length] - [description length]; 418 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 419 NSForegroundColorAttributeName), 420 descriptionLocation); 421 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation, 422 NSForegroundColorAttributeName), 423 [description length]); 424 425 // Should have three font runs, not bold, bold, then not bold again. 426 // The first run is the contents and the separator, the second run 427 // is the first run of the description. 428 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, 429 NSFontAttributeName), descriptionLocation); 430 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask)); 431 432 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation, 433 NSFontAttributeName), runLength1); 434 EXPECT_TRUE(RunHasFontTrait(decorated, descriptionLocation, NSBoldFontMask)); 435 436 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation + runLength1, 437 NSFontAttributeName), runLength2); 438 EXPECT_FALSE(RunHasFontTrait(decorated, descriptionLocation + runLength1, 439 NSBoldFontMask)); 440} 441 442TEST_F(AutocompletePopupViewMacTest, ElideString) { 443 NSString* const contents = @"This is a test with long contents"; 444 const string16 contents16(base::SysNSStringToUTF16(contents)); 445 446 const float kWide = 1000.0; 447 const float kNarrow = 20.0; 448 449 NSDictionary* attributes = 450 [NSDictionary dictionaryWithObject:font_.GetNativeFont() 451 forKey:NSFontAttributeName]; 452 scoped_nsobject<NSMutableAttributedString> as( 453 [[NSMutableAttributedString alloc] initWithString:contents 454 attributes:attributes]); 455 456 // Nothing happens if the space is really wide. 457 NSMutableAttributedString* ret = 458 AutocompletePopupViewMac::ElideString(as, contents16, font_, kWide); 459 EXPECT_TRUE(ret == as); 460 EXPECT_TRUE([[as string] isEqualToString:contents]); 461 462 // When elided, result is the same as ElideText(). 463 ret = AutocompletePopupViewMac::ElideString(as, contents16, font_, kNarrow); 464 string16 elided = ui::ElideText(contents16, font_, kNarrow, false); 465 EXPECT_TRUE(ret == as); 466 EXPECT_FALSE([[as string] isEqualToString:contents]); 467 EXPECT_TRUE([[as string] isEqualToString:base::SysUTF16ToNSString(elided)]); 468 469 // When elided, result is the same as ElideText(). 470 ret = AutocompletePopupViewMac::ElideString(as, contents16, font_, 0.0); 471 elided = ui::ElideText(contents16, font_, 0.0, false); 472 EXPECT_TRUE(ret == as); 473 EXPECT_FALSE([[as string] isEqualToString:contents]); 474 EXPECT_TRUE([[as string] isEqualToString:base::SysUTF16ToNSString(elided)]); 475} 476 477TEST_F(AutocompletePopupViewMacTest, MatchTextElide) { 478 NSString* const contents = @"This is a test with long contents"; 479 NSString* const description = @"That was a test"; 480 // Match "long". 481 const NSUInteger runLength1 = 20, runLength2 = 4, runLength3 = 9; 482 // Make sure nobody messed up the inputs. 483 EXPECT_EQ(runLength1 + runLength2 + runLength3, [contents length]); 484 485 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents), 486 base::SysNSStringToUTF16(description)); 487 488 // Push each run onto contents classifications. 489 m.contents_class.push_back( 490 ACMatchClassification(0, ACMatchClassification::NONE)); 491 m.contents_class.push_back( 492 ACMatchClassification(runLength1, ACMatchClassification::MATCH)); 493 m.contents_class.push_back( 494 ACMatchClassification(runLength1 + runLength2, 495 ACMatchClassification::URL)); 496 497 // Figure out the width of the contents. 498 NSDictionary* attributes = 499 [NSDictionary dictionaryWithObject:font_.GetNativeFont() 500 forKey:NSFontAttributeName]; 501 const float contentsWidth = [contents sizeWithAttributes:attributes].width; 502 503 // After accounting for the width of the image, this will force us 504 // to elide the contents. 505 float cellWidth = ceil(contentsWidth / 0.7); 506 507 NSAttributedString* decorated = 508 AutocompletePopupViewMac::MatchText(m, font_, cellWidth); 509 510 // Results contain a prefix of the contents and all of description. 511 NSString* commonPrefix = 512 [[decorated string] commonPrefixWithString:contents options:0]; 513 EXPECT_GT([commonPrefix length], 0U); 514 EXPECT_LT([commonPrefix length], [contents length]); 515 EXPECT_TRUE([[decorated string] hasSuffix:description]); 516 517 // At one point the code had a bug where elided text was being 518 // marked up using pre-elided offsets, resulting in out-of-range 519 // values being passed to NSAttributedString. Push the ellipsis 520 // through part of each run to verify that we don't continue to see 521 // such things. 522 while([commonPrefix length] > runLength1 - 3) { 523 EXPECT_GT(cellWidth, 0.0); 524 cellWidth -= 1.0; 525 decorated = AutocompletePopupViewMac::MatchText(m, font_, cellWidth); 526 commonPrefix = 527 [[decorated string] commonPrefixWithString:contents options:0]; 528 ASSERT_GT([commonPrefix length], 0U); 529 } 530} 531 532// TODO(shess): Test that 533// AutocompletePopupViewMac::UpdatePopupAppearance() creates/destroys 534// the popup according to result contents. Test that the matrix gets 535// the right number of results. Test the contents of the cells for 536// the right strings. Icons? Maybe, that seems harder to test. 537// Styling seems almost impossible. 538 539// TODO(shess): Test that AutocompletePopupViewMac::PaintUpdatesNow() 540// updates the matrix selection. 541 542// TODO(shess): Test that AutocompletePopupViewMac::AcceptInput() 543// updates the model's selection from the matrix before returning. 544// Could possibly test that via -select:. 545 546// TODO(shess): Test that AutocompleteButtonCell returns the right 547// background colors for on, highlighted, and neither. 548 549// TODO(shess): Test that AutocompleteMatrixTarget can be initialized 550// and then sends -select: to the view. 551 552} // namespace 553