1 // Copyright 2014 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 "components/metrics/proto/omnibox_event.pb.h"
9 #include "components/omnibox/browser/autocomplete_match.h"
10 #include "components/omnibox/browser/autocomplete_scheme_classifier.h"
11 #include "components/omnibox/browser/keyword_provider.h"
12 #include "components/omnibox/browser/mock_autocomplete_provider_client.h"
13 #include "components/search_engines/search_engines_switches.h"
14 #include "components/search_engines/template_url.h"
15 #include "components/search_engines/template_url_service.h"
16 #include "testing/gmock/include/gmock/gmock.h"
17 #include "testing/gtest/include/gtest/gtest.h"
20 using base::ASCIIToUTF16
;
24 class TestingSchemeClassifier
: public AutocompleteSchemeClassifier
{
26 metrics::OmniboxInputType::Type
GetInputTypeForScheme(
27 const std::string
& scheme
) const override
{
28 if (net::URLRequest::IsHandledProtocol(scheme
))
29 return metrics::OmniboxInputType::URL
;
30 return metrics::OmniboxInputType::INVALID
;
36 class KeywordProviderTest
: public testing::Test
{
38 template<class ResultType
>
40 const ResultType member
;
41 bool allowed_to_be_default_match
;
44 template<class ResultType
>
46 const base::string16 input
;
47 const size_t num_results
;
48 const MatchType
<ResultType
> output
[3];
51 KeywordProviderTest() : kw_provider_(NULL
) { }
52 ~KeywordProviderTest() override
{}
54 void SetUp() override
;
55 void TearDown() override
;
57 template<class ResultType
>
58 void RunTest(TestData
<ResultType
>* keyword_cases
,
60 ResultType
AutocompleteMatch::* member
);
63 static const TemplateURLService::Initializer kTestData
[];
65 scoped_refptr
<KeywordProvider
> kw_provider_
;
66 scoped_ptr
<MockAutocompleteProviderClient
> client_
;
70 const TemplateURLService::Initializer
KeywordProviderTest::kTestData
[] = {
71 { "aa", "aa.com?foo={searchTerms}", "aa" },
72 { "aaaa", "http://aaaa/?aaaa=1&b={searchTerms}&c", "aaaa" },
73 { "aaaaa", "{searchTerms}", "aaaaa" },
74 { "ab", "bogus URL {searchTerms}", "ab" },
75 { "weasel", "weasel{searchTerms}weasel", "weasel" },
76 { "www", " +%2B?={searchTerms}foo ", "www" },
77 { "nonsub", "http://nonsubstituting-keyword.com/", "nonsub" },
78 { "z", "{searchTerms}=z", "z" },
81 void KeywordProviderTest::SetUp() {
82 scoped_ptr
<TemplateURLService
> template_url_service(
83 new TemplateURLService(kTestData
, arraysize(kTestData
)));
84 client_
.reset(new MockAutocompleteProviderClient());
85 client_
->set_template_url_service(template_url_service
.Pass());
86 kw_provider_
= new KeywordProvider(client_
.get(), nullptr);
89 void KeywordProviderTest::TearDown() {
94 template<class ResultType
>
95 void KeywordProviderTest::RunTest(TestData
<ResultType
>* keyword_cases
,
97 ResultType
AutocompleteMatch::* member
) {
99 for (int i
= 0; i
< num_cases
; ++i
) {
100 SCOPED_TRACE(keyword_cases
[i
].input
);
101 AutocompleteInput
input(
102 keyword_cases
[i
].input
, base::string16::npos
, std::string(), GURL(),
103 metrics::OmniboxEventProto::INVALID_SPEC
, true, false, true, true,
104 false, TestingSchemeClassifier());
105 kw_provider_
->Start(input
, false);
106 EXPECT_TRUE(kw_provider_
->done());
107 matches
= kw_provider_
->matches();
108 ASSERT_EQ(keyword_cases
[i
].num_results
, matches
.size());
109 for (size_t j
= 0; j
< matches
.size(); ++j
) {
110 EXPECT_EQ(keyword_cases
[i
].output
[j
].member
, matches
[j
].*member
);
111 EXPECT_EQ(keyword_cases
[i
].output
[j
].allowed_to_be_default_match
,
112 matches
[j
].allowed_to_be_default_match
);
117 TEST_F(KeywordProviderTest
, Edit
) {
118 const MatchType
<base::string16
> kEmptyMatch
= { base::string16(), false };
119 TestData
<base::string16
> edit_cases
[] = {
120 // Searching for a nonexistent prefix should give nothing.
121 { ASCIIToUTF16("Not Found"), 0,
122 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
123 { ASCIIToUTF16("aaaaaNot Found"), 0,
124 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
126 // Check that tokenization only collapses whitespace between first tokens,
127 // no-query-input cases have a space appended, and action is not escaped.
128 { ASCIIToUTF16("z"), 1,
129 { { ASCIIToUTF16("z "), true }, kEmptyMatch
, kEmptyMatch
} },
130 { ASCIIToUTF16("z \t"), 1,
131 { { ASCIIToUTF16("z "), true }, kEmptyMatch
, kEmptyMatch
} },
133 // Check that exact, substituting keywords with a verbatim search term
134 // don't generate a result. (These are handled by SearchProvider.)
135 { ASCIIToUTF16("z foo"), 0,
136 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
137 { ASCIIToUTF16("z a b c++"), 0,
138 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
140 // Matches should be limited to three, and sorted in quality order, not
142 { ASCIIToUTF16("aaa"), 2,
143 { { ASCIIToUTF16("aaaa "), false },
144 { ASCIIToUTF16("aaaaa "), false },
146 { ASCIIToUTF16("a 1 2 3"), 3,
147 { { ASCIIToUTF16("aa 1 2 3"), false },
148 { ASCIIToUTF16("ab 1 2 3"), false },
149 { ASCIIToUTF16("aaaa 1 2 3"), false } } },
150 { ASCIIToUTF16("www.a"), 3,
151 { { ASCIIToUTF16("aa "), false },
152 { ASCIIToUTF16("ab "), false },
153 { ASCIIToUTF16("aaaa "), false } } },
154 // Exact matches should prevent returning inexact matches. Also, the
155 // verbatim query for this keyword match should not be returned. (It's
156 // returned by SearchProvider.)
157 { ASCIIToUTF16("aaaa foo"), 0,
158 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
159 { ASCIIToUTF16("www.aaaa foo"), 0,
160 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
162 // Clean up keyword input properly. "http" and "https" are the only
164 { ASCIIToUTF16("www"), 1,
165 { { ASCIIToUTF16("www "), true }, kEmptyMatch
, kEmptyMatch
}},
166 { ASCIIToUTF16("www."), 0,
167 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
168 { ASCIIToUTF16("www.w w"), 2,
169 { { ASCIIToUTF16("www w"), false },
170 { ASCIIToUTF16("weasel w"), false },
172 { ASCIIToUTF16("http://www"), 1,
173 { { ASCIIToUTF16("www "), true }, kEmptyMatch
, kEmptyMatch
} },
174 { ASCIIToUTF16("http://www."), 0,
175 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
176 { ASCIIToUTF16("ftp: blah"), 0,
177 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
178 { ASCIIToUTF16("mailto:z"), 0,
179 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
180 { ASCIIToUTF16("ftp://z"), 0,
181 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
182 { ASCIIToUTF16("https://z"), 1,
183 { { ASCIIToUTF16("z "), true }, kEmptyMatch
, kEmptyMatch
} },
185 // Non-substituting keywords, whether typed fully or not
186 // should not add a space.
187 { ASCIIToUTF16("nonsu"), 1,
188 { { ASCIIToUTF16("nonsub"), false }, kEmptyMatch
, kEmptyMatch
} },
189 { ASCIIToUTF16("nonsub"), 1,
190 { { ASCIIToUTF16("nonsub"), true }, kEmptyMatch
, kEmptyMatch
} },
193 RunTest
<base::string16
>(edit_cases
, arraysize(edit_cases
),
194 &AutocompleteMatch::fill_into_edit
);
197 TEST_F(KeywordProviderTest
, URL
) {
198 const MatchType
<GURL
> kEmptyMatch
= { GURL(), false };
199 TestData
<GURL
> url_cases
[] = {
200 // No query input -> empty destination URL.
201 { ASCIIToUTF16("z"), 1,
202 { { GURL(), true }, kEmptyMatch
, kEmptyMatch
} },
203 { ASCIIToUTF16("z \t"), 1,
204 { { GURL(), true }, kEmptyMatch
, kEmptyMatch
} },
206 // Check that tokenization only collapses whitespace between first tokens
207 // and query input, but not rest of URL, is escaped.
208 { ASCIIToUTF16("w bar +baz"), 2,
209 { { GURL(" +%2B?=bar+%2Bbazfoo "), false },
210 { GURL("bar+%2Bbaz=z"), false },
213 // Substitution should work with various locations of the "%s".
214 { ASCIIToUTF16("aaa 1a2b"), 2,
215 { { GURL("http://aaaa/?aaaa=1&b=1a2b&c"), false },
216 { GURL("1a2b"), false },
218 { ASCIIToUTF16("a 1 2 3"), 3,
219 { { GURL("aa.com?foo=1+2+3"), false },
220 { GURL("bogus URL 1+2+3"), false },
221 { GURL("http://aaaa/?aaaa=1&b=1+2+3&c"), false } } },
222 { ASCIIToUTF16("www.w w"), 2,
223 { { GURL(" +%2B?=wfoo "), false },
224 { GURL("weaselwweasel"), false },
228 RunTest
<GURL
>(url_cases
, arraysize(url_cases
),
229 &AutocompleteMatch::destination_url
);
232 TEST_F(KeywordProviderTest
, Contents
) {
233 const MatchType
<base::string16
> kEmptyMatch
= { base::string16(), false };
234 TestData
<base::string16
> contents_cases
[] = {
235 // No query input -> substitute "<enter query>" into contents.
236 { ASCIIToUTF16("z"), 1,
237 { { ASCIIToUTF16("Search z for <enter query>"), true },
238 kEmptyMatch
, kEmptyMatch
} },
239 { ASCIIToUTF16("z \t"), 1,
240 { { ASCIIToUTF16("Search z for <enter query>"), true },
241 kEmptyMatch
, kEmptyMatch
} },
243 // Exact keyword matches with remaining text should return nothing.
244 { ASCIIToUTF16("www.www www"), 0,
245 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
246 { ASCIIToUTF16("z a b c++"), 0,
247 { kEmptyMatch
, kEmptyMatch
, kEmptyMatch
} },
249 // Exact keyword matches with remaining text when the keyword is an
250 // extension keyword should return something. This is tested in
251 // chrome/browser/extensions/api/omnibox/omnibox_apitest.cc's
252 // in OmniboxApiTest's Basic test.
254 // Substitution should work with various locations of the "%s".
255 { ASCIIToUTF16("aaa"), 2,
256 { { ASCIIToUTF16("Search aaaa for <enter query>"), false },
257 { ASCIIToUTF16("Search aaaaa for <enter query>"), false },
259 { ASCIIToUTF16("www.w w"), 2,
260 { { ASCIIToUTF16("Search www for w"), false },
261 { ASCIIToUTF16("Search weasel for w"), false },
263 // Also, check that tokenization only collapses whitespace between first
264 // tokens and contents are not escaped or unescaped.
265 { ASCIIToUTF16("a 1 2+ 3"), 3,
266 { { ASCIIToUTF16("Search aa for 1 2+ 3"), false },
267 { ASCIIToUTF16("Search ab for 1 2+ 3"), false },
268 { ASCIIToUTF16("Search aaaa for 1 2+ 3"), false } } },
271 RunTest
<base::string16
>(contents_cases
, arraysize(contents_cases
),
272 &AutocompleteMatch::contents
);
275 TEST_F(KeywordProviderTest
, AddKeyword
) {
276 TemplateURLData data
;
277 data
.SetShortName(ASCIIToUTF16("Test"));
278 base::string16
keyword(ASCIIToUTF16("foo"));
279 data
.SetKeyword(keyword
);
280 data
.SetURL("http://www.google.com/foo?q={searchTerms}");
281 TemplateURL
* template_url
= new TemplateURL(data
);
282 client_
->GetTemplateURLService()->Add(template_url
);
285 client_
->GetTemplateURLService()->GetTemplateURLForKeyword(keyword
));
288 TEST_F(KeywordProviderTest
, RemoveKeyword
) {
289 TemplateURLService
* template_url_service
= client_
->GetTemplateURLService();
290 base::string16
url(ASCIIToUTF16("http://aaaa/?aaaa=1&b={searchTerms}&c"));
291 template_url_service
->Remove(
292 template_url_service
->GetTemplateURLForKeyword(ASCIIToUTF16("aaaa")));
293 ASSERT_TRUE(template_url_service
->GetTemplateURLForKeyword(
294 ASCIIToUTF16("aaaa")) == NULL
);
297 TEST_F(KeywordProviderTest
, GetKeywordForInput
) {
298 EXPECT_EQ(ASCIIToUTF16("aa"),
299 kw_provider_
->GetKeywordForText(ASCIIToUTF16("aa")));
300 EXPECT_EQ(base::string16(),
301 kw_provider_
->GetKeywordForText(ASCIIToUTF16("aafoo")));
302 EXPECT_EQ(base::string16(),
303 kw_provider_
->GetKeywordForText(ASCIIToUTF16("aa foo")));
306 TEST_F(KeywordProviderTest
, GetSubstitutingTemplateURLForInput
) {
308 const std::string text
;
309 const size_t cursor_position
;
310 const bool allow_exact_keyword_match
;
311 const std::string expected_url
;
312 const std::string updated_text
;
313 const size_t updated_cursor_position
;
315 { "foo", base::string16::npos
, true, "", "foo", base::string16::npos
},
316 { "aa foo", base::string16::npos
, true, "aa.com?foo={searchTerms}", "foo",
317 base::string16::npos
},
319 // Cursor adjustment.
320 { "aa foo", base::string16::npos
, true, "aa.com?foo={searchTerms}", "foo",
321 base::string16::npos
},
322 { "aa foo", 4u, true, "aa.com?foo={searchTerms}", "foo", 1u },
323 // Cursor at the end.
324 { "aa foo", 6u, true, "aa.com?foo={searchTerms}", "foo", 3u },
325 // Cursor before the first character of the remaining text.
326 { "aa foo", 3u, true, "aa.com?foo={searchTerms}", "foo", 0u },
329 { "aa foo ", 7u, true, "aa.com?foo={searchTerms}", "foo ", 4u },
330 // Trailing space without remaining text, cursor in the middle.
331 { "aa ", 3u, true, "aa.com?foo={searchTerms}", "", base::string16::npos
},
332 // Trailing space without remaining text, cursor at the end.
333 { "aa ", 4u, true, "aa.com?foo={searchTerms}", "", base::string16::npos
},
334 // Extra space after keyword, cursor at the end.
335 { "aa foo ", 8u, true, "aa.com?foo={searchTerms}", "foo ", 4u },
336 // Extra space after keyword, cursor in the middle.
337 { "aa foo ", 3u, true, "aa.com?foo={searchTerms}", "foo ", 0 },
338 // Extra space after keyword, no trailing space, cursor at the end.
339 { "aa foo", 7u, true, "aa.com?foo={searchTerms}", "foo", 3u },
340 // Extra space after keyword, no trailing space, cursor in the middle.
341 { "aa foo", 5u, true, "aa.com?foo={searchTerms}", "foo", 1u },
343 // Disallow exact keyword match.
344 { "aa foo", base::string16::npos
, false, "", "aa foo",
345 base::string16::npos
},
347 for (size_t i
= 0; i
< arraysize(cases
); i
++) {
348 AutocompleteInput
input(ASCIIToUTF16(cases
[i
].text
),
349 cases
[i
].cursor_position
, std::string(), GURL(),
350 metrics::OmniboxEventProto::INVALID_SPEC
, false,
351 false, cases
[i
].allow_exact_keyword_match
, true,
352 false, TestingSchemeClassifier());
353 const TemplateURL
* url
=
354 KeywordProvider::GetSubstitutingTemplateURLForInput(
355 client_
->GetTemplateURLService(), &input
);
356 if (cases
[i
].expected_url
.empty())
359 EXPECT_EQ(cases
[i
].expected_url
, url
->url());
360 EXPECT_EQ(ASCIIToUTF16(cases
[i
].updated_text
), input
.text());
361 EXPECT_EQ(cases
[i
].updated_cursor_position
, input
.cursor_position());
365 // If extra query params are specified on the command line, they should be
366 // reflected (only) in the default search provider's destination URL.
367 TEST_F(KeywordProviderTest
, ExtraQueryParams
) {
368 base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
369 switches::kExtraSearchQueryParams
, "a=b");
371 TestData
<GURL
> url_cases
[] = {
372 { ASCIIToUTF16("a 1 2 3"), 3,
373 { { GURL("aa.com?a=b&foo=1+2+3"), false },
374 { GURL("bogus URL 1+2+3"), false },
375 { GURL("http://aaaa/?aaaa=1&b=1+2+3&c"), false } } },
378 RunTest
<GURL
>(url_cases
, arraysize(url_cases
),
379 &AutocompleteMatch::destination_url
);
382 TEST_F(KeywordProviderTest
, DoesNotProvideMatchesOnFocus
) {
383 AutocompleteInput
input(ASCIIToUTF16("aaa"), base::string16::npos
,
384 std::string(), GURL(),
385 metrics::OmniboxEventProto::INVALID_SPEC
, true, false,
386 true, true, true, TestingSchemeClassifier());
387 kw_provider_
->Start(input
, false);
388 ASSERT_TRUE(kw_provider_
->matches().empty());