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