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#include "chrome/browser/autocomplete/autocomplete_popup_view_gtk.h"
6
7#include <gtk/gtk.h>
8
9#include "base/utf_string_conversions.h"
10#include "chrome/browser/autocomplete/autocomplete.h"
11#include "chrome/browser/autocomplete/autocomplete_match.h"
12#include "chrome/browser/ui/gtk/gtk_util.h"
13#include "testing/platform_test.h"
14
15namespace {
16
17static const float kLargeWidth = 10000;
18
19const GdkColor kContentTextColor = GDK_COLOR_RGB(0x00, 0x00, 0x00);
20const GdkColor kDimContentTextColor = GDK_COLOR_RGB(0x80, 0x80, 0x80);
21const GdkColor kURLTextColor = GDK_COLOR_RGB(0x00, 0x88, 0x00);
22
23}  // namespace
24
25class AutocompletePopupViewGtkTest : public PlatformTest {
26 public:
27  AutocompletePopupViewGtkTest() { }
28
29  virtual void SetUp() {
30    PlatformTest::SetUp();
31
32    window_ = gtk_window_new(GTK_WINDOW_POPUP);
33    layout_ = gtk_widget_create_pango_layout(window_, NULL);
34  }
35
36  virtual void TearDown() {
37    g_object_unref(layout_);
38    gtk_widget_destroy(window_);
39
40    PlatformTest::TearDown();
41  }
42
43  // The google C++ Testing Framework documentation suggests making
44  // accessors in the fixture so that each test doesn't need to be a
45  // friend of the class being tested.  This method just proxies the
46  // call through after adding the fixture's layout_.
47  void SetupLayoutForMatch(
48      const string16& text,
49      const AutocompleteMatch::ACMatchClassifications& classifications,
50      const GdkColor* base_color,
51      const GdkColor* dim_color,
52      const GdkColor* url_color,
53      const std::string& prefix_text) {
54    AutocompletePopupViewGtk::SetupLayoutForMatch(layout_,
55                                                  text,
56                                                  classifications,
57                                                  base_color,
58                                                  dim_color,
59                                                  url_color,
60                                                  prefix_text);
61  }
62
63  struct RunInfo {
64    PangoAttribute* attr_;
65    guint length_;
66    RunInfo() : attr_(NULL), length_(0) { }
67  };
68
69  RunInfo RunInfoForAttrType(guint location,
70                             guint end_location,
71                             PangoAttrType type) {
72    RunInfo retval;
73
74    PangoAttrList* attrs = pango_layout_get_attributes(layout_);
75    if (!attrs)
76      return retval;
77
78    PangoAttrIterator* attr_iter = pango_attr_list_get_iterator(attrs);
79    if (!attr_iter)
80      return retval;
81
82    for (gboolean more = true, findNextStart = false;
83        more;
84        more = pango_attr_iterator_next(attr_iter)) {
85      PangoAttribute* attr = pango_attr_iterator_get(attr_iter, type);
86
87      // This iterator segment doesn't have any elements of the
88      // desired type; keep looking.
89      if (!attr)
90        continue;
91
92      // Skip attribute ranges before the desired start point.
93      if (attr->end_index <= location)
94        continue;
95
96      // If the matching type went past the iterator segment, then set
97      // the length to the next start - location.
98      if (findNextStart) {
99        // If the start is still less than the location, then reset
100        // the match.  Otherwise, check that the new attribute is, in
101        // fact different before shortening the run length.
102        if (attr->start_index <= location) {
103          findNextStart = false;
104        } else if (!pango_attribute_equal(retval.attr_, attr)) {
105          retval.length_ = attr->start_index - location;
106          break;
107        }
108      }
109
110      gint start_range, end_range;
111      pango_attr_iterator_range(attr_iter,
112                                &start_range,
113                                &end_range);
114
115      // Now we have a match.  May need to keep going to shorten
116      // length if we reach a new item of the same type.
117      retval.attr_ = attr;
118      if (attr->end_index > (guint)end_range) {
119        retval.length_ = end_location - location;
120        findNextStart = true;
121      } else {
122        retval.length_ = attr->end_index - location;
123        break;
124      }
125    }
126
127    pango_attr_iterator_destroy(attr_iter);
128    return retval;
129  }
130
131  guint RunLengthForAttrType(guint location,
132                            guint end_location,
133                            PangoAttrType type) {
134    RunInfo info = RunInfoForAttrType(location,
135                                      end_location,
136                                      type);
137    return info.length_;
138  }
139
140  gboolean RunHasAttribute(guint location,
141                           guint end_location,
142                           PangoAttribute* attribute) {
143    RunInfo info = RunInfoForAttrType(location,
144                                      end_location,
145                                      attribute->klass->type);
146
147    return info.attr_ && pango_attribute_equal(info.attr_, attribute);
148  }
149
150  gboolean RunHasColor(guint location,
151                       guint end_location,
152                       const GdkColor& color) {
153    PangoAttribute* attribute =
154        pango_attr_foreground_new(color.red,
155                                  color.green,
156                                  color.blue);
157
158    gboolean retval = RunHasAttribute(location,
159                                      end_location,
160                                      attribute);
161
162    pango_attribute_destroy(attribute);
163
164    return retval;
165  }
166
167  gboolean RunHasWeight(guint location,
168                        guint end_location,
169                        PangoWeight weight) {
170    PangoAttribute* attribute = pango_attr_weight_new(weight);
171
172    gboolean retval = RunHasAttribute(location,
173                                      end_location,
174                                      attribute);
175
176    pango_attribute_destroy(attribute);
177
178    return retval;
179  }
180
181  GtkWidget* window_;
182  PangoLayout* layout_;
183
184 private:
185  DISALLOW_COPY_AND_ASSIGN(AutocompletePopupViewGtkTest);
186};
187
188// Simple inputs with no matches should result in styled output who's
189// text matches the input string, with the passed-in color, and
190// nothing bolded.
191TEST_F(AutocompletePopupViewGtkTest, DecorateMatchedStringNoMatch) {
192  const string16 kContents = ASCIIToUTF16("This is a test");
193
194  AutocompleteMatch::ACMatchClassifications classifications;
195
196  SetupLayoutForMatch(kContents,
197                      classifications,
198                      &kContentTextColor,
199                      &kDimContentTextColor,
200                      &kURLTextColor,
201                      std::string());
202
203  EXPECT_EQ(kContents.size(),
204            RunLengthForAttrType(0U,
205                                 kContents.size(),
206                                 PANGO_ATTR_FOREGROUND));
207
208  EXPECT_TRUE(RunHasColor(0U,
209                          kContents.size(),
210                          kContentTextColor));
211
212  // This part's a little wacky - either we don't have a weight, or
213  // the weight run is the entire string and is NORMAL
214  guint weightLength = RunLengthForAttrType(0U,
215                                           kContents.size(),
216                                           PANGO_ATTR_WEIGHT);
217  if (weightLength) {
218    EXPECT_EQ(kContents.size(), weightLength);
219    EXPECT_TRUE(RunHasWeight(0U,
220                             kContents.size(),
221                             PANGO_WEIGHT_NORMAL));
222  }
223}
224
225// Identical to DecorateMatchedStringNoMatch, except test that URL
226// style gets a different color than we passed in.
227TEST_F(AutocompletePopupViewGtkTest, DecorateMatchedStringURLNoMatch) {
228  const string16 kContents = ASCIIToUTF16("This is a test");
229  AutocompleteMatch::ACMatchClassifications classifications;
230
231  classifications.push_back(
232      ACMatchClassification(0U, ACMatchClassification::URL));
233
234  SetupLayoutForMatch(kContents,
235                      classifications,
236                      &kContentTextColor,
237                      &kDimContentTextColor,
238                      &kURLTextColor,
239                      std::string());
240
241  EXPECT_EQ(kContents.size(),
242            RunLengthForAttrType(0U,
243                                 kContents.size(),
244                                 PANGO_ATTR_FOREGROUND));
245  EXPECT_TRUE(RunHasColor(0U,
246                          kContents.size(),
247                          kURLTextColor));
248
249  // This part's a little wacky - either we don't have a weight, or
250  // the weight run is the entire string and is NORMAL
251  guint weightLength = RunLengthForAttrType(0U,
252                                           kContents.size(),
253                                           PANGO_ATTR_WEIGHT);
254  if (weightLength) {
255    EXPECT_EQ(kContents.size(), weightLength);
256    EXPECT_TRUE(RunHasWeight(0U,
257                             kContents.size(),
258                             PANGO_WEIGHT_NORMAL));
259  }
260}
261
262// Test that DIM works as expected.
263TEST_F(AutocompletePopupViewGtkTest, DecorateMatchedStringDimNoMatch) {
264  const string16 kContents = ASCIIToUTF16("This is a test");
265  // Dim "is".
266  const guint runLength1 = 5, runLength2 = 2, runLength3 = 7;
267  // Make sure nobody messed up the inputs.
268  EXPECT_EQ(runLength1 + runLength2 + runLength3, kContents.size());
269
270  // Push each run onto classifications.
271  AutocompleteMatch::ACMatchClassifications classifications;
272  classifications.push_back(
273      ACMatchClassification(0U, ACMatchClassification::NONE));
274  classifications.push_back(
275      ACMatchClassification(runLength1, ACMatchClassification::DIM));
276  classifications.push_back(
277      ACMatchClassification(runLength1 + runLength2,
278                            ACMatchClassification::NONE));
279
280  SetupLayoutForMatch(kContents,
281                      classifications,
282                      &kContentTextColor,
283                      &kDimContentTextColor,
284                      &kURLTextColor,
285                      std::string());
286
287  // Check the runs have expected color and length.
288  EXPECT_EQ(runLength1,
289            RunLengthForAttrType(0U,
290                                 kContents.size(),
291                                 PANGO_ATTR_FOREGROUND));
292  EXPECT_TRUE(RunHasColor(0U,
293                          kContents.size(),
294                          kContentTextColor));
295  EXPECT_EQ(runLength2,
296            RunLengthForAttrType(runLength1,
297                                 kContents.size(),
298                                 PANGO_ATTR_FOREGROUND));
299  EXPECT_TRUE(RunHasColor(runLength1,
300                          kContents.size(),
301                          kDimContentTextColor));
302  EXPECT_EQ(runLength3,
303            RunLengthForAttrType(runLength1 + runLength2,
304                                 kContents.size(),
305                                 PANGO_ATTR_FOREGROUND));
306  EXPECT_TRUE(RunHasColor(runLength1 + runLength2,
307                          kContents.size(),
308                          kContentTextColor));
309
310  // This part's a little wacky - either we don't have a weight, or
311  // the weight run is the entire string and is NORMAL
312  guint weightLength = RunLengthForAttrType(0U,
313                                           kContents.size(),
314                                           PANGO_ATTR_WEIGHT);
315  if (weightLength) {
316    EXPECT_EQ(kContents.size(), weightLength);
317    EXPECT_TRUE(RunHasWeight(0U,
318                             kContents.size(),
319                             PANGO_WEIGHT_NORMAL));
320  }
321}
322
323// Test that the matched run gets bold-faced, but keeps the same
324// color.
325TEST_F(AutocompletePopupViewGtkTest, DecorateMatchedStringMatch) {
326  const string16 kContents = ASCIIToUTF16("This is a test");
327  // Match "is".
328  const guint runLength1 = 5, runLength2 = 2, runLength3 = 7;
329  // Make sure nobody messed up the inputs.
330  EXPECT_EQ(runLength1 + runLength2 + runLength3, kContents.size());
331
332  // Push each run onto classifications.
333  AutocompleteMatch::ACMatchClassifications classifications;
334  classifications.push_back(
335      ACMatchClassification(0U, ACMatchClassification::NONE));
336  classifications.push_back(
337      ACMatchClassification(runLength1, ACMatchClassification::MATCH));
338  classifications.push_back(
339      ACMatchClassification(runLength1 + runLength2,
340                            ACMatchClassification::NONE));
341
342  SetupLayoutForMatch(kContents,
343                      classifications,
344                      &kContentTextColor,
345                      &kDimContentTextColor,
346                      &kURLTextColor,
347                      std::string());
348
349  // Check the runs have expected weight and length.
350  EXPECT_EQ(runLength1,
351            RunLengthForAttrType(0U,
352                                 kContents.size(),
353                                 PANGO_ATTR_WEIGHT));
354  EXPECT_TRUE(RunHasWeight(0U,
355                          kContents.size(),
356                          PANGO_WEIGHT_NORMAL));
357  EXPECT_EQ(runLength2,
358            RunLengthForAttrType(runLength1,
359                                 kContents.size(),
360                                 PANGO_ATTR_WEIGHT));
361  EXPECT_TRUE(RunHasWeight(runLength1,
362                          kContents.size(),
363                          PANGO_WEIGHT_BOLD));
364  EXPECT_EQ(runLength3,
365            RunLengthForAttrType(runLength1 + runLength2,
366                                 kContents.size(),
367                                 PANGO_ATTR_WEIGHT));
368  EXPECT_TRUE(RunHasWeight(runLength1 + runLength2,
369                          kContents.size(),
370                          PANGO_WEIGHT_NORMAL));
371
372  // The entire string should be the same, normal color.
373  EXPECT_EQ(kContents.size(),
374            RunLengthForAttrType(0U,
375                                 kContents.size(),
376                                 PANGO_ATTR_FOREGROUND));
377  EXPECT_TRUE(RunHasColor(0U,
378                          kContents.size(),
379                          kContentTextColor));
380}
381
382// Just like DecorateMatchedStringURLMatch, this time with URL style.
383TEST_F(AutocompletePopupViewGtkTest, DecorateMatchedStringURLMatch) {
384  const string16 kContents = ASCIIToUTF16("http://hello.world/");
385  // Match "hello".
386  const guint runLength1 = 7, runLength2 = 5, runLength3 = 7;
387  // Make sure nobody messed up the inputs.
388  EXPECT_EQ(runLength1 + runLength2 + runLength3, kContents.size());
389
390  // Push each run onto classifications.
391  AutocompleteMatch::ACMatchClassifications classifications;
392  classifications.push_back(
393      ACMatchClassification(0U, ACMatchClassification::URL));
394  const int kURLMatch =
395      ACMatchClassification::URL | ACMatchClassification::MATCH;
396  classifications.push_back(
397      ACMatchClassification(runLength1,
398                            kURLMatch));
399  classifications.push_back(
400      ACMatchClassification(runLength1 + runLength2,
401                            ACMatchClassification::URL));
402
403  SetupLayoutForMatch(kContents,
404                      classifications,
405                      &kContentTextColor,
406                      &kDimContentTextColor,
407                      &kURLTextColor,
408                      std::string());
409
410  // One color for the entire string, and it's not the one we passed
411  // in.
412  EXPECT_EQ(kContents.size(),
413            RunLengthForAttrType(0U,
414                                 kContents.size(),
415                                 PANGO_ATTR_FOREGROUND));
416  EXPECT_TRUE(RunHasColor(0U,
417                          kContents.size(),
418                          kURLTextColor));
419}
420