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