1 // Copyright (c) 2012 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.
5 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
7 #include "base/memory/scoped_ptr.h"
8 #include "base/strings/sys_string_conversions.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/ui/cocoa/cocoa_profile_test.h"
11 #import "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h"
12 #include "chrome/test/base/testing_profile.h"
13 #include "ui/gfx/rect.h"
14 #include "ui/gfx/text_elider.h"
18 const float kLargeWidth = 10000;
20 // Returns the length of the run starting at |location| for which
21 // |attributeName| remains the same.
22 NSUInteger RunLengthForAttribute(NSAttributedString* string,
24 NSString* attributeName) {
25 const NSRange full_range = NSMakeRange(0, [string length]);
27 [string attribute:attributeName
28 atIndex:location longestEffectiveRange:&range inRange:full_range];
30 // In order to signal when the run doesn't start exactly at location, return
31 // a weirdo length. This causes the incorrect expectation to manifest at the
32 // calling location, which is more useful than an EXPECT_EQ() would be here.
33 if (range.location != location) {
40 // Return true if the run starting at |location| has |color| for attribute
41 // NSForegroundColorAttributeName.
42 bool RunHasColor(NSAttributedString* string,
45 const NSRange full_range = NSMakeRange(0, [string length]);
47 NSColor* run_color = [string attribute:NSForegroundColorAttributeName
49 longestEffectiveRange:&range
52 // According to one "Ali Ozer", you can compare objects within the same color
53 // space using -isEqual:. Converting color spaces seems too heavyweight for
55 // http://lists.apple.com/archives/cocoa-dev/2005/May/msg00186.html
56 return [run_color isEqual:color] ? true : false;
59 // Return true if the run starting at |location| has the font trait(s) in |mask|
60 // font in NSFontAttributeName.
61 bool RunHasFontTrait(NSAttributedString* string,
63 NSFontTraitMask mask) {
64 const NSRange full_range = NSMakeRange(0, [string length]);
66 NSFont* run_font = [string attribute:NSFontAttributeName
68 longestEffectiveRange:&range
70 NSFontManager* fontManager = [NSFontManager sharedFontManager];
71 if (run_font && (mask == ([fontManager traitsOfFont:run_font] & mask))) {
77 // AutocompleteMatch doesn't really have the right constructor for our
78 // needs. Fake one for us to use.
79 AutocompleteMatch MakeMatch(const string16& contents,
80 const string16& description) {
81 AutocompleteMatch m(NULL, 1, true, AutocompleteMatchType::URL_WHAT_YOU_TYPED);
82 m.contents = contents;
83 m.description = description;
87 class MockOmniboxPopupViewMac : public OmniboxPopupViewMac {
89 MockOmniboxPopupViewMac(OmniboxView* omnibox_view,
90 OmniboxEditModel* edit_model,
92 : OmniboxPopupViewMac(omnibox_view, edit_model, field) {
95 void SetResultCount(size_t count) {
97 for (size_t i = 0; i < count; ++i)
98 matches.push_back(AutocompleteMatch());
100 result_.AppendMatches(matches);
104 virtual const AutocompleteResult& GetResult() const OVERRIDE {
109 AutocompleteResult result_;
112 class OmniboxPopupViewMacTest : public CocoaProfileTest {
114 OmniboxPopupViewMacTest() {
115 color_ = [NSColor blackColor];
116 dim_color_ = [NSColor darkGrayColor];
118 base::SysNSStringToUTF8([[NSFont userFontOfSize:12] fontName]), 12);
122 NSColor* color_; // weak
123 NSColor* dim_color_; // weak
127 DISALLOW_COPY_AND_ASSIGN(OmniboxPopupViewMacTest);
130 // Simple inputs with no matches should result in styled output who's text
131 // matches the input string, with the passed-in color, and nothing bolded.
132 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringNoMatch) {
133 NSString* const string = @"This is a test";
134 AutocompleteMatch::ACMatchClassifications classifications;
136 NSAttributedString* decorated =
137 OmniboxPopupViewMac::DecorateMatchedString(
138 base::SysNSStringToUTF16(string), classifications,
139 color_, dim_color_, font_);
141 // Result has same characters as the input.
142 EXPECT_EQ([decorated length], [string length]);
143 EXPECT_TRUE([[decorated string] isEqualToString:string]);
145 // Our passed-in color for the entire string.
146 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
147 NSForegroundColorAttributeName),
149 EXPECT_TRUE(RunHasColor(decorated, 0U, color_));
151 // An unbolded font for the entire string.
152 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, NSFontAttributeName),
154 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
157 // Identical to DecorateMatchedStringNoMatch, except test that URL style gets a
158 // different color than we passed in.
159 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringURLNoMatch) {
160 NSString* const string = @"This is a test";
161 AutocompleteMatch::ACMatchClassifications classifications;
163 classifications.push_back(
164 ACMatchClassification(0, ACMatchClassification::URL));
166 NSAttributedString* decorated =
167 OmniboxPopupViewMac::DecorateMatchedString(
168 base::SysNSStringToUTF16(string), classifications,
169 color_, dim_color_, font_);
171 // Result has same characters as the input.
172 EXPECT_EQ([decorated length], [string length]);
173 EXPECT_TRUE([[decorated string] isEqualToString:string]);
175 // One color for the entire string, and it's not the one we passed in.
176 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
177 NSForegroundColorAttributeName),
179 EXPECT_FALSE(RunHasColor(decorated, 0U, color_));
181 // An unbolded font for the entire string.
182 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
183 NSFontAttributeName), [string length]);
184 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
187 // Test that DIM works as expected.
188 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringDimNoMatch) {
189 NSString* const string = @"This is a test";
191 const NSUInteger run_length_1 = 5, run_length_2 = 2, run_length_3 = 7;
192 // Make sure nobody messed up the inputs.
193 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [string length]);
195 // Push each run onto classifications.
196 AutocompleteMatch::ACMatchClassifications classifications;
197 classifications.push_back(
198 ACMatchClassification(0, ACMatchClassification::NONE));
199 classifications.push_back(
200 ACMatchClassification(run_length_1, ACMatchClassification::DIM));
201 classifications.push_back(
202 ACMatchClassification(run_length_1 + run_length_2,
203 ACMatchClassification::NONE));
205 NSAttributedString* decorated =
206 OmniboxPopupViewMac::DecorateMatchedString(
207 base::SysNSStringToUTF16(string), classifications,
208 color_, dim_color_, font_);
210 // Result has same characters as the input.
211 EXPECT_EQ([decorated length], [string length]);
212 EXPECT_TRUE([[decorated string] isEqualToString:string]);
214 // Should have three font runs, normal, dim, normal.
215 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
216 NSForegroundColorAttributeName),
218 EXPECT_TRUE(RunHasColor(decorated, 0U, color_));
220 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
221 NSForegroundColorAttributeName),
223 EXPECT_TRUE(RunHasColor(decorated, run_length_1, dim_color_));
225 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
226 NSForegroundColorAttributeName),
228 EXPECT_TRUE(RunHasColor(decorated, run_length_1 + run_length_2, color_));
230 // An unbolded font for the entire string.
231 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
232 NSFontAttributeName), [string length]);
233 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
236 // Test that the matched run gets bold-faced, but keeps the same color.
237 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringMatch) {
238 NSString* const string = @"This is a test";
240 const NSUInteger run_length_1 = 5, run_length_2 = 2, run_length_3 = 7;
241 // Make sure nobody messed up the inputs.
242 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [string length]);
244 // Push each run onto classifications.
245 AutocompleteMatch::ACMatchClassifications classifications;
246 classifications.push_back(
247 ACMatchClassification(0, ACMatchClassification::NONE));
248 classifications.push_back(
249 ACMatchClassification(run_length_1, ACMatchClassification::MATCH));
250 classifications.push_back(
251 ACMatchClassification(run_length_1 + run_length_2,
252 ACMatchClassification::NONE));
254 NSAttributedString* decorated =
255 OmniboxPopupViewMac::DecorateMatchedString(
256 base::SysNSStringToUTF16(string), classifications,
257 color_, dim_color_, font_);
259 // Result has same characters as the input.
260 EXPECT_EQ([decorated length], [string length]);
261 EXPECT_TRUE([[decorated string] isEqualToString:string]);
263 // Our passed-in color for the entire string.
264 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
265 NSForegroundColorAttributeName),
267 EXPECT_TRUE(RunHasColor(decorated, 0U, color_));
269 // Should have three font runs, not bold, bold, then not bold again.
270 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
271 NSFontAttributeName), run_length_1);
272 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
274 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
275 NSFontAttributeName), run_length_2);
276 EXPECT_TRUE(RunHasFontTrait(decorated, run_length_1, NSBoldFontMask));
278 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
279 NSFontAttributeName), run_length_3);
280 EXPECT_FALSE(RunHasFontTrait(decorated, run_length_1 + run_length_2,
284 // Just like DecorateMatchedStringURLMatch, this time with URL style.
285 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringURLMatch) {
286 NSString* const string = @"http://hello.world/";
288 const NSUInteger run_length_1 = 7, run_length_2 = 5, run_length_3 = 7;
289 // Make sure nobody messed up the inputs.
290 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [string length]);
292 // Push each run onto classifications.
293 AutocompleteMatch::ACMatchClassifications classifications;
294 classifications.push_back(
295 ACMatchClassification(0, ACMatchClassification::URL));
296 const int kURLMatch = ACMatchClassification::URL|ACMatchClassification::MATCH;
297 classifications.push_back(ACMatchClassification(run_length_1, kURLMatch));
298 classifications.push_back(
299 ACMatchClassification(run_length_1 + run_length_2,
300 ACMatchClassification::URL));
302 NSAttributedString* decorated =
303 OmniboxPopupViewMac::DecorateMatchedString(
304 base::SysNSStringToUTF16(string), classifications,
305 color_, dim_color_, font_);
307 // Result has same characters as the input.
308 EXPECT_EQ([decorated length], [string length]);
309 EXPECT_TRUE([[decorated string] isEqualToString:string]);
311 // One color for the entire string, and it's not the one we passed in.
312 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
313 NSForegroundColorAttributeName),
315 EXPECT_FALSE(RunHasColor(decorated, 0U, color_));
317 // Should have three font runs, not bold, bold, then not bold again.
318 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
319 NSFontAttributeName), run_length_1);
320 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
322 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
323 NSFontAttributeName), run_length_2);
324 EXPECT_TRUE(RunHasFontTrait(decorated, run_length_1, NSBoldFontMask));
326 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
327 NSFontAttributeName), run_length_3);
328 EXPECT_FALSE(RunHasFontTrait(decorated, run_length_1 + run_length_2,
332 // Check that matches with both contents and description come back
333 // with contents at the beginning, description at the end, and
334 // something separating them. Not being specific about the separator
335 // on purpose, in case it changes.
336 TEST_F(OmniboxPopupViewMacTest, MatchText) {
337 NSString* const contents = @"contents";
338 NSString* const description = @"description";
339 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
340 base::SysNSStringToUTF16(description));
342 NSAttributedString* decorated =
343 OmniboxPopupViewMac::MatchText(m, font_, kLargeWidth);
345 // Result contains the characters of the input in the right places.
346 EXPECT_GT([decorated length], [contents length] + [description length]);
347 EXPECT_TRUE([[decorated string] hasPrefix:contents]);
348 EXPECT_TRUE([[decorated string] hasSuffix:description]);
350 // Check that the description is a different color from the
352 const NSUInteger descriptionLocation =
353 [decorated length] - [description length];
354 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
355 NSForegroundColorAttributeName),
356 descriptionLocation);
357 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
358 NSForegroundColorAttributeName),
359 [description length]);
361 // Same font all the way through, nothing bold.
362 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
363 NSFontAttributeName), [decorated length]);
364 EXPECT_FALSE(RunHasFontTrait(decorated, 0, NSBoldFontMask));
367 // Check that MatchText() styles content matches as expected.
368 TEST_F(OmniboxPopupViewMacTest, MatchTextContentsMatch) {
369 NSString* const contents = @"This is a test";
371 const NSUInteger run_length_1 = 5, run_length_2 = 2, run_length_3 = 7;
372 // Make sure nobody messed up the inputs.
373 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [contents length]);
375 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
378 // Push each run onto contents classifications.
379 m.contents_class.push_back(
380 ACMatchClassification(0, ACMatchClassification::NONE));
381 m.contents_class.push_back(
382 ACMatchClassification(run_length_1, ACMatchClassification::MATCH));
383 m.contents_class.push_back(
384 ACMatchClassification(run_length_1 + run_length_2,
385 ACMatchClassification::NONE));
387 NSAttributedString* decorated =
388 OmniboxPopupViewMac::MatchText(m, font_, kLargeWidth);
390 // Result has same characters as the input.
391 EXPECT_EQ([decorated length], [contents length]);
392 EXPECT_TRUE([[decorated string] isEqualToString:contents]);
394 // Result is all one color.
395 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
396 NSForegroundColorAttributeName),
399 // Should have three font runs, not bold, bold, then not bold again.
400 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
401 NSFontAttributeName), run_length_1);
402 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
404 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
405 NSFontAttributeName), run_length_2);
406 EXPECT_TRUE(RunHasFontTrait(decorated, run_length_1, NSBoldFontMask));
408 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
409 NSFontAttributeName), run_length_3);
410 EXPECT_FALSE(RunHasFontTrait(decorated, run_length_1 + run_length_2,
414 // Check that MatchText() styles description matches as expected.
415 TEST_F(OmniboxPopupViewMacTest, MatchTextDescriptionMatch) {
416 NSString* const contents = @"This is a test";
417 NSString* const description = @"That was a test";
419 const NSUInteger run_length_1 = 8, run_length_2 = 7;
420 // Make sure nobody messed up the inputs.
421 EXPECT_EQ(run_length_1 + run_length_2, [description length]);
423 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
424 base::SysNSStringToUTF16(description));
426 // Push each run onto contents classifications.
427 m.description_class.push_back(
428 ACMatchClassification(0, ACMatchClassification::MATCH));
429 m.description_class.push_back(
430 ACMatchClassification(run_length_1, ACMatchClassification::NONE));
432 NSAttributedString* decorated =
433 OmniboxPopupViewMac::MatchText(m, font_, kLargeWidth);
435 // Result contains the characters of the input.
436 EXPECT_GT([decorated length], [contents length] + [description length]);
437 EXPECT_TRUE([[decorated string] hasPrefix:contents]);
438 EXPECT_TRUE([[decorated string] hasSuffix:description]);
440 // Check that the description is a different color from the
442 const NSUInteger descriptionLocation =
443 [decorated length] - [description length];
444 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
445 NSForegroundColorAttributeName),
446 descriptionLocation);
447 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
448 NSForegroundColorAttributeName),
449 [description length]);
451 // Should have three font runs, not bold, bold, then not bold again.
452 // The first run is the contents and the separator, the second run
453 // is the first run of the description.
454 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
455 NSFontAttributeName), descriptionLocation);
456 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
458 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
459 NSFontAttributeName), run_length_1);
460 EXPECT_TRUE(RunHasFontTrait(decorated, descriptionLocation, NSBoldFontMask));
462 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation + run_length_1,
463 NSFontAttributeName), run_length_2);
464 EXPECT_FALSE(RunHasFontTrait(decorated, descriptionLocation + run_length_1,
468 TEST_F(OmniboxPopupViewMacTest, ElideString) {
469 NSString* const contents = @"This is a test with long contents";
470 const string16 contents16(base::SysNSStringToUTF16(contents));
472 const float kWide = 1000.0;
473 const float kNarrow = 20.0;
475 NSDictionary* attributes =
476 [NSDictionary dictionaryWithObject:font_.GetNativeFont()
477 forKey:NSFontAttributeName];
478 base::scoped_nsobject<NSMutableAttributedString> as(
479 [[NSMutableAttributedString alloc] initWithString:contents
480 attributes:attributes]);
482 // Nothing happens if the space is really wide.
483 NSMutableAttributedString* ret =
484 OmniboxPopupViewMac::ElideString(as, contents16, font_, kWide);
485 EXPECT_TRUE(ret == as);
486 EXPECT_TRUE([[as string] isEqualToString:contents]);
488 // When elided, result is the same as ElideText().
489 ret = OmniboxPopupViewMac::ElideString(as, contents16, font_, kNarrow);
491 gfx::ElideText(contents16, font_, kNarrow, gfx::ELIDE_AT_END);
492 EXPECT_TRUE(ret == as);
493 EXPECT_FALSE([[as string] isEqualToString:contents]);
494 EXPECT_TRUE([[as string] isEqualToString:base::SysUTF16ToNSString(elided)]);
496 // When elided, result is the same as ElideText().
497 ret = OmniboxPopupViewMac::ElideString(as, contents16, font_, 0.0);
498 elided = gfx::ElideText(contents16, font_, 0.0, gfx::ELIDE_AT_END);
499 EXPECT_TRUE(ret == as);
500 EXPECT_FALSE([[as string] isEqualToString:contents]);
501 EXPECT_TRUE([[as string] isEqualToString:base::SysUTF16ToNSString(elided)]);
504 TEST_F(OmniboxPopupViewMacTest, MatchTextElide) {
505 NSString* const contents = @"This is a test with long contents";
506 NSString* const description = @"That was a test";
508 const NSUInteger run_length_1 = 20, run_length_2 = 4, run_length_3 = 9;
509 // Make sure nobody messed up the inputs.
510 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [contents length]);
512 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
513 base::SysNSStringToUTF16(description));
515 // Push each run onto contents classifications.
516 m.contents_class.push_back(
517 ACMatchClassification(0, ACMatchClassification::NONE));
518 m.contents_class.push_back(
519 ACMatchClassification(run_length_1, ACMatchClassification::MATCH));
520 m.contents_class.push_back(
521 ACMatchClassification(run_length_1 + run_length_2,
522 ACMatchClassification::URL));
524 // Figure out the width of the contents.
525 NSDictionary* attributes =
526 [NSDictionary dictionaryWithObject:font_.GetNativeFont()
527 forKey:NSFontAttributeName];
528 const float contentsWidth = [contents sizeWithAttributes:attributes].width;
530 // After accounting for the width of the image, this will force us
531 // to elide the contents.
532 float cellWidth = ceil(contentsWidth / 0.7);
534 NSAttributedString* decorated =
535 OmniboxPopupViewMac::MatchText(m, font_, cellWidth);
537 // Results contain a prefix of the contents and all of description.
538 NSString* commonPrefix =
539 [[decorated string] commonPrefixWithString:contents options:0];
540 EXPECT_GT([commonPrefix length], 0U);
541 EXPECT_LT([commonPrefix length], [contents length]);
542 EXPECT_TRUE([[decorated string] hasSuffix:description]);
544 // At one point the code had a bug where elided text was being
545 // marked up using pre-elided offsets, resulting in out-of-range
546 // values being passed to NSAttributedString. Push the ellipsis
547 // through part of each run to verify that we don't continue to see
549 while([commonPrefix length] > run_length_1 - 3) {
550 EXPECT_GT(cellWidth, 0.0);
552 decorated = OmniboxPopupViewMac::MatchText(m, font_, cellWidth);
554 [[decorated string] commonPrefixWithString:contents options:0];
555 ASSERT_GT([commonPrefix length], 0U);
559 TEST_F(OmniboxPopupViewMacTest, UpdatePopupAppearance) {
560 base::scoped_nsobject<NSTextField> field(
561 [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 100, 20)]);
562 [[test_window() contentView] addSubview:field];
564 OmniboxViewMac view(NULL, profile(), NULL, NULL);
565 MockOmniboxPopupViewMac popup_view(&view, view.model(), field);
567 popup_view.UpdatePopupAppearance();
568 EXPECT_FALSE(popup_view.IsOpen());
569 EXPECT_EQ(0, [popup_view.matrix() numberOfRows]);
571 popup_view.SetResultCount(3);
572 popup_view.UpdatePopupAppearance();
573 EXPECT_TRUE(popup_view.IsOpen());
574 EXPECT_EQ(3, [popup_view.matrix() numberOfRows]);
576 int old_height = popup_view.GetTargetBounds().height();
577 popup_view.SetResultCount(5);
578 popup_view.UpdatePopupAppearance();
579 EXPECT_GT(popup_view.GetTargetBounds().height(), old_height);
580 EXPECT_EQ(5, [popup_view.matrix() numberOfRows]);
582 popup_view.SetResultCount(0);
583 popup_view.UpdatePopupAppearance();
584 EXPECT_FALSE(popup_view.IsOpen());
585 EXPECT_EQ(0, [popup_view.matrix() numberOfRows]);