autocomplete_popup_view_mac_unittest.mm revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
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(
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_.GetNativeFont()
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_.GetNativeFont()
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