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.
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"
17 class KeywordProviderTest : public testing::Test {
19 template<class ResultType>
21 const ResultType member;
22 bool allowed_to_be_default_match;
25 template<class ResultType>
28 const size_t num_results;
29 const MatchType<ResultType> output[3];
32 KeywordProviderTest() : kw_provider_(NULL) { }
33 virtual ~KeywordProviderTest() { }
36 virtual void TearDown();
38 template<class ResultType>
39 void RunTest(TestData<ResultType>* keyword_cases,
41 ResultType AutocompleteMatch::* member);
44 static const TemplateURLService::Initializer kTestData[];
46 scoped_refptr<KeywordProvider> kw_provider_;
47 scoped_ptr<TemplateURLService> model_;
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" },
61 void KeywordProviderTest::SetUp() {
62 model_.reset(new TemplateURLService(kTestData, arraysize(kTestData)));
63 kw_provider_ = new KeywordProvider(NULL, model_.get());
66 void KeywordProviderTest::TearDown() {
71 template<class ResultType>
72 void KeywordProviderTest::RunTest(
73 TestData<ResultType>* keyword_cases,
75 ResultType AutocompleteMatch::* member) {
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);
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 } },
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 } },
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 } },
117 // Matches should be limited to three, and sorted in quality order, not
119 { ASCIIToUTF16("aaa"), 2,
120 { { ASCIIToUTF16("aaaa "), false },
121 { ASCIIToUTF16("aaaaa "), false },
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 } },
139 // Clean up keyword input properly. "http" and "https" are the only
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 },
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 } },
163 RunTest<string16>(edit_cases, arraysize(edit_cases),
164 &AutocompleteMatch::fill_into_edit);
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 } },
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 },
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 },
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 },
198 RunTest<GURL>(url_cases, arraysize(url_cases),
199 &AutocompleteMatch::destination_url);
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 } },
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 } },
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.
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 },
229 { ASCIIToUTF16("www.w w"), 2,
230 { { ASCIIToUTF16("Search www for w"), false },
231 { ASCIIToUTF16("Search weasel for w"), false },
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 } } },
241 RunTest<string16>(contents_cases, arraysize(contents_cases),
242 &AutocompleteMatch::contents);
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));
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);
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")));
271 TEST_F(KeywordProviderTest, GetSubstitutingTemplateURLForInput) {
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;
280 { "foo", string16::npos, true, "", "foo", string16::npos },
281 { "aa foo", string16::npos, true, "aa.com?foo={searchTerms}", "foo",
284 // Cursor adjustment.
285 { "aa foo", string16::npos, true, "aa.com?foo={searchTerms}", "foo",
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 },
294 { "aa foo ", 7u, true, "aa.com?foo={searchTerms}", "foo ", 4u },
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 ", 4u },
301 // Extra space after keyword, cursor in the middle.
302 { "aa foo ", 3u, true, "aa.com?foo={searchTerms}", "foo ", 0 },
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 },
308 // Disallow exact keyword match.
309 { "aa foo", string16::npos, false, "", "aa foo", string16::npos },
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(),
320 if (cases[i].expected_url.empty())
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());
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");
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 } } },
342 RunTest<GURL>(url_cases, arraysize(url_cases),
343 &AutocompleteMatch::destination_url);