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/font_list.h"
14 #include "ui/gfx/rect.h"
15 #include "ui/gfx/text_elider.h"
19 const float kLargeWidth = 10000;
21 // Returns the length of the run starting at |location| for which
22 // |attributeName| remains the same.
23 NSUInteger RunLengthForAttribute(NSAttributedString* string,
25 NSString* attributeName) {
26 const NSRange full_range = NSMakeRange(0, [string length]);
28 [string attribute:attributeName
29 atIndex:location longestEffectiveRange:&range inRange:full_range];
31 // In order to signal when the run doesn't start exactly at location, return
32 // a weirdo length. This causes the incorrect expectation to manifest at the
33 // calling location, which is more useful than an EXPECT_EQ() would be here.
34 if (range.location != location) {
41 // Return true if the run starting at |location| has |color| for attribute
42 // NSForegroundColorAttributeName.
43 bool RunHasColor(NSAttributedString* string,
46 const NSRange full_range = NSMakeRange(0, [string length]);
48 NSColor* run_color = [string attribute:NSForegroundColorAttributeName
50 longestEffectiveRange:&range
53 // According to one "Ali Ozer", you can compare objects within the same color
54 // space using -isEqual:. Converting color spaces seems too heavyweight for
56 // http://lists.apple.com/archives/cocoa-dev/2005/May/msg00186.html
57 return [run_color isEqual:color] ? true : false;
60 // Return true if the run starting at |location| has the font trait(s) in |mask|
61 // font in NSFontAttributeName.
62 bool RunHasFontTrait(NSAttributedString* string,
64 NSFontTraitMask mask) {
65 const NSRange full_range = NSMakeRange(0, [string length]);
67 NSFont* run_font = [string attribute:NSFontAttributeName
69 longestEffectiveRange:&range
71 NSFontManager* fontManager = [NSFontManager sharedFontManager];
72 if (run_font && (mask == ([fontManager traitsOfFont:run_font] & mask))) {
78 // AutocompleteMatch doesn't really have the right constructor for our
79 // needs. Fake one for us to use.
80 AutocompleteMatch MakeMatch(const base::string16& contents,
81 const base::string16& description) {
82 AutocompleteMatch m(NULL, 1, true, AutocompleteMatchType::URL_WHAT_YOU_TYPED);
83 m.contents = contents;
84 m.description = description;
88 class MockOmniboxPopupViewMac : public OmniboxPopupViewMac {
90 MockOmniboxPopupViewMac(OmniboxView* omnibox_view,
91 OmniboxEditModel* edit_model,
93 : OmniboxPopupViewMac(omnibox_view, edit_model, field) {
96 void SetResultCount(size_t count) {
98 for (size_t i = 0; i < count; ++i)
99 matches.push_back(AutocompleteMatch());
101 result_.AppendMatches(matches);
105 virtual const AutocompleteResult& GetResult() const OVERRIDE {
110 AutocompleteResult result_;
113 class OmniboxPopupViewMacTest : public CocoaProfileTest {
115 OmniboxPopupViewMacTest() {
116 color_ = [NSColor blackColor];
117 dim_color_ = [NSColor darkGrayColor];
119 base::SysNSStringToUTF8([[NSFont userFontOfSize:12] fontName]), 12);
123 NSColor* color_; // weak
124 NSColor* dim_color_; // weak
128 DISALLOW_COPY_AND_ASSIGN(OmniboxPopupViewMacTest);
131 // Simple inputs with no matches should result in styled output who's text
132 // matches the input string, with the passed-in color, and nothing bolded.
133 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringNoMatch) {
134 NSString* const string = @"This is a test";
135 AutocompleteMatch::ACMatchClassifications classifications;
137 NSAttributedString* decorated =
138 OmniboxPopupViewMac::DecorateMatchedString(
139 base::SysNSStringToUTF16(string), classifications,
140 color_, dim_color_, font_);
142 // Result has same characters as the input.
143 EXPECT_EQ([decorated length], [string length]);
144 EXPECT_TRUE([[decorated string] isEqualToString:string]);
146 // Our passed-in color for the entire string.
147 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
148 NSForegroundColorAttributeName),
150 EXPECT_TRUE(RunHasColor(decorated, 0U, color_));
152 // An unbolded font for the entire string.
153 EXPECT_EQ(RunLengthForAttribute(decorated, 0U, NSFontAttributeName),
155 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
158 // Identical to DecorateMatchedStringNoMatch, except test that URL style gets a
159 // different color than we passed in.
160 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringURLNoMatch) {
161 NSString* const string = @"This is a test";
162 AutocompleteMatch::ACMatchClassifications classifications;
164 classifications.push_back(
165 ACMatchClassification(0, ACMatchClassification::URL));
167 NSAttributedString* decorated =
168 OmniboxPopupViewMac::DecorateMatchedString(
169 base::SysNSStringToUTF16(string), classifications,
170 color_, dim_color_, font_);
172 // Result has same characters as the input.
173 EXPECT_EQ([decorated length], [string length]);
174 EXPECT_TRUE([[decorated string] isEqualToString:string]);
176 // One color for the entire string, and it's not the one we passed in.
177 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
178 NSForegroundColorAttributeName),
180 EXPECT_FALSE(RunHasColor(decorated, 0U, color_));
182 // An unbolded font for the entire string.
183 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
184 NSFontAttributeName), [string length]);
185 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
188 // Test that DIM works as expected.
189 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringDimNoMatch) {
190 NSString* const string = @"This is a test";
192 const NSUInteger run_length_1 = 5, run_length_2 = 2, run_length_3 = 7;
193 // Make sure nobody messed up the inputs.
194 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [string length]);
196 // Push each run onto classifications.
197 AutocompleteMatch::ACMatchClassifications classifications;
198 classifications.push_back(
199 ACMatchClassification(0, ACMatchClassification::NONE));
200 classifications.push_back(
201 ACMatchClassification(run_length_1, ACMatchClassification::DIM));
202 classifications.push_back(
203 ACMatchClassification(run_length_1 + run_length_2,
204 ACMatchClassification::NONE));
206 NSAttributedString* decorated =
207 OmniboxPopupViewMac::DecorateMatchedString(
208 base::SysNSStringToUTF16(string), classifications,
209 color_, dim_color_, font_);
211 // Result has same characters as the input.
212 EXPECT_EQ([decorated length], [string length]);
213 EXPECT_TRUE([[decorated string] isEqualToString:string]);
215 // Should have three font runs, normal, dim, normal.
216 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
217 NSForegroundColorAttributeName),
219 EXPECT_TRUE(RunHasColor(decorated, 0U, color_));
221 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
222 NSForegroundColorAttributeName),
224 EXPECT_TRUE(RunHasColor(decorated, run_length_1, dim_color_));
226 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
227 NSForegroundColorAttributeName),
229 EXPECT_TRUE(RunHasColor(decorated, run_length_1 + run_length_2, color_));
231 // An unbolded font for the entire string.
232 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
233 NSFontAttributeName), [string length]);
234 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
237 // Test that the matched run gets bold-faced, but keeps the same color.
238 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringMatch) {
239 NSString* const string = @"This is a test";
241 const NSUInteger run_length_1 = 5, run_length_2 = 2, run_length_3 = 7;
242 // Make sure nobody messed up the inputs.
243 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [string length]);
245 // Push each run onto classifications.
246 AutocompleteMatch::ACMatchClassifications classifications;
247 classifications.push_back(
248 ACMatchClassification(0, ACMatchClassification::NONE));
249 classifications.push_back(
250 ACMatchClassification(run_length_1, ACMatchClassification::MATCH));
251 classifications.push_back(
252 ACMatchClassification(run_length_1 + run_length_2,
253 ACMatchClassification::NONE));
255 NSAttributedString* decorated =
256 OmniboxPopupViewMac::DecorateMatchedString(
257 base::SysNSStringToUTF16(string), classifications,
258 color_, dim_color_, font_);
260 // Result has same characters as the input.
261 EXPECT_EQ([decorated length], [string length]);
262 EXPECT_TRUE([[decorated string] isEqualToString:string]);
264 // Our passed-in color for the entire string.
265 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
266 NSForegroundColorAttributeName),
268 EXPECT_TRUE(RunHasColor(decorated, 0U, color_));
270 // Should have three font runs, not bold, bold, then not bold again.
271 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
272 NSFontAttributeName), run_length_1);
273 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
275 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
276 NSFontAttributeName), run_length_2);
277 EXPECT_TRUE(RunHasFontTrait(decorated, run_length_1, NSBoldFontMask));
279 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
280 NSFontAttributeName), run_length_3);
281 EXPECT_FALSE(RunHasFontTrait(decorated, run_length_1 + run_length_2,
285 // Just like DecorateMatchedStringURLMatch, this time with URL style.
286 TEST_F(OmniboxPopupViewMacTest, DecorateMatchedStringURLMatch) {
287 NSString* const string = @"http://hello.world/";
289 const NSUInteger run_length_1 = 7, run_length_2 = 5, run_length_3 = 7;
290 // Make sure nobody messed up the inputs.
291 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [string length]);
293 // Push each run onto classifications.
294 AutocompleteMatch::ACMatchClassifications classifications;
295 classifications.push_back(
296 ACMatchClassification(0, ACMatchClassification::URL));
297 const int kURLMatch = ACMatchClassification::URL|ACMatchClassification::MATCH;
298 classifications.push_back(ACMatchClassification(run_length_1, kURLMatch));
299 classifications.push_back(
300 ACMatchClassification(run_length_1 + run_length_2,
301 ACMatchClassification::URL));
303 NSAttributedString* decorated =
304 OmniboxPopupViewMac::DecorateMatchedString(
305 base::SysNSStringToUTF16(string), classifications,
306 color_, dim_color_, font_);
308 // Result has same characters as the input.
309 EXPECT_EQ([decorated length], [string length]);
310 EXPECT_TRUE([[decorated string] isEqualToString:string]);
312 // One color for the entire string, and it's not the one we passed in.
313 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
314 NSForegroundColorAttributeName),
316 EXPECT_FALSE(RunHasColor(decorated, 0U, color_));
318 // Should have three font runs, not bold, bold, then not bold again.
319 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
320 NSFontAttributeName), run_length_1);
321 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
323 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
324 NSFontAttributeName), run_length_2);
325 EXPECT_TRUE(RunHasFontTrait(decorated, run_length_1, NSBoldFontMask));
327 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
328 NSFontAttributeName), run_length_3);
329 EXPECT_FALSE(RunHasFontTrait(decorated, run_length_1 + run_length_2,
333 // Check that matches with both contents and description come back
334 // with contents at the beginning, description at the end, and
335 // something separating them. Not being specific about the separator
336 // on purpose, in case it changes.
337 TEST_F(OmniboxPopupViewMacTest, MatchText) {
338 NSString* const contents = @"contents";
339 NSString* const description = @"description";
340 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
341 base::SysNSStringToUTF16(description));
343 NSAttributedString* decorated =
344 OmniboxPopupViewMac::MatchText(m, font_, kLargeWidth);
346 // Result contains the characters of the input in the right places.
347 EXPECT_GT([decorated length], [contents length] + [description length]);
348 EXPECT_TRUE([[decorated string] hasPrefix:contents]);
349 EXPECT_TRUE([[decorated string] hasSuffix:description]);
351 // Check that the description is a different color from the
353 const NSUInteger descriptionLocation =
354 [decorated length] - [description length];
355 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
356 NSForegroundColorAttributeName),
357 descriptionLocation);
358 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
359 NSForegroundColorAttributeName),
360 [description length]);
362 // Same font all the way through, nothing bold.
363 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
364 NSFontAttributeName), [decorated length]);
365 EXPECT_FALSE(RunHasFontTrait(decorated, 0, NSBoldFontMask));
368 // Check that MatchText() styles content matches as expected.
369 TEST_F(OmniboxPopupViewMacTest, MatchTextContentsMatch) {
370 NSString* const contents = @"This is a test";
372 const NSUInteger run_length_1 = 5, run_length_2 = 2, run_length_3 = 7;
373 // Make sure nobody messed up the inputs.
374 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [contents length]);
376 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
379 // Push each run onto contents classifications.
380 m.contents_class.push_back(
381 ACMatchClassification(0, ACMatchClassification::NONE));
382 m.contents_class.push_back(
383 ACMatchClassification(run_length_1, ACMatchClassification::MATCH));
384 m.contents_class.push_back(
385 ACMatchClassification(run_length_1 + run_length_2,
386 ACMatchClassification::NONE));
388 NSAttributedString* decorated =
389 OmniboxPopupViewMac::MatchText(m, font_, kLargeWidth);
391 // Result has same characters as the input.
392 EXPECT_EQ([decorated length], [contents length]);
393 EXPECT_TRUE([[decorated string] isEqualToString:contents]);
395 // Result is all one color.
396 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
397 NSForegroundColorAttributeName),
400 // Should have three font runs, not bold, bold, then not bold again.
401 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
402 NSFontAttributeName), run_length_1);
403 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
405 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1,
406 NSFontAttributeName), run_length_2);
407 EXPECT_TRUE(RunHasFontTrait(decorated, run_length_1, NSBoldFontMask));
409 EXPECT_EQ(RunLengthForAttribute(decorated, run_length_1 + run_length_2,
410 NSFontAttributeName), run_length_3);
411 EXPECT_FALSE(RunHasFontTrait(decorated, run_length_1 + run_length_2,
415 // Check that MatchText() styles description matches as expected.
416 TEST_F(OmniboxPopupViewMacTest, MatchTextDescriptionMatch) {
417 NSString* const contents = @"This is a test";
418 NSString* const description = @"That was a test";
420 const NSUInteger run_length_1 = 8, run_length_2 = 7;
421 // Make sure nobody messed up the inputs.
422 EXPECT_EQ(run_length_1 + run_length_2, [description length]);
424 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
425 base::SysNSStringToUTF16(description));
427 // Push each run onto contents classifications.
428 m.description_class.push_back(
429 ACMatchClassification(0, ACMatchClassification::MATCH));
430 m.description_class.push_back(
431 ACMatchClassification(run_length_1, ACMatchClassification::NONE));
433 NSAttributedString* decorated =
434 OmniboxPopupViewMac::MatchText(m, font_, kLargeWidth);
436 // Result contains the characters of the input.
437 EXPECT_GT([decorated length], [contents length] + [description length]);
438 EXPECT_TRUE([[decorated string] hasPrefix:contents]);
439 EXPECT_TRUE([[decorated string] hasSuffix:description]);
441 // Check that the description is a different color from the
443 const NSUInteger descriptionLocation =
444 [decorated length] - [description length];
445 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
446 NSForegroundColorAttributeName),
447 descriptionLocation);
448 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
449 NSForegroundColorAttributeName),
450 [description length]);
452 // Should have three font runs, not bold, bold, then not bold again.
453 // The first run is the contents and the separator, the second run
454 // is the first run of the description.
455 EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
456 NSFontAttributeName), descriptionLocation);
457 EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
459 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
460 NSFontAttributeName), run_length_1);
461 EXPECT_TRUE(RunHasFontTrait(decorated, descriptionLocation, NSBoldFontMask));
463 EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation + run_length_1,
464 NSFontAttributeName), run_length_2);
465 EXPECT_FALSE(RunHasFontTrait(decorated, descriptionLocation + run_length_1,
469 TEST_F(OmniboxPopupViewMacTest, ElideString) {
470 NSString* const contents = @"This is a test with long contents";
471 const base::string16 contents16(base::SysNSStringToUTF16(contents));
473 const float kWide = 1000.0;
474 const float kNarrow = 20.0;
476 NSDictionary* attributes =
477 [NSDictionary dictionaryWithObject:font_.GetNativeFont()
478 forKey:NSFontAttributeName];
479 base::scoped_nsobject<NSMutableAttributedString> as(
480 [[NSMutableAttributedString alloc] initWithString:contents
481 attributes:attributes]);
483 // Nothing happens if the space is really wide.
484 NSMutableAttributedString* ret =
485 OmniboxPopupViewMac::ElideString(as, contents16, font_, kWide);
486 EXPECT_TRUE(ret == as);
487 EXPECT_TRUE([[as string] isEqualToString:contents]);
489 // When elided, result is the same as ElideText().
490 gfx::FontList font_list(font_);
491 ret = OmniboxPopupViewMac::ElideString(as, contents16, font_, kNarrow);
492 base::string16 elided =
493 gfx::ElideText(contents16, font_list, kNarrow, gfx::ELIDE_AT_END);
494 EXPECT_TRUE(ret == as);
495 EXPECT_FALSE([[as string] isEqualToString:contents]);
496 EXPECT_TRUE([[as string] isEqualToString:base::SysUTF16ToNSString(elided)]);
498 // When elided, result is the same as ElideText().
499 ret = OmniboxPopupViewMac::ElideString(as, contents16, font_, 0.0);
500 elided = gfx::ElideText(contents16, font_list, 0.0, gfx::ELIDE_AT_END);
501 EXPECT_TRUE(ret == as);
502 EXPECT_FALSE([[as string] isEqualToString:contents]);
503 EXPECT_TRUE([[as string] isEqualToString:base::SysUTF16ToNSString(elided)]);
506 TEST_F(OmniboxPopupViewMacTest, MatchTextElide) {
507 NSString* const contents = @"This is a test with long contents";
508 NSString* const description = @"That was a test";
510 const NSUInteger run_length_1 = 20, run_length_2 = 4, run_length_3 = 9;
511 // Make sure nobody messed up the inputs.
512 EXPECT_EQ(run_length_1 + run_length_2 + run_length_3, [contents length]);
514 AutocompleteMatch m = MakeMatch(base::SysNSStringToUTF16(contents),
515 base::SysNSStringToUTF16(description));
517 // Push each run onto contents classifications.
518 m.contents_class.push_back(
519 ACMatchClassification(0, ACMatchClassification::NONE));
520 m.contents_class.push_back(
521 ACMatchClassification(run_length_1, ACMatchClassification::MATCH));
522 m.contents_class.push_back(
523 ACMatchClassification(run_length_1 + run_length_2,
524 ACMatchClassification::URL));
526 // Figure out the width of the contents.
527 NSDictionary* attributes =
528 [NSDictionary dictionaryWithObject:font_.GetNativeFont()
529 forKey:NSFontAttributeName];
530 const float contentsWidth = [contents sizeWithAttributes:attributes].width;
532 // After accounting for the width of the image, this will force us
533 // to elide the contents.
534 float cellWidth = ceil(contentsWidth / 0.7);
536 NSAttributedString* decorated =
537 OmniboxPopupViewMac::MatchText(m, font_, cellWidth);
539 // Results contain a prefix of the contents and all of description.
540 NSString* commonPrefix =
541 [[decorated string] commonPrefixWithString:contents options:0];
542 EXPECT_GT([commonPrefix length], 0U);
543 EXPECT_LT([commonPrefix length], [contents length]);
544 EXPECT_TRUE([[decorated string] hasSuffix:description]);
546 // At one point the code had a bug where elided text was being
547 // marked up using pre-elided offsets, resulting in out-of-range
548 // values being passed to NSAttributedString. Push the ellipsis
549 // through part of each run to verify that we don't continue to see
551 while([commonPrefix length] > run_length_1 - 3) {
552 EXPECT_GT(cellWidth, 0.0);
554 decorated = OmniboxPopupViewMac::MatchText(m, font_, cellWidth);
556 [[decorated string] commonPrefixWithString:contents options:0];
557 ASSERT_GT([commonPrefix length], 0U);
561 TEST_F(OmniboxPopupViewMacTest, UpdatePopupAppearance) {
562 base::scoped_nsobject<NSTextField> field(
563 [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 100, 20)]);
564 [[test_window() contentView] addSubview:field];
566 OmniboxViewMac view(NULL, profile(), NULL, NULL);
567 MockOmniboxPopupViewMac popup_view(&view, view.model(), field);
569 popup_view.UpdatePopupAppearance();
570 EXPECT_FALSE(popup_view.IsOpen());
571 EXPECT_EQ(0, [popup_view.matrix() numberOfRows]);
573 popup_view.SetResultCount(3);
574 popup_view.UpdatePopupAppearance();
575 EXPECT_TRUE(popup_view.IsOpen());
576 EXPECT_EQ(3, [popup_view.matrix() numberOfRows]);
578 int old_height = popup_view.GetTargetBounds().height();
579 popup_view.SetResultCount(5);
580 popup_view.UpdatePopupAppearance();
581 EXPECT_GT(popup_view.GetTargetBounds().height(), old_height);
582 EXPECT_EQ(5, [popup_view.matrix() numberOfRows]);
584 popup_view.SetResultCount(0);
585 popup_view.UpdatePopupAppearance();
586 EXPECT_FALSE(popup_view.IsOpen());
587 EXPECT_EQ(0, [popup_view.matrix() numberOfRows]);