Home | History | Annotate | Download | only in autocomplete
      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 "base/command_line.h"
      6 #include "base/message_loop/message_loop.h"
      7 #include "base/strings/utf_string_conversions.h"
      8 #include "chrome/browser/autocomplete/autocomplete_match.h"
      9 #include "chrome/browser/autocomplete/keyword_provider.h"
     10 #include "chrome/browser/search_engines/template_url.h"
     11 #include "chrome/browser/search_engines/template_url_service.h"
     12 #include "chrome/common/chrome_switches.h"
     13 #include "chrome/test/base/testing_browser_process.h"
     14 #include "components/metrics/proto/omnibox_event.pb.h"
     15 #include "testing/gtest/include/gtest/gtest.h"
     16 #include "url/gurl.h"
     17 
     18 using base::ASCIIToUTF16;
     19 
     20 class KeywordProviderTest : public testing::Test {
     21  protected:
     22   template<class ResultType>
     23   struct MatchType {
     24     const ResultType member;
     25     bool allowed_to_be_default_match;
     26   };
     27 
     28   template<class ResultType>
     29   struct TestData {
     30     const base::string16 input;
     31     const size_t num_results;
     32     const MatchType<ResultType> output[3];
     33   };
     34 
     35   KeywordProviderTest() : kw_provider_(NULL) { }
     36   virtual ~KeywordProviderTest() { }
     37 
     38   virtual void SetUp();
     39   virtual void TearDown();
     40 
     41   template<class ResultType>
     42   void RunTest(TestData<ResultType>* keyword_cases,
     43                int num_cases,
     44                ResultType AutocompleteMatch::* member);
     45 
     46  protected:
     47   static const TemplateURLService::Initializer kTestData[];
     48 
     49   scoped_refptr<KeywordProvider> kw_provider_;
     50   scoped_ptr<TemplateURLService> model_;
     51 };
     52 
     53 // static
     54 const TemplateURLService::Initializer KeywordProviderTest::kTestData[] = {
     55   { "aa", "aa.com?foo={searchTerms}", "aa" },
     56   { "aaaa", "http://aaaa/?aaaa=1&b={searchTerms}&c", "aaaa" },
     57   { "aaaaa", "{searchTerms}", "aaaaa" },
     58   { "ab", "bogus URL {searchTerms}", "ab" },
     59   { "weasel", "weasel{searchTerms}weasel", "weasel" },
     60   { "www", " +%2B?={searchTerms}foo ", "www" },
     61   { "nonsub", "http://nonsubstituting-keyword.com/", "nonsub" },
     62   { "z", "{searchTerms}=z", "z" },
     63 };
     64 
     65 void KeywordProviderTest::SetUp() {
     66   model_.reset(new TemplateURLService(kTestData, arraysize(kTestData)));
     67   kw_provider_ = new KeywordProvider(NULL, model_.get());
     68 }
     69 
     70 void KeywordProviderTest::TearDown() {
     71   model_.reset();
     72   kw_provider_ = NULL;
     73 }
     74 
     75 template<class ResultType>
     76 void KeywordProviderTest::RunTest(
     77     TestData<ResultType>* keyword_cases,
     78     int num_cases,
     79     ResultType AutocompleteMatch::* member) {
     80   ACMatches matches;
     81   for (int i = 0; i < num_cases; ++i) {
     82     SCOPED_TRACE(keyword_cases[i].input);
     83     AutocompleteInput input(keyword_cases[i].input, base::string16::npos,
     84                             base::string16(), GURL(),
     85                             metrics::OmniboxEventProto::INVALID_SPEC, true,
     86                             false, true, true);
     87     kw_provider_->Start(input, false);
     88     EXPECT_TRUE(kw_provider_->done());
     89     matches = kw_provider_->matches();
     90     ASSERT_EQ(keyword_cases[i].num_results, matches.size());
     91     for (size_t j = 0; j < matches.size(); ++j) {
     92       EXPECT_EQ(keyword_cases[i].output[j].member, matches[j].*member);
     93       EXPECT_EQ(keyword_cases[i].output[j].allowed_to_be_default_match,
     94                 matches[j].allowed_to_be_default_match);
     95     }
     96   }
     97 }
     98 
     99 TEST_F(KeywordProviderTest, Edit) {
    100   const MatchType<base::string16> kEmptyMatch = { base::string16(), false };
    101   TestData<base::string16> edit_cases[] = {
    102     // Searching for a nonexistent prefix should give nothing.
    103     { ASCIIToUTF16("Not Found"), 0,
    104       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    105     { ASCIIToUTF16("aaaaaNot Found"), 0,
    106       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    107 
    108     // Check that tokenization only collapses whitespace between first tokens,
    109     // no-query-input cases have a space appended, and action is not escaped.
    110     { ASCIIToUTF16("z"), 1,
    111       { { ASCIIToUTF16("z "), true }, kEmptyMatch, kEmptyMatch } },
    112     { ASCIIToUTF16("z    \t"), 1,
    113       { { ASCIIToUTF16("z "), true }, kEmptyMatch, kEmptyMatch } },
    114 
    115     // Check that exact, substituting keywords with a verbatim search term
    116     // don't generate a result.  (These are handled by SearchProvider.)
    117     { ASCIIToUTF16("z foo"), 0,
    118       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    119     { ASCIIToUTF16("z   a   b   c++"), 0,
    120       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    121 
    122     // Matches should be limited to three, and sorted in quality order, not
    123     // alphabetical.
    124     { ASCIIToUTF16("aaa"), 2,
    125       { { ASCIIToUTF16("aaaa "), false },
    126         { ASCIIToUTF16("aaaaa "), false },
    127         kEmptyMatch } },
    128     { ASCIIToUTF16("a 1 2 3"), 3,
    129      { { ASCIIToUTF16("aa 1 2 3"), false },
    130        { ASCIIToUTF16("ab 1 2 3"), false },
    131        { ASCIIToUTF16("aaaa 1 2 3"), false } } },
    132     { ASCIIToUTF16("www.a"), 3,
    133       { { ASCIIToUTF16("aa "), false },
    134         { ASCIIToUTF16("ab "), false },
    135         { ASCIIToUTF16("aaaa "), false } } },
    136     // Exact matches should prevent returning inexact matches.  Also, the
    137     // verbatim query for this keyword match should not be returned.  (It's
    138     // returned by SearchProvider.)
    139     { ASCIIToUTF16("aaaa foo"), 0,
    140       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    141     { ASCIIToUTF16("www.aaaa foo"), 0,
    142       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    143 
    144     // Clean up keyword input properly.  "http" and "https" are the only
    145     // allowed schemes.
    146     { ASCIIToUTF16("www"), 1,
    147       { { ASCIIToUTF16("www "), true }, kEmptyMatch, kEmptyMatch }},
    148     { ASCIIToUTF16("www."), 0,
    149       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    150     { ASCIIToUTF16("www.w w"), 2,
    151       { { ASCIIToUTF16("www w"), false },
    152         { ASCIIToUTF16("weasel w"), false },
    153         kEmptyMatch } },
    154     { ASCIIToUTF16("http://www"), 1,
    155       { { ASCIIToUTF16("www "), true }, kEmptyMatch, kEmptyMatch } },
    156     { ASCIIToUTF16("http://www."), 0,
    157       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    158     { ASCIIToUTF16("ftp: blah"), 0,
    159       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    160     { ASCIIToUTF16("mailto:z"), 0,
    161       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    162     { ASCIIToUTF16("ftp://z"), 0,
    163       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    164     { ASCIIToUTF16("https://z"), 1,
    165       { { ASCIIToUTF16("z "), true }, kEmptyMatch, kEmptyMatch } },
    166 
    167     // Non-substituting keywords, whether typed fully or not
    168     // should not add a space.
    169     { ASCIIToUTF16("nonsu"), 1,
    170       { { ASCIIToUTF16("nonsub"), false }, kEmptyMatch, kEmptyMatch } },
    171     { ASCIIToUTF16("nonsub"), 1,
    172       { { ASCIIToUTF16("nonsub"), true }, kEmptyMatch, kEmptyMatch } },
    173   };
    174 
    175   RunTest<base::string16>(edit_cases, arraysize(edit_cases),
    176                     &AutocompleteMatch::fill_into_edit);
    177 }
    178 
    179 TEST_F(KeywordProviderTest, URL) {
    180   const MatchType<GURL> kEmptyMatch = { GURL(), false };
    181   TestData<GURL> url_cases[] = {
    182     // No query input -> empty destination URL.
    183     { ASCIIToUTF16("z"), 1,
    184       { { GURL(), true }, kEmptyMatch, kEmptyMatch } },
    185     { ASCIIToUTF16("z    \t"), 1,
    186       { { GURL(), true }, kEmptyMatch, kEmptyMatch } },
    187 
    188     // Check that tokenization only collapses whitespace between first tokens
    189     // and query input, but not rest of URL, is escaped.
    190     { ASCIIToUTF16("w  bar +baz"), 2,
    191       { { GURL(" +%2B?=bar+%2Bbazfoo "), false },
    192         { GURL("bar+%2Bbaz=z"), false },
    193         kEmptyMatch } },
    194 
    195     // Substitution should work with various locations of the "%s".
    196     { ASCIIToUTF16("aaa 1a2b"), 2,
    197       { { GURL("http://aaaa/?aaaa=1&b=1a2b&c"), false },
    198         { GURL("1a2b"), false },
    199         kEmptyMatch } },
    200     { ASCIIToUTF16("a 1 2 3"), 3,
    201       { { GURL("aa.com?foo=1+2+3"), false },
    202         { GURL("bogus URL 1+2+3"), false },
    203         { GURL("http://aaaa/?aaaa=1&b=1+2+3&c"), false } } },
    204     { ASCIIToUTF16("www.w w"), 2,
    205       { { GURL(" +%2B?=wfoo "), false },
    206         { GURL("weaselwweasel"), false },
    207         kEmptyMatch } },
    208   };
    209 
    210   RunTest<GURL>(url_cases, arraysize(url_cases),
    211                 &AutocompleteMatch::destination_url);
    212 }
    213 
    214 TEST_F(KeywordProviderTest, Contents) {
    215   const MatchType<base::string16> kEmptyMatch = { base::string16(), false };
    216   TestData<base::string16> contents_cases[] = {
    217     // No query input -> substitute "<enter query>" into contents.
    218     { ASCIIToUTF16("z"), 1,
    219       { { ASCIIToUTF16("Search z for <enter query>"), true },
    220         kEmptyMatch, kEmptyMatch } },
    221     { ASCIIToUTF16("z    \t"), 1,
    222       { { ASCIIToUTF16("Search z for <enter query>"), true },
    223         kEmptyMatch, kEmptyMatch } },
    224 
    225     // Exact keyword matches with remaining text should return nothing.
    226     { ASCIIToUTF16("www.www www"), 0,
    227       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    228     { ASCIIToUTF16("z   a   b   c++"), 0,
    229       { kEmptyMatch, kEmptyMatch, kEmptyMatch } },
    230 
    231     // Exact keyword matches with remaining text when the keyword is an
    232     // extension keyword should return something.  This is tested in
    233     // chrome/browser/extensions/api/omnibox/omnibox_apitest.cc's
    234     // in OmniboxApiTest's Basic test.
    235 
    236     // Substitution should work with various locations of the "%s".
    237     { ASCIIToUTF16("aaa"), 2,
    238       { { ASCIIToUTF16("Search aaaa for <enter query>"), false },
    239         { ASCIIToUTF16("Search aaaaa for <enter query>"), false },
    240         kEmptyMatch} },
    241     { ASCIIToUTF16("www.w w"), 2,
    242       { { ASCIIToUTF16("Search www for w"), false },
    243         { ASCIIToUTF16("Search weasel for w"), false },
    244         kEmptyMatch } },
    245     // Also, check that tokenization only collapses whitespace between first
    246     // tokens and contents are not escaped or unescaped.
    247     { ASCIIToUTF16("a   1 2+ 3"), 3,
    248       { { ASCIIToUTF16("Search aa for 1 2+ 3"), false },
    249         { ASCIIToUTF16("Search ab for 1 2+ 3"), false },
    250         { ASCIIToUTF16("Search aaaa for 1 2+ 3"), false } } },
    251   };
    252 
    253   RunTest<base::string16>(contents_cases, arraysize(contents_cases),
    254                     &AutocompleteMatch::contents);
    255 }
    256 
    257 TEST_F(KeywordProviderTest, AddKeyword) {
    258   TemplateURLData data;
    259   data.short_name = ASCIIToUTF16("Test");
    260   base::string16 keyword(ASCIIToUTF16("foo"));
    261   data.SetKeyword(keyword);
    262   data.SetURL("http://www.google.com/foo?q={searchTerms}");
    263   TemplateURL* template_url = new TemplateURL(data);
    264   model_->Add(template_url);
    265   ASSERT_TRUE(template_url == model_->GetTemplateURLForKeyword(keyword));
    266 }
    267 
    268 TEST_F(KeywordProviderTest, RemoveKeyword) {
    269   base::string16 url(ASCIIToUTF16("http://aaaa/?aaaa=1&b={searchTerms}&c"));
    270   model_->Remove(model_->GetTemplateURLForKeyword(ASCIIToUTF16("aaaa")));
    271   ASSERT_TRUE(model_->GetTemplateURLForKeyword(ASCIIToUTF16("aaaa")) == NULL);
    272 }
    273 
    274 TEST_F(KeywordProviderTest, GetKeywordForInput) {
    275   EXPECT_EQ(ASCIIToUTF16("aa"),
    276       kw_provider_->GetKeywordForText(ASCIIToUTF16("aa")));
    277   EXPECT_EQ(base::string16(),
    278       kw_provider_->GetKeywordForText(ASCIIToUTF16("aafoo")));
    279   EXPECT_EQ(base::string16(),
    280       kw_provider_->GetKeywordForText(ASCIIToUTF16("aa foo")));
    281 }
    282 
    283 TEST_F(KeywordProviderTest, GetSubstitutingTemplateURLForInput) {
    284   struct {
    285     const std::string text;
    286     const size_t cursor_position;
    287     const bool allow_exact_keyword_match;
    288     const std::string expected_url;
    289     const std::string updated_text;
    290     const size_t updated_cursor_position;
    291   } cases[] = {
    292     { "foo", base::string16::npos, true, "", "foo", base::string16::npos },
    293     { "aa foo", base::string16::npos, true, "aa.com?foo={searchTerms}", "foo",
    294       base::string16::npos },
    295 
    296     // Cursor adjustment.
    297     { "aa foo", base::string16::npos, true, "aa.com?foo={searchTerms}", "foo",
    298       base::string16::npos },
    299     { "aa foo", 4u, true, "aa.com?foo={searchTerms}", "foo", 1u },
    300     // Cursor at the end.
    301     { "aa foo", 6u, true, "aa.com?foo={searchTerms}", "foo", 3u },
    302     // Cursor before the first character of the remaining text.
    303     { "aa foo", 3u, true, "aa.com?foo={searchTerms}", "foo", 0u },
    304 
    305     // Trailing space.
    306     { "aa foo ", 7u, true, "aa.com?foo={searchTerms}", "foo ", 4u },
    307     // Trailing space without remaining text, cursor in the middle.
    308     { "aa  ", 3u, true, "aa.com?foo={searchTerms}", "", base::string16::npos },
    309     // Trailing space without remaining text, cursor at the end.
    310     { "aa  ", 4u, true, "aa.com?foo={searchTerms}", "", base::string16::npos },
    311     // Extra space after keyword, cursor at the end.
    312     { "aa  foo ", 8u, true, "aa.com?foo={searchTerms}", "foo ", 4u },
    313     // Extra space after keyword, cursor in the middle.
    314     { "aa  foo ", 3u, true, "aa.com?foo={searchTerms}", "foo ", 0 },
    315     // Extra space after keyword, no trailing space, cursor at the end.
    316     { "aa  foo", 7u, true, "aa.com?foo={searchTerms}", "foo", 3u },
    317     // Extra space after keyword, no trailing space, cursor in the middle.
    318     { "aa  foo", 5u, true, "aa.com?foo={searchTerms}", "foo", 1u },
    319 
    320     // Disallow exact keyword match.
    321     { "aa foo", base::string16::npos, false, "", "aa foo",
    322       base::string16::npos },
    323   };
    324   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); i++) {
    325     AutocompleteInput input(ASCIIToUTF16(cases[i].text),
    326                             cases[i].cursor_position, base::string16(), GURL(),
    327                             metrics::OmniboxEventProto::INVALID_SPEC, false,
    328                             false, cases[i].allow_exact_keyword_match, true);
    329     const TemplateURL* url =
    330         KeywordProvider::GetSubstitutingTemplateURLForInput(model_.get(),
    331                                                             &input);
    332     if (cases[i].expected_url.empty())
    333       EXPECT_FALSE(url);
    334     else
    335       EXPECT_EQ(cases[i].expected_url, url->url());
    336     EXPECT_EQ(ASCIIToUTF16(cases[i].updated_text), input.text());
    337     EXPECT_EQ(cases[i].updated_cursor_position, input.cursor_position());
    338   }
    339 }
    340 
    341 // If extra query params are specified on the command line, they should be
    342 // reflected (only) in the default search provider's destination URL.
    343 TEST_F(KeywordProviderTest, ExtraQueryParams) {
    344   CommandLine::ForCurrentProcess()->AppendSwitchASCII(
    345       switches::kExtraSearchQueryParams, "a=b");
    346 
    347   TestData<GURL> url_cases[] = {
    348     { ASCIIToUTF16("a 1 2 3"), 3,
    349       { { GURL("aa.com?a=b&foo=1+2+3"), false },
    350         { GURL("bogus URL 1+2+3"), false },
    351         { GURL("http://aaaa/?aaaa=1&b=1+2+3&c"), false } } },
    352   };
    353 
    354   RunTest<GURL>(url_cases, arraysize(url_cases),
    355                 &AutocompleteMatch::destination_url);
    356 }
    357