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