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