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