OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h" | |
6 | |
7 #include <gtk/gtk.h> | |
8 | |
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" | |
19 | |
20 namespace { | |
21 | |
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); | |
25 | |
26 class TestableOmniboxPopupViewGtk : public OmniboxPopupViewGtk { | |
27 public: | |
28 TestableOmniboxPopupViewGtk() | |
29 : OmniboxPopupViewGtk(gfx::Font(), NULL, NULL, NULL), | |
30 show_called_(false), | |
31 hide_called_(false) { | |
32 } | |
33 | |
34 virtual ~TestableOmniboxPopupViewGtk() { | |
35 } | |
36 | |
37 virtual void Show(size_t num_results) OVERRIDE { | |
38 show_called_ = true; | |
39 } | |
40 | |
41 virtual void Hide() OVERRIDE { | |
42 hide_called_ = true; | |
43 } | |
44 | |
45 virtual const AutocompleteResult& GetResult() const OVERRIDE { | |
46 return result_; | |
47 } | |
48 | |
49 using OmniboxPopupViewGtk::GetRectForLine; | |
50 using OmniboxPopupViewGtk::LineFromY; | |
51 using OmniboxPopupViewGtk::GetHiddenMatchCount; | |
52 | |
53 AutocompleteResult result_; | |
54 bool show_called_; | |
55 bool hide_called_; | |
56 }; | |
57 | |
58 } // namespace | |
59 | |
60 class OmniboxPopupViewGtkTest : public PlatformTest { | |
61 public: | |
62 OmniboxPopupViewGtkTest() {} | |
63 | |
64 virtual void SetUp() { | |
65 PlatformTest::SetUp(); | |
66 | |
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"))); | |
72 } | |
73 | |
74 virtual void TearDown() { | |
75 g_object_unref(layout_); | |
76 gtk_widget_destroy(window_); | |
77 | |
78 PlatformTest::TearDown(); | |
79 } | |
80 | |
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_, | |
93 text, | |
94 classifications, | |
95 base_color, | |
96 dim_color, | |
97 url_color, | |
98 prefix_text); | |
99 } | |
100 | |
101 struct RunInfo { | |
102 PangoAttribute* attr_; | |
103 guint length_; | |
104 RunInfo() : attr_(NULL), length_(0) { } | |
105 }; | |
106 | |
107 RunInfo RunInfoForAttrType(guint location, | |
108 guint end_location, | |
109 PangoAttrType type) { | |
110 RunInfo retval; | |
111 | |
112 PangoAttrList* attrs = pango_layout_get_attributes(layout_); | |
113 if (!attrs) | |
114 return retval; | |
115 | |
116 PangoAttrIterator* attr_iter = pango_attr_list_get_iterator(attrs); | |
117 if (!attr_iter) | |
118 return retval; | |
119 | |
120 for (gboolean more = true, findNextStart = false; | |
121 more; | |
122 more = pango_attr_iterator_next(attr_iter)) { | |
123 PangoAttribute* attr = pango_attr_iterator_get(attr_iter, type); | |
124 | |
125 // This iterator segment doesn't have any elements of the | |
126 // desired type; keep looking. | |
127 if (!attr) | |
128 continue; | |
129 | |
130 // Skip attribute ranges before the desired start point. | |
131 if (attr->end_index <= location) | |
132 continue; | |
133 | |
134 // If the matching type went past the iterator segment, then set | |
135 // the length to the next start - location. | |
136 if (findNextStart) { | |
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; | |
144 break; | |
145 } | |
146 } | |
147 | |
148 gint start_range, end_range; | |
149 pango_attr_iterator_range(attr_iter, | |
150 &start_range, | |
151 &end_range); | |
152 | |
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. | |
155 retval.attr_ = attr; | |
156 if (attr->end_index > (guint)end_range) { | |
157 retval.length_ = end_location - location; | |
158 findNextStart = true; | |
159 } else { | |
160 retval.length_ = attr->end_index - location; | |
161 break; | |
162 } | |
163 } | |
164 | |
165 pango_attr_iterator_destroy(attr_iter); | |
166 return retval; | |
167 } | |
168 | |
169 guint RunLengthForAttrType(guint location, | |
170 guint end_location, | |
171 PangoAttrType type) { | |
172 RunInfo info = RunInfoForAttrType(location, | |
173 end_location, | |
174 type); | |
175 return info.length_; | |
176 } | |
177 | |
178 gboolean RunHasAttribute(guint location, | |
179 guint end_location, | |
180 PangoAttribute* attribute) { | |
181 RunInfo info = RunInfoForAttrType(location, | |
182 end_location, | |
183 attribute->klass->type); | |
184 | |
185 return info.attr_ && pango_attribute_equal(info.attr_, attribute); | |
186 } | |
187 | |
188 gboolean RunHasColor(guint location, | |
189 guint end_location, | |
190 const GdkColor& color) { | |
191 PangoAttribute* attribute = | |
192 pango_attr_foreground_new(color.red, | |
193 color.green, | |
194 color.blue); | |
195 | |
196 gboolean retval = RunHasAttribute(location, | |
197 end_location, | |
198 attribute); | |
199 | |
200 pango_attribute_destroy(attribute); | |
201 | |
202 return retval; | |
203 } | |
204 | |
205 gboolean RunHasWeight(guint location, | |
206 guint end_location, | |
207 PangoWeight weight) { | |
208 PangoAttribute* attribute = pango_attr_weight_new(weight); | |
209 | |
210 gboolean retval = RunHasAttribute(location, | |
211 end_location, | |
212 attribute); | |
213 | |
214 pango_attribute_destroy(attribute); | |
215 | |
216 return retval; | |
217 } | |
218 | |
219 GtkWidget* window_; | |
220 PangoLayout* layout_; | |
221 | |
222 scoped_ptr<TestableOmniboxPopupViewGtk> view_; | |
223 scoped_ptr<base::FieldTrialList> field_trial_list_; | |
224 | |
225 private: | |
226 DISALLOW_COPY_AND_ASSIGN(OmniboxPopupViewGtkTest); | |
227 }; | |
228 | |
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 | |
231 // nothing bolded. | |
232 TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringNoMatch) { | |
233 const base::string16 kContents = base::ASCIIToUTF16("This is a test"); | |
234 | |
235 AutocompleteMatch::ACMatchClassifications classifications; | |
236 | |
237 SetupLayoutForMatch(kContents, | |
238 classifications, | |
239 &kContentTextColor, | |
240 &kDimContentTextColor, | |
241 &kURLTextColor, | |
242 std::string()); | |
243 | |
244 EXPECT_EQ(kContents.length(), RunLengthForAttrType(0U, kContents.length(), | |
245 PANGO_ATTR_FOREGROUND)); | |
246 | |
247 EXPECT_TRUE(RunHasColor(0U, kContents.length(), kContentTextColor)); | |
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, kContents.length(), | |
252 PANGO_ATTR_WEIGHT); | |
253 if (weightLength) { | |
254 EXPECT_EQ(kContents.length(), weightLength); | |
255 EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL)); | |
256 } | |
257 } | |
258 | |
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; | |
264 | |
265 classifications.push_back( | |
266 ACMatchClassification(0U, ACMatchClassification::URL)); | |
267 | |
268 SetupLayoutForMatch(kContents, | |
269 classifications, | |
270 &kContentTextColor, | |
271 &kDimContentTextColor, | |
272 &kURLTextColor, | |
273 std::string()); | |
274 | |
275 EXPECT_EQ(kContents.length(), RunLengthForAttrType(0U, kContents.length(), | |
276 PANGO_ATTR_FOREGROUND)); | |
277 EXPECT_TRUE(RunHasColor(0U, kContents.length(), kURLTextColor)); | |
278 | |
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(), | |
282 PANGO_ATTR_WEIGHT); | |
283 if (weightLength) { | |
284 EXPECT_EQ(kContents.length(), weightLength); | |
285 EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL)); | |
286 } | |
287 } | |
288 | |
289 // Test that DIM works as expected. | |
290 TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringDimNoMatch) { | |
291 const base::string16 kContents = base::ASCIIToUTF16("This is a test"); | |
292 // Dim "is". | |
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()); | |
296 | |
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)); | |
306 | |
307 SetupLayoutForMatch(kContents, | |
308 classifications, | |
309 &kContentTextColor, | |
310 &kDimContentTextColor, | |
311 &kURLTextColor, | |
312 std::string()); | |
313 | |
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, | |
323 kContents.length(), | |
324 PANGO_ATTR_FOREGROUND)); | |
325 EXPECT_TRUE(RunHasColor(kRunLength1 + kRunLength2, kContents.length(), | |
326 kContentTextColor)); | |
327 | |
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(), | |
331 PANGO_ATTR_WEIGHT); | |
332 if (weightLength) { | |
333 EXPECT_EQ(kContents.length(), weightLength); | |
334 EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL)); | |
335 } | |
336 } | |
337 | |
338 // Test that the matched run gets bold-faced, but keeps the same | |
339 // color. | |
340 TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringMatch) { | |
341 const base::string16 kContents = base::ASCIIToUTF16("This is a test"); | |
342 // Match "is". | |
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()); | |
346 | |
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)); | |
356 | |
357 SetupLayoutForMatch(kContents, | |
358 classifications, | |
359 &kContentTextColor, | |
360 &kDimContentTextColor, | |
361 &kURLTextColor, | |
362 std::string()); | |
363 | |
364 // Check the runs have expected weight and length. | |
365 EXPECT_EQ(kRunLength1, RunLengthForAttrType(0U, kContents.length(), | |
366 PANGO_ATTR_WEIGHT)); | |
367 EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL)); | |
368 EXPECT_EQ(kRunLength2, RunLengthForAttrType(kRunLength1, kContents.length(), | |
369 PANGO_ATTR_WEIGHT)); | |
370 EXPECT_TRUE(RunHasWeight(kRunLength1, kContents.length(), PANGO_WEIGHT_BOLD)); | |
371 EXPECT_EQ(kRunLength3, RunLengthForAttrType(kRunLength1 + kRunLength2, | |
372 kContents.length(), | |
373 PANGO_ATTR_WEIGHT)); | |
374 EXPECT_TRUE(RunHasWeight(kRunLength1 + kRunLength2, kContents.length(), | |
375 PANGO_WEIGHT_NORMAL)); | |
376 | |
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)); | |
381 } | |
382 | |
383 // Just like DecorateMatchedStringURLMatch, this time with URL style. | |
384 TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringURLMatch) { | |
385 const base::string16 kContents = base::ASCIIToUTF16("http://hello.world/"); | |
386 // Match "hello". | |
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()); | |
390 | |
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)); | |
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.length(), RunLengthForAttrType(0U, kContents.length(), | |
413 PANGO_ATTR_FOREGROUND)); | |
414 EXPECT_TRUE(RunHasColor(0U, kContents.length(), kURLTextColor)); | |
415 } | |
416 | |
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")); | |
421 ACMatches matches; | |
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()); | |
428 | |
429 // Since there is only one match which is hidden, the popup should close. | |
430 view_->UpdatePopupAppearance(); | |
431 EXPECT_TRUE(view_->hide_called_); | |
432 } | |
433 | |
434 // Test that the top match is skipped if the model indicates it should be | |
435 // hidden. | |
436 TEST_F(OmniboxPopupViewGtkTest, SkipsTopMatchIfHidden) { | |
437 ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial( | |
438 "InstantExtended", "Group1 hide_verbatim:1")); | |
439 ACMatches matches; | |
440 { | |
441 AutocompleteMatch match; | |
442 match.destination_url = GURL("http://verbatim/"); | |
443 match.type = AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED; | |
444 matches.push_back(match); | |
445 } | |
446 { | |
447 AutocompleteMatch match; | |
448 match.destination_url = GURL("http://not-verbatim/"); | |
449 match.type = AutocompleteMatchType::SEARCH_OTHER_ENGINE; | |
450 matches.push_back(match); | |
451 } | |
452 view_->result_.AppendMatches(matches); | |
453 ASSERT_TRUE(view_->result_.ShouldHideTopMatch()); | |
454 | |
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()); | |
459 } | |
460 | |
461 // Test that the top match is not skipped if the model does not indicate it | |
462 // should be hidden. | |
463 TEST_F(OmniboxPopupViewGtkTest, DoesNotSkipTopMatchIfVisible) { | |
464 ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial( | |
465 "InstantExtended", "Group1 hide_verbatim:1")); | |
466 ACMatches matches; | |
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()); | |
473 | |
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()); | |
478 | |
479 // The single match is visible so the popup should be open. | |
480 view_->UpdatePopupAppearance(); | |
481 EXPECT_TRUE(view_->show_called_); | |
482 } | |
OLD | NEW |