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 #include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h"
9 #include "base/memory/scoped_ptr.h"
10 #include "base/metrics/field_trial.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/autocomplete/autocomplete_match.h"
13 #include "chrome/browser/autocomplete/autocomplete_result.h"
14 #include "components/variations/entropy_provider.h"
15 #include "testing/platform_test.h"
16 #include "ui/base/gtk/gtk_hig_constants.h"
17 #include "ui/gfx/font.h"
18 #include "ui/gfx/rect.h"
22 const GdkColor kContentTextColor
= GDK_COLOR_RGB(0x00, 0x00, 0x00);
23 const GdkColor kDimContentTextColor
= GDK_COLOR_RGB(0x80, 0x80, 0x80);
24 const GdkColor kURLTextColor
= GDK_COLOR_RGB(0x00, 0x88, 0x00);
26 class TestableOmniboxPopupViewGtk
: public OmniboxPopupViewGtk
{
28 TestableOmniboxPopupViewGtk()
29 : OmniboxPopupViewGtk(gfx::Font(), NULL
, NULL
, NULL
),
34 virtual ~TestableOmniboxPopupViewGtk() {
37 virtual void Show(size_t num_results
) OVERRIDE
{
41 virtual void Hide() OVERRIDE
{
45 virtual const AutocompleteResult
& GetResult() const OVERRIDE
{
49 using OmniboxPopupViewGtk::GetRectForLine
;
50 using OmniboxPopupViewGtk::LineFromY
;
51 using OmniboxPopupViewGtk::GetHiddenMatchCount
;
53 AutocompleteResult result_
;
60 class OmniboxPopupViewGtkTest
: public PlatformTest
{
62 OmniboxPopupViewGtkTest() {}
64 virtual void SetUp() {
65 PlatformTest::SetUp();
67 window_
= gtk_window_new(GTK_WINDOW_POPUP
);
68 layout_
= gtk_widget_create_pango_layout(window_
, NULL
);
69 view_
.reset(new TestableOmniboxPopupViewGtk
);
70 field_trial_list_
.reset(new base::FieldTrialList(
71 new metrics::SHA1EntropyProvider("42")));
74 virtual void TearDown() {
75 g_object_unref(layout_
);
76 gtk_widget_destroy(window_
);
78 PlatformTest::TearDown();
81 // The google C++ Testing Framework documentation suggests making
82 // accessors in the fixture so that each test doesn't need to be a
83 // friend of the class being tested. This method just proxies the
84 // call through after adding the fixture's layout_.
85 void SetupLayoutForMatch(
86 const base::string16
& text
,
87 const AutocompleteMatch::ACMatchClassifications
& classifications
,
88 const GdkColor
* base_color
,
89 const GdkColor
* dim_color
,
90 const GdkColor
* url_color
,
91 const std::string
& prefix_text
) {
92 OmniboxPopupViewGtk::SetupLayoutForMatch(layout_
,
102 PangoAttribute
* attr_
;
104 RunInfo() : attr_(NULL
), length_(0) { }
107 RunInfo
RunInfoForAttrType(guint location
,
109 PangoAttrType type
) {
112 PangoAttrList
* attrs
= pango_layout_get_attributes(layout_
);
116 PangoAttrIterator
* attr_iter
= pango_attr_list_get_iterator(attrs
);
120 for (gboolean more
= true, findNextStart
= false;
122 more
= pango_attr_iterator_next(attr_iter
)) {
123 PangoAttribute
* attr
= pango_attr_iterator_get(attr_iter
, type
);
125 // This iterator segment doesn't have any elements of the
126 // desired type; keep looking.
130 // Skip attribute ranges before the desired start point.
131 if (attr
->end_index
<= location
)
134 // If the matching type went past the iterator segment, then set
135 // the length to the next start - location.
137 // If the start is still less than the location, then reset
138 // the match. Otherwise, check that the new attribute is, in
139 // fact different before shortening the run length.
140 if (attr
->start_index
<= location
) {
141 findNextStart
= false;
142 } else if (!pango_attribute_equal(retval
.attr_
, attr
)) {
143 retval
.length_
= attr
->start_index
- location
;
148 gint start_range
, end_range
;
149 pango_attr_iterator_range(attr_iter
,
153 // Now we have a match. May need to keep going to shorten
154 // length if we reach a new item of the same type.
156 if (attr
->end_index
> (guint
)end_range
) {
157 retval
.length_
= end_location
- location
;
158 findNextStart
= true;
160 retval
.length_
= attr
->end_index
- location
;
165 pango_attr_iterator_destroy(attr_iter
);
169 guint
RunLengthForAttrType(guint location
,
171 PangoAttrType type
) {
172 RunInfo info
= RunInfoForAttrType(location
,
178 gboolean
RunHasAttribute(guint location
,
180 PangoAttribute
* attribute
) {
181 RunInfo info
= RunInfoForAttrType(location
,
183 attribute
->klass
->type
);
185 return info
.attr_
&& pango_attribute_equal(info
.attr_
, attribute
);
188 gboolean
RunHasColor(guint location
,
190 const GdkColor
& color
) {
191 PangoAttribute
* attribute
=
192 pango_attr_foreground_new(color
.red
,
196 gboolean retval
= RunHasAttribute(location
,
200 pango_attribute_destroy(attribute
);
205 gboolean
RunHasWeight(guint location
,
207 PangoWeight weight
) {
208 PangoAttribute
* attribute
= pango_attr_weight_new(weight
);
210 gboolean retval
= RunHasAttribute(location
,
214 pango_attribute_destroy(attribute
);
220 PangoLayout
* layout_
;
222 scoped_ptr
<TestableOmniboxPopupViewGtk
> view_
;
223 scoped_ptr
<base::FieldTrialList
> field_trial_list_
;
226 DISALLOW_COPY_AND_ASSIGN(OmniboxPopupViewGtkTest
);
229 // Simple inputs with no matches should result in styled output who's
230 // text matches the input string, with the passed-in color, and
232 TEST_F(OmniboxPopupViewGtkTest
, DecorateMatchedStringNoMatch
) {
233 const base::string16 kContents
= base::ASCIIToUTF16("This is a test");
235 AutocompleteMatch::ACMatchClassifications classifications
;
237 SetupLayoutForMatch(kContents
,
240 &kDimContentTextColor
,
244 EXPECT_EQ(kContents
.length(), RunLengthForAttrType(0U, kContents
.length(),
245 PANGO_ATTR_FOREGROUND
));
247 EXPECT_TRUE(RunHasColor(0U, kContents
.length(), kContentTextColor
));
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, kContents
.length(),
254 EXPECT_EQ(kContents
.length(), weightLength
);
255 EXPECT_TRUE(RunHasWeight(0U, kContents
.length(), PANGO_WEIGHT_NORMAL
));
259 // Identical to DecorateMatchedStringNoMatch, except test that URL
260 // style gets a different color than we passed in.
261 TEST_F(OmniboxPopupViewGtkTest
, DecorateMatchedStringURLNoMatch
) {
262 const base::string16 kContents
= base::ASCIIToUTF16("This is a test");
263 AutocompleteMatch::ACMatchClassifications classifications
;
265 classifications
.push_back(
266 ACMatchClassification(0U, ACMatchClassification::URL
));
268 SetupLayoutForMatch(kContents
,
271 &kDimContentTextColor
,
275 EXPECT_EQ(kContents
.length(), RunLengthForAttrType(0U, kContents
.length(),
276 PANGO_ATTR_FOREGROUND
));
277 EXPECT_TRUE(RunHasColor(0U, kContents
.length(), kURLTextColor
));
279 // This part's a little wacky - either we don't have a weight, or
280 // the weight run is the entire string and is NORMAL
281 guint weightLength
= RunLengthForAttrType(0U, kContents
.length(),
284 EXPECT_EQ(kContents
.length(), weightLength
);
285 EXPECT_TRUE(RunHasWeight(0U, kContents
.length(), PANGO_WEIGHT_NORMAL
));
289 // Test that DIM works as expected.
290 TEST_F(OmniboxPopupViewGtkTest
, DecorateMatchedStringDimNoMatch
) {
291 const base::string16 kContents
= base::ASCIIToUTF16("This is a test");
293 const guint kRunLength1
= 5, kRunLength2
= 2, kRunLength3
= 7;
294 // Make sure nobody messed up the inputs.
295 EXPECT_EQ(kRunLength1
+ kRunLength2
+ kRunLength3
, kContents
.length());
297 // Push each run onto classifications.
298 AutocompleteMatch::ACMatchClassifications classifications
;
299 classifications
.push_back(
300 ACMatchClassification(0U, ACMatchClassification::NONE
));
301 classifications
.push_back(
302 ACMatchClassification(kRunLength1
, ACMatchClassification::DIM
));
303 classifications
.push_back(
304 ACMatchClassification(kRunLength1
+ kRunLength2
,
305 ACMatchClassification::NONE
));
307 SetupLayoutForMatch(kContents
,
310 &kDimContentTextColor
,
314 // Check the runs have expected color and length.
315 EXPECT_EQ(kRunLength1
, RunLengthForAttrType(0U, kContents
.length(),
316 PANGO_ATTR_FOREGROUND
));
317 EXPECT_TRUE(RunHasColor(0U, kContents
.length(), kContentTextColor
));
318 EXPECT_EQ(kRunLength2
, RunLengthForAttrType(kRunLength1
, kContents
.length(),
319 PANGO_ATTR_FOREGROUND
));
320 EXPECT_TRUE(RunHasColor(kRunLength1
, kContents
.length(),
321 kDimContentTextColor
));
322 EXPECT_EQ(kRunLength3
, RunLengthForAttrType(kRunLength1
+ kRunLength2
,
324 PANGO_ATTR_FOREGROUND
));
325 EXPECT_TRUE(RunHasColor(kRunLength1
+ kRunLength2
, kContents
.length(),
328 // This part's a little wacky - either we don't have a weight, or
329 // the weight run is the entire string and is NORMAL
330 guint weightLength
= RunLengthForAttrType(0U, kContents
.length(),
333 EXPECT_EQ(kContents
.length(), weightLength
);
334 EXPECT_TRUE(RunHasWeight(0U, kContents
.length(), PANGO_WEIGHT_NORMAL
));
338 // Test that the matched run gets bold-faced, but keeps the same
340 TEST_F(OmniboxPopupViewGtkTest
, DecorateMatchedStringMatch
) {
341 const base::string16 kContents
= base::ASCIIToUTF16("This is a test");
343 const guint kRunLength1
= 5, kRunLength2
= 2, kRunLength3
= 7;
344 // Make sure nobody messed up the inputs.
345 EXPECT_EQ(kRunLength1
+ kRunLength2
+ kRunLength3
, kContents
.length());
347 // Push each run onto classifications.
348 AutocompleteMatch::ACMatchClassifications classifications
;
349 classifications
.push_back(
350 ACMatchClassification(0U, ACMatchClassification::NONE
));
351 classifications
.push_back(
352 ACMatchClassification(kRunLength1
, ACMatchClassification::MATCH
));
353 classifications
.push_back(
354 ACMatchClassification(kRunLength1
+ kRunLength2
,
355 ACMatchClassification::NONE
));
357 SetupLayoutForMatch(kContents
,
360 &kDimContentTextColor
,
364 // Check the runs have expected weight and length.
365 EXPECT_EQ(kRunLength1
, RunLengthForAttrType(0U, kContents
.length(),
367 EXPECT_TRUE(RunHasWeight(0U, kContents
.length(), PANGO_WEIGHT_NORMAL
));
368 EXPECT_EQ(kRunLength2
, RunLengthForAttrType(kRunLength1
, kContents
.length(),
370 EXPECT_TRUE(RunHasWeight(kRunLength1
, kContents
.length(), PANGO_WEIGHT_BOLD
));
371 EXPECT_EQ(kRunLength3
, RunLengthForAttrType(kRunLength1
+ kRunLength2
,
374 EXPECT_TRUE(RunHasWeight(kRunLength1
+ kRunLength2
, kContents
.length(),
375 PANGO_WEIGHT_NORMAL
));
377 // The entire string should be the same, normal color.
378 EXPECT_EQ(kContents
.length(), RunLengthForAttrType(0U, kContents
.length(),
379 PANGO_ATTR_FOREGROUND
));
380 EXPECT_TRUE(RunHasColor(0U, kContents
.length(), kContentTextColor
));
383 // Just like DecorateMatchedStringURLMatch, this time with URL style.
384 TEST_F(OmniboxPopupViewGtkTest
, DecorateMatchedStringURLMatch
) {
385 const base::string16 kContents
= base::ASCIIToUTF16("http://hello.world/");
387 const guint kRunLength1
= 7, kRunLength2
= 5, kRunLength3
= 7;
388 // Make sure nobody messed up the inputs.
389 EXPECT_EQ(kRunLength1
+ kRunLength2
+ kRunLength3
, kContents
.length());
391 // Push each run onto classifications.
392 AutocompleteMatch::ACMatchClassifications classifications
;
393 classifications
.push_back(
394 ACMatchClassification(0U, ACMatchClassification::URL
));
395 const int kURLMatch
=
396 ACMatchClassification::URL
| ACMatchClassification::MATCH
;
397 classifications
.push_back(
398 ACMatchClassification(kRunLength1
, kURLMatch
));
399 classifications
.push_back(
400 ACMatchClassification(kRunLength1
+ kRunLength2
,
401 ACMatchClassification::URL
));
403 SetupLayoutForMatch(kContents
,
406 &kDimContentTextColor
,
410 // One color for the entire string, and it's not the one we passed
412 EXPECT_EQ(kContents
.length(), RunLengthForAttrType(0U, kContents
.length(),
413 PANGO_ATTR_FOREGROUND
));
414 EXPECT_TRUE(RunHasColor(0U, kContents
.length(), kURLTextColor
));
417 // Test that the popup is not shown if there is only one hidden match.
418 TEST_F(OmniboxPopupViewGtkTest
, HidesIfOnlyOneHiddenMatch
) {
419 ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
420 "InstantExtended", "Group1 hide_verbatim:1"));
422 AutocompleteMatch match
;
423 match
.destination_url
= GURL("http://verbatim/");
424 match
.type
= AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
;
425 matches
.push_back(match
);
426 view_
->result_
.AppendMatches(matches
);
427 ASSERT_TRUE(view_
->result_
.ShouldHideTopMatch());
429 // Since there is only one match which is hidden, the popup should close.
430 view_
->UpdatePopupAppearance();
431 EXPECT_TRUE(view_
->hide_called_
);
434 // Test that the top match is skipped if the model indicates it should be
436 TEST_F(OmniboxPopupViewGtkTest
, SkipsTopMatchIfHidden
) {
437 ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
438 "InstantExtended", "Group1 hide_verbatim:1"));
441 AutocompleteMatch match
;
442 match
.destination_url
= GURL("http://verbatim/");
443 match
.type
= AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
;
444 matches
.push_back(match
);
447 AutocompleteMatch match
;
448 match
.destination_url
= GURL("http://not-verbatim/");
449 match
.type
= AutocompleteMatchType::SEARCH_OTHER_ENGINE
;
450 matches
.push_back(match
);
452 view_
->result_
.AppendMatches(matches
);
453 ASSERT_TRUE(view_
->result_
.ShouldHideTopMatch());
455 EXPECT_EQ(1U, view_
->GetHiddenMatchCount());
456 EXPECT_EQ(1U, view_
->LineFromY(0));
457 gfx::Rect rect
= view_
->GetRectForLine(1, 100);
458 EXPECT_EQ(1, rect
.y());
461 // Test that the top match is not skipped if the model does not indicate it
463 TEST_F(OmniboxPopupViewGtkTest
, DoesNotSkipTopMatchIfVisible
) {
464 ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
465 "InstantExtended", "Group1 hide_verbatim:1"));
467 AutocompleteMatch match
;
468 match
.destination_url
= GURL("http://not-verbatim/");
469 match
.type
= AutocompleteMatchType::SEARCH_OTHER_ENGINE
;
470 matches
.push_back(match
);
471 view_
->result_
.AppendMatches(matches
);
472 ASSERT_FALSE(view_
->result_
.ShouldHideTopMatch());
474 EXPECT_EQ(0U, view_
->GetHiddenMatchCount());
475 EXPECT_EQ(0U, view_
->LineFromY(0));
476 gfx::Rect rect
= view_
->GetRectForLine(1, 100);
477 EXPECT_EQ(25, rect
.y());
479 // The single match is visible so the popup should be open.
480 view_
->UpdatePopupAppearance();
481 EXPECT_TRUE(view_
->show_called_
);