blob: 4d268f31991be6b4938ac8a6a032b93ae192e695 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h"
#include <gtk/gtk.h>
#include "base/memory/scoped_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/autocomplete/autocomplete_result.h"
#include "components/variations/entropy_provider.h"
#include "testing/platform_test.h"
#include "ui/base/gtk/gtk_hig_constants.h"
#include "ui/gfx/font.h"
#include "ui/gfx/rect.h"
namespace {
const float kLargeWidth = 10000;
const GdkColor kContentTextColor = GDK_COLOR_RGB(0x00, 0x00, 0x00);
const GdkColor kDimContentTextColor = GDK_COLOR_RGB(0x80, 0x80, 0x80);
const GdkColor kURLTextColor = GDK_COLOR_RGB(0x00, 0x88, 0x00);
class TestableOmniboxPopupViewGtk : public OmniboxPopupViewGtk {
public:
TestableOmniboxPopupViewGtk()
: OmniboxPopupViewGtk(gfx::Font(), NULL, NULL, NULL),
show_called_(false),
hide_called_(false) {
}
virtual ~TestableOmniboxPopupViewGtk() {
}
virtual void Show(size_t num_results) OVERRIDE {
show_called_ = true;
}
virtual void Hide() OVERRIDE {
hide_called_ = true;
}
virtual const AutocompleteResult& GetResult() const OVERRIDE {
return result_;
}
using OmniboxPopupViewGtk::GetRectForLine;
using OmniboxPopupViewGtk::LineFromY;
using OmniboxPopupViewGtk::GetHiddenMatchCount;
AutocompleteResult result_;
bool show_called_;
bool hide_called_;
};
} // namespace
class OmniboxPopupViewGtkTest : public PlatformTest {
public:
OmniboxPopupViewGtkTest() {}
virtual void SetUp() {
PlatformTest::SetUp();
window_ = gtk_window_new(GTK_WINDOW_POPUP);
layout_ = gtk_widget_create_pango_layout(window_, NULL);
view_.reset(new TestableOmniboxPopupViewGtk);
field_trial_list_.reset(new base::FieldTrialList(
new metrics::SHA1EntropyProvider("42")));
}
virtual void TearDown() {
g_object_unref(layout_);
gtk_widget_destroy(window_);
PlatformTest::TearDown();
}
// The google C++ Testing Framework documentation suggests making
// accessors in the fixture so that each test doesn't need to be a
// friend of the class being tested. This method just proxies the
// call through after adding the fixture's layout_.
void SetupLayoutForMatch(
const string16& text,
const AutocompleteMatch::ACMatchClassifications& classifications,
const GdkColor* base_color,
const GdkColor* dim_color,
const GdkColor* url_color,
const std::string& prefix_text) {
OmniboxPopupViewGtk::SetupLayoutForMatch(layout_,
text,
classifications,
base_color,
dim_color,
url_color,
prefix_text);
}
struct RunInfo {
PangoAttribute* attr_;
guint length_;
RunInfo() : attr_(NULL), length_(0) { }
};
RunInfo RunInfoForAttrType(guint location,
guint end_location,
PangoAttrType type) {
RunInfo retval;
PangoAttrList* attrs = pango_layout_get_attributes(layout_);
if (!attrs)
return retval;
PangoAttrIterator* attr_iter = pango_attr_list_get_iterator(attrs);
if (!attr_iter)
return retval;
for (gboolean more = true, findNextStart = false;
more;
more = pango_attr_iterator_next(attr_iter)) {
PangoAttribute* attr = pango_attr_iterator_get(attr_iter, type);
// This iterator segment doesn't have any elements of the
// desired type; keep looking.
if (!attr)
continue;
// Skip attribute ranges before the desired start point.
if (attr->end_index <= location)
continue;
// If the matching type went past the iterator segment, then set
// the length to the next start - location.
if (findNextStart) {
// If the start is still less than the location, then reset
// the match. Otherwise, check that the new attribute is, in
// fact different before shortening the run length.
if (attr->start_index <= location) {
findNextStart = false;
} else if (!pango_attribute_equal(retval.attr_, attr)) {
retval.length_ = attr->start_index - location;
break;
}
}
gint start_range, end_range;
pango_attr_iterator_range(attr_iter,
&start_range,
&end_range);
// Now we have a match. May need to keep going to shorten
// length if we reach a new item of the same type.
retval.attr_ = attr;
if (attr->end_index > (guint)end_range) {
retval.length_ = end_location - location;
findNextStart = true;
} else {
retval.length_ = attr->end_index - location;
break;
}
}
pango_attr_iterator_destroy(attr_iter);
return retval;
}
guint RunLengthForAttrType(guint location,
guint end_location,
PangoAttrType type) {
RunInfo info = RunInfoForAttrType(location,
end_location,
type);
return info.length_;
}
gboolean RunHasAttribute(guint location,
guint end_location,
PangoAttribute* attribute) {
RunInfo info = RunInfoForAttrType(location,
end_location,
attribute->klass->type);
return info.attr_ && pango_attribute_equal(info.attr_, attribute);
}
gboolean RunHasColor(guint location,
guint end_location,
const GdkColor& color) {
PangoAttribute* attribute =
pango_attr_foreground_new(color.red,
color.green,
color.blue);
gboolean retval = RunHasAttribute(location,
end_location,
attribute);
pango_attribute_destroy(attribute);
return retval;
}
gboolean RunHasWeight(guint location,
guint end_location,
PangoWeight weight) {
PangoAttribute* attribute = pango_attr_weight_new(weight);
gboolean retval = RunHasAttribute(location,
end_location,
attribute);
pango_attribute_destroy(attribute);
return retval;
}
GtkWidget* window_;
PangoLayout* layout_;
scoped_ptr<TestableOmniboxPopupViewGtk> view_;
scoped_ptr<base::FieldTrialList> field_trial_list_;
private:
DISALLOW_COPY_AND_ASSIGN(OmniboxPopupViewGtkTest);
};
// Simple inputs with no matches should result in styled output who's
// text matches the input string, with the passed-in color, and
// nothing bolded.
TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringNoMatch) {
const string16 kContents = ASCIIToUTF16("This is a test");
AutocompleteMatch::ACMatchClassifications classifications;
SetupLayoutForMatch(kContents,
classifications,
&kContentTextColor,
&kDimContentTextColor,
&kURLTextColor,
std::string());
EXPECT_EQ(kContents.length(), RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(0U, kContents.length(), kContentTextColor));
// This part's a little wacky - either we don't have a weight, or
// the weight run is the entire string and is NORMAL
guint weightLength = RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_WEIGHT);
if (weightLength) {
EXPECT_EQ(kContents.length(), weightLength);
EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL));
}
}
// Identical to DecorateMatchedStringNoMatch, except test that URL
// style gets a different color than we passed in.
TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringURLNoMatch) {
const string16 kContents = ASCIIToUTF16("This is a test");
AutocompleteMatch::ACMatchClassifications classifications;
classifications.push_back(
ACMatchClassification(0U, ACMatchClassification::URL));
SetupLayoutForMatch(kContents,
classifications,
&kContentTextColor,
&kDimContentTextColor,
&kURLTextColor,
std::string());
EXPECT_EQ(kContents.length(), RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(0U, kContents.length(), kURLTextColor));
// This part's a little wacky - either we don't have a weight, or
// the weight run is the entire string and is NORMAL
guint weightLength = RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_WEIGHT);
if (weightLength) {
EXPECT_EQ(kContents.length(), weightLength);
EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL));
}
}
// Test that DIM works as expected.
TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringDimNoMatch) {
const string16 kContents = ASCIIToUTF16("This is a test");
// Dim "is".
const guint kRunLength1 = 5, kRunLength2 = 2, kRunLength3 = 7;
// Make sure nobody messed up the inputs.
EXPECT_EQ(kRunLength1 + kRunLength2 + kRunLength3, kContents.length());
// Push each run onto classifications.
AutocompleteMatch::ACMatchClassifications classifications;
classifications.push_back(
ACMatchClassification(0U, ACMatchClassification::NONE));
classifications.push_back(
ACMatchClassification(kRunLength1, ACMatchClassification::DIM));
classifications.push_back(
ACMatchClassification(kRunLength1 + kRunLength2,
ACMatchClassification::NONE));
SetupLayoutForMatch(kContents,
classifications,
&kContentTextColor,
&kDimContentTextColor,
&kURLTextColor,
std::string());
// Check the runs have expected color and length.
EXPECT_EQ(kRunLength1, RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(0U, kContents.length(), kContentTextColor));
EXPECT_EQ(kRunLength2, RunLengthForAttrType(kRunLength1, kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(kRunLength1, kContents.length(),
kDimContentTextColor));
EXPECT_EQ(kRunLength3, RunLengthForAttrType(kRunLength1 + kRunLength2,
kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(kRunLength1 + kRunLength2, kContents.length(),
kContentTextColor));
// This part's a little wacky - either we don't have a weight, or
// the weight run is the entire string and is NORMAL
guint weightLength = RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_WEIGHT);
if (weightLength) {
EXPECT_EQ(kContents.length(), weightLength);
EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL));
}
}
// Test that the matched run gets bold-faced, but keeps the same
// color.
TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringMatch) {
const string16 kContents = ASCIIToUTF16("This is a test");
// Match "is".
const guint kRunLength1 = 5, kRunLength2 = 2, kRunLength3 = 7;
// Make sure nobody messed up the inputs.
EXPECT_EQ(kRunLength1 + kRunLength2 + kRunLength3, kContents.length());
// Push each run onto classifications.
AutocompleteMatch::ACMatchClassifications classifications;
classifications.push_back(
ACMatchClassification(0U, ACMatchClassification::NONE));
classifications.push_back(
ACMatchClassification(kRunLength1, ACMatchClassification::MATCH));
classifications.push_back(
ACMatchClassification(kRunLength1 + kRunLength2,
ACMatchClassification::NONE));
SetupLayoutForMatch(kContents,
classifications,
&kContentTextColor,
&kDimContentTextColor,
&kURLTextColor,
std::string());
// Check the runs have expected weight and length.
EXPECT_EQ(kRunLength1, RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_WEIGHT));
EXPECT_TRUE(RunHasWeight(0U, kContents.length(), PANGO_WEIGHT_NORMAL));
EXPECT_EQ(kRunLength2, RunLengthForAttrType(kRunLength1, kContents.length(),
PANGO_ATTR_WEIGHT));
EXPECT_TRUE(RunHasWeight(kRunLength1, kContents.length(), PANGO_WEIGHT_BOLD));
EXPECT_EQ(kRunLength3, RunLengthForAttrType(kRunLength1 + kRunLength2,
kContents.length(),
PANGO_ATTR_WEIGHT));
EXPECT_TRUE(RunHasWeight(kRunLength1 + kRunLength2, kContents.length(),
PANGO_WEIGHT_NORMAL));
// The entire string should be the same, normal color.
EXPECT_EQ(kContents.length(), RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(0U, kContents.length(), kContentTextColor));
}
// Just like DecorateMatchedStringURLMatch, this time with URL style.
TEST_F(OmniboxPopupViewGtkTest, DecorateMatchedStringURLMatch) {
const string16 kContents = ASCIIToUTF16("http://hello.world/");
// Match "hello".
const guint kRunLength1 = 7, kRunLength2 = 5, kRunLength3 = 7;
// Make sure nobody messed up the inputs.
EXPECT_EQ(kRunLength1 + kRunLength2 + kRunLength3, kContents.length());
// Push each run onto classifications.
AutocompleteMatch::ACMatchClassifications classifications;
classifications.push_back(
ACMatchClassification(0U, ACMatchClassification::URL));
const int kURLMatch =
ACMatchClassification::URL | ACMatchClassification::MATCH;
classifications.push_back(
ACMatchClassification(kRunLength1, kURLMatch));
classifications.push_back(
ACMatchClassification(kRunLength1 + kRunLength2,
ACMatchClassification::URL));
SetupLayoutForMatch(kContents,
classifications,
&kContentTextColor,
&kDimContentTextColor,
&kURLTextColor,
std::string());
// One color for the entire string, and it's not the one we passed
// in.
EXPECT_EQ(kContents.length(), RunLengthForAttrType(0U, kContents.length(),
PANGO_ATTR_FOREGROUND));
EXPECT_TRUE(RunHasColor(0U, kContents.length(), kURLTextColor));
}
// Test that the popup is not shown if there is only one hidden match.
TEST_F(OmniboxPopupViewGtkTest, HidesIfOnlyOneHiddenMatch) {
ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
"InstantExtended", "Group1 hide_verbatim:1"));
ACMatches matches;
AutocompleteMatch match;
match.destination_url = GURL("http://verbatim/");
match.type = AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED;
matches.push_back(match);
view_->result_.AppendMatches(matches);
ASSERT_TRUE(view_->result_.ShouldHideTopMatch());
// Since there is only one match which is hidden, the popup should close.
view_->UpdatePopupAppearance();
EXPECT_TRUE(view_->hide_called_);
}
// Test that the top match is skipped if the model indicates it should be
// hidden.
TEST_F(OmniboxPopupViewGtkTest, SkipsTopMatchIfHidden) {
ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
"InstantExtended", "Group1 hide_verbatim:1"));
ACMatches matches;
{
AutocompleteMatch match;
match.destination_url = GURL("http://verbatim/");
match.type = AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED;
matches.push_back(match);
}
{
AutocompleteMatch match;
match.destination_url = GURL("http://not-verbatim/");
match.type = AutocompleteMatchType::SEARCH_OTHER_ENGINE;
matches.push_back(match);
}
view_->result_.AppendMatches(matches);
ASSERT_TRUE(view_->result_.ShouldHideTopMatch());
EXPECT_EQ(1U, view_->GetHiddenMatchCount());
EXPECT_EQ(1U, view_->LineFromY(0));
gfx::Rect rect = view_->GetRectForLine(1, 100);
EXPECT_EQ(1, rect.y());
}
// Test that the top match is not skipped if the model does not indicate it
// should be hidden.
TEST_F(OmniboxPopupViewGtkTest, DoesNotSkipTopMatchIfVisible) {
ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
"InstantExtended", "Group1 hide_verbatim:1"));
ACMatches matches;
AutocompleteMatch match;
match.destination_url = GURL("http://not-verbatim/");
match.type = AutocompleteMatchType::SEARCH_OTHER_ENGINE;
matches.push_back(match);
view_->result_.AppendMatches(matches);
ASSERT_FALSE(view_->result_.ShouldHideTopMatch());
EXPECT_EQ(0U, view_->GetHiddenMatchCount());
EXPECT_EQ(0U, view_->LineFromY(0));
gfx::Rect rect = view_->GetRectForLine(1, 100);
EXPECT_EQ(25, rect.y());
// The single match is visible so the popup should be open.
view_->UpdatePopupAppearance();
EXPECT_TRUE(view_->show_called_);
}