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