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