blob: 5dbfdd51bdfc4adb48ad930b73bc7bc6bc209f7c [file] [log] [blame]
mattreynolds5afc01692016-08-19 22:09:561// Copyright 2016 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 "components/omnibox/browser/physical_web_provider.h"
6
7#include <memory>
8#include <string>
9
10#include "base/memory/ptr_util.h"
11#include "base/strings/string_number_conversions.h"
12#include "base/strings/utf_string_conversions.h"
13#include "base/values.h"
14#include "components/metrics/proto/omnibox_event.pb.h"
15#include "components/omnibox/browser/mock_autocomplete_provider_client.h"
16#include "components/omnibox/browser/test_scheme_classifier.h"
17#include "components/physical_web/data_source/physical_web_data_source.h"
hayesjordan1098aecd2016-08-24 02:33:1018#include "components/physical_web/data_source/physical_web_listener.h"
mattreynolds5afc01692016-08-19 22:09:5619#include "grit/components_strings.h"
20#include "testing/gmock/include/gmock/gmock.h"
21#include "testing/gtest/include/gtest/gtest.h"
22#include "ui/base/l10n/l10n_util.h"
mattreynolds08c79092016-09-14 22:00:0823#include "ui/gfx/text_elider.h"
mattreynolds5afc01692016-08-19 22:09:5624#include "url/gurl.h"
25
26namespace {
27
28// A mock implementation of the Physical Web data source that allows setting
29// metadata for nearby URLs directly.
30class MockPhysicalWebDataSource : public PhysicalWebDataSource {
31 public:
32 MockPhysicalWebDataSource() : metadata_(new base::ListValue()) {}
33 ~MockPhysicalWebDataSource() override {}
34
35 void StartDiscovery(bool network_request_enabled) override {}
36 void StopDiscovery() override {}
37
38 std::unique_ptr<base::ListValue> GetMetadata() override {
39 return metadata_->CreateDeepCopy();
40 }
41
42 bool HasUnresolvedDiscoveries() override {
43 return false;
44 }
45
hayesjordan1098aecd2016-08-24 02:33:1046 void RegisterListener(PhysicalWebListener* physical_web_listener) override {}
47
48 void UnregisterListener(
49 PhysicalWebListener* physical_web_listener) override {}
50
mattreynolds5afc01692016-08-19 22:09:5651 // for testing
52 void SetMetadata(std::unique_ptr<base::ListValue> metadata) {
53 metadata_ = std::move(metadata);
54 }
55
56 private:
57 std::unique_ptr<base::ListValue> metadata_;
58};
59
60// An autocomplete provider client that embeds the mock Physical Web data
61// source.
62class FakeAutocompleteProviderClient
63 : public testing::NiceMock<MockAutocompleteProviderClient> {
64 public:
65 FakeAutocompleteProviderClient()
mattreynoldsc7e56a0e2016-09-27 00:11:4966 : physical_web_data_source_(
67 base::MakeUnique<MockPhysicalWebDataSource>()),
68 is_off_the_record_(false)
mattreynolds5afc01692016-08-19 22:09:5669 {
70 }
71
72 const AutocompleteSchemeClassifier& GetSchemeClassifier() const override {
73 return scheme_classifier_;
74 }
75
76 PhysicalWebDataSource* GetPhysicalWebDataSource() override {
77 return physical_web_data_source_.get();
78 }
79
mattreynoldsc7e56a0e2016-09-27 00:11:4980 bool IsOffTheRecord() const override {
81 return is_off_the_record_;
82 }
83
mattreynolds5afc01692016-08-19 22:09:5684 // Convenience method to avoid downcasts when accessing the mock data source.
85 MockPhysicalWebDataSource* GetMockPhysicalWebDataSource() {
86 return physical_web_data_source_.get();
87 }
88
mattreynoldsc7e56a0e2016-09-27 00:11:4989 // Allow tests to enable incognito mode.
90 void SetOffTheRecord(bool is_off_the_record) {
91 is_off_the_record_ = is_off_the_record;
92 }
93
mattreynolds5afc01692016-08-19 22:09:5694 private:
95 std::unique_ptr<MockPhysicalWebDataSource> physical_web_data_source_;
96 TestSchemeClassifier scheme_classifier_;
mattreynoldsc7e56a0e2016-09-27 00:11:4997 bool is_off_the_record_;
mattreynolds5afc01692016-08-19 22:09:5698};
99
mattreynolds08c79092016-09-14 22:00:08100} // namespace
101
mattreynolds5afc01692016-08-19 22:09:56102class PhysicalWebProviderTest : public testing::Test {
103 protected:
104 PhysicalWebProviderTest() : provider_(NULL) {}
105 ~PhysicalWebProviderTest() override {}
106
107 void SetUp() override {
108 client_.reset(new FakeAutocompleteProviderClient());
mattreynolds2b530de2016-09-02 18:20:16109 provider_ = PhysicalWebProvider::Create(client_.get(), nullptr);
mattreynolds5afc01692016-08-19 22:09:56110 }
111
112 void TearDown() override {
113 provider_ = NULL;
114 }
115
116 // Create a dummy metadata list with |metadata_count| items. Each item is
117 // populated with a unique scanned URL and page metadata.
118 static std::unique_ptr<base::ListValue> CreateMetadata(
119 size_t metadata_count) {
120 auto metadata_list = base::MakeUnique<base::ListValue>();
121 for (size_t i = 0; i < metadata_count; ++i) {
122 std::string item_id = base::SizeTToString(i);
123 std::string url = "https://example.com/" + item_id;
124 auto metadata_item = base::MakeUnique<base::DictionaryValue>();
125 metadata_item->SetString("scannedUrl", url);
126 metadata_item->SetString("resolvedUrl", url);
127 metadata_item->SetString("icon", url);
128 metadata_item->SetString("title", "Example title " + item_id);
129 metadata_item->SetString("description", "Example description " + item_id);
130 metadata_list->Append(std::move(metadata_item));
131 }
132 return metadata_list;
133 }
134
mattreynolds2b530de2016-09-02 18:20:16135 // Construct an AutocompleteInput to represent tapping the omnibox from the
136 // new tab page.
137 static AutocompleteInput CreateInputForNTP() {
138 return AutocompleteInput(
139 base::string16(), base::string16::npos, std::string(), GURL(),
140 metrics::OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS,
141 false, false, true, true, true, TestSchemeClassifier());
142 }
143
144 // Construct an AutocompleteInput to represent tapping the omnibox with |url|
145 // as the current web page.
146 static AutocompleteInput CreateInputWithCurrentUrl(const std::string& url) {
mattreynolds08c79092016-09-14 22:00:08147 return AutocompleteInput(base::UTF8ToUTF16(url), base::string16::npos,
mattreynolds2b530de2016-09-02 18:20:16148 std::string(), GURL(url),
149 metrics::OmniboxEventProto::OTHER, false, false,
150 true, true, true, TestSchemeClassifier());
151 }
152
mattreynolds08c79092016-09-14 22:00:08153 // For a given |match|, check that the destination URL, contents string,
154 // description string, and default match state agree with the values specified
155 // in |url|, |contents|, |description|, and |allowed_to_be_default_match|.
156 static void ValidateMatch(const AutocompleteMatch& match,
157 const std::string& url,
158 const std::string& contents,
159 const std::string& description,
160 bool allowed_to_be_default_match) {
161 EXPECT_EQ(url, match.destination_url.spec());
162 EXPECT_EQ(contents, base::UTF16ToUTF8(match.contents));
163 EXPECT_EQ(description, base::UTF16ToUTF8(match.description));
164 EXPECT_EQ(allowed_to_be_default_match, match.allowed_to_be_default_match);
165 }
166
167 // Returns the contents string for an overflow item. Use |truncated_title|
168 // as the title of the first match, |match_count_without_default| as the
169 // total number of matches (not counting the default match), and
170 // |metadata_count| as the number of nearby Physical Web URLs for which we
171 // have metadata.
172 static std::string ConstructOverflowItemContents(
173 const std::string& truncated_title,
174 size_t match_count_without_default,
175 size_t metadata_count) {
176 // Don't treat the overflow item as a metadata match.
177 const size_t metadata_match_count = match_count_without_default - 1;
178 // Determine how many URLs we didn't create match items for.
179 const size_t additional_url_count = metadata_count - metadata_match_count;
180
181 // Build the contents string.
182 if (truncated_title.empty()) {
183 return l10n_util::GetPluralStringFUTF8(
184 IDS_PHYSICAL_WEB_OVERFLOW_EMPTY_TITLE, additional_url_count);
185 } else {
186 // Subtract one from the additional URL count because the first item is
187 // represented by its title.
188 std::string contents_suffix = l10n_util::GetPluralStringFUTF8(
189 IDS_PHYSICAL_WEB_OVERFLOW, additional_url_count - 1);
190 return truncated_title + " " + contents_suffix;
191 }
192 }
193
194 // Run a test case using |input| as the simulated state of the omnibox input
195 // field, |metadata_list| as the list of simulated Physical Web metadata,
196 // and |title_truncated| as the truncated title of the first match. In
197 // addition to checking the fields of the overflow item, this will also check
198 // that the total number of matches is equal to |expected_match_count| and
199 // that a default match and overflow item are only present when
200 // |should_expect_default_match| or |should_expect_overflow_item| are true.
201 // Metadata matches are not checked.
202 void OverflowItemTestCase(const AutocompleteInput& input,
203 std::unique_ptr<base::ListValue> metadata_list,
204 const std::string& title_truncated,
205 size_t expected_match_count,
206 bool should_expect_default_match,
207 bool should_expect_overflow_item) {
208 const size_t metadata_count = metadata_list->GetSize();
209
210 MockPhysicalWebDataSource* data_source =
211 client_->GetMockPhysicalWebDataSource();
212 EXPECT_TRUE(data_source);
213
214 data_source->SetMetadata(std::move(metadata_list));
215
216 provider_->Start(input, false);
217
218 const size_t match_count = provider_->matches().size();
219 EXPECT_EQ(expected_match_count, match_count);
220
221 const size_t match_count_without_default =
222 should_expect_default_match ? match_count - 1 : match_count;
223
224 if (should_expect_overflow_item) {
225 EXPECT_LT(match_count_without_default, metadata_count);
226 } else {
227 EXPECT_EQ(match_count_without_default, metadata_count);
228 }
229
230 size_t overflow_match_count = 0;
231 size_t default_match_count = 0;
232 for (const auto& match : provider_->matches()) {
233 if (match.type == AutocompleteMatchType::PHYSICAL_WEB_OVERFLOW) {
234 std::string contents = ConstructOverflowItemContents(
235 title_truncated, match_count_without_default, metadata_count);
236 ValidateMatch(
237 match, "chrome://physical-web/", contents,
238 l10n_util::GetStringUTF8(IDS_PHYSICAL_WEB_OVERFLOW_DESCRIPTION),
239 false);
240 ++overflow_match_count;
241 } else if (match.allowed_to_be_default_match) {
242 ++default_match_count;
243 }
244 }
245 EXPECT_EQ(should_expect_overflow_item ? 1U : 0U, overflow_match_count);
246 EXPECT_EQ(should_expect_default_match ? 1U : 0U, default_match_count);
247 }
248
mattreynolds5afc01692016-08-19 22:09:56249 std::unique_ptr<FakeAutocompleteProviderClient> client_;
250 scoped_refptr<PhysicalWebProvider> provider_;
251
252 private:
253 DISALLOW_COPY_AND_ASSIGN(PhysicalWebProviderTest);
254};
255
256TEST_F(PhysicalWebProviderTest, TestEmptyMetadataListCreatesNoMatches) {
257 MockPhysicalWebDataSource* data_source =
258 client_->GetMockPhysicalWebDataSource();
259 EXPECT_TRUE(data_source);
260
261 data_source->SetMetadata(CreateMetadata(0));
262
mattreynolds2b530de2016-09-02 18:20:16263 // Run the test with no text in the omnibox input to simulate NTP.
264 provider_->Start(CreateInputForNTP(), false);
265 EXPECT_TRUE(provider_->matches().empty());
mattreynolds5afc01692016-08-19 22:09:56266
mattreynolds2b530de2016-09-02 18:20:16267 // Run the test again with a URL in the omnibox input.
268 provider_->Start(CreateInputWithCurrentUrl("http://www.cnn.com"), false);
mattreynolds5afc01692016-08-19 22:09:56269 EXPECT_TRUE(provider_->matches().empty());
270}
271
272TEST_F(PhysicalWebProviderTest, TestSingleMetadataItemCreatesOneMatch) {
273 MockPhysicalWebDataSource* data_source =
274 client_->GetMockPhysicalWebDataSource();
275 EXPECT_TRUE(data_source);
276
277 // Extract the URL and title before inserting the metadata into the data
278 // source.
279 std::unique_ptr<base::ListValue> metadata_list = CreateMetadata(1);
280 base::DictionaryValue* metadata_item;
281 EXPECT_TRUE(metadata_list->GetDictionary(0, &metadata_item));
282 std::string resolved_url;
283 EXPECT_TRUE(metadata_item->GetString("resolvedUrl", &resolved_url));
284 std::string title;
285 EXPECT_TRUE(metadata_item->GetString("title", &title));
286
287 data_source->SetMetadata(std::move(metadata_list));
288
mattreynolds2b530de2016-09-02 18:20:16289 // Run the test with no text in the omnibox input to simulate NTP.
290 provider_->Start(CreateInputForNTP(), false);
mattreynolds5afc01692016-08-19 22:09:56291
292 // Check that there is only one match item and its fields are correct.
293 EXPECT_EQ(1U, provider_->matches().size());
294 const AutocompleteMatch& metadata_match = provider_->matches().front();
295 EXPECT_EQ(AutocompleteMatchType::PHYSICAL_WEB, metadata_match.type);
mattreynolds08c79092016-09-14 22:00:08296 ValidateMatch(metadata_match, resolved_url, resolved_url, title, false);
mattreynolds2b530de2016-09-02 18:20:16297
298 // Run the test again with a URL in the omnibox input. An additional match
299 // should be added as a default match.
300 provider_->Start(CreateInputWithCurrentUrl("http://www.cnn.com"), false);
301
302 size_t metadata_match_count = 0;
303 size_t default_match_count = 0;
304 for (const auto& match : provider_->matches()) {
305 if (match.type == AutocompleteMatchType::PHYSICAL_WEB) {
mattreynolds08c79092016-09-14 22:00:08306 ValidateMatch(match, resolved_url, resolved_url, title, false);
mattreynolds2b530de2016-09-02 18:20:16307 ++metadata_match_count;
308 } else {
309 EXPECT_TRUE(match.allowed_to_be_default_match);
310 ++default_match_count;
311 }
312 }
313 EXPECT_EQ(2U, provider_->matches().size());
314 EXPECT_EQ(1U, metadata_match_count);
315 EXPECT_EQ(1U, default_match_count);
mattreynolds5afc01692016-08-19 22:09:56316}
317
mattreynolds5afc01692016-08-19 22:09:56318TEST_F(PhysicalWebProviderTest, TestNoMatchesWithUserInput) {
319 MockPhysicalWebDataSource* data_source =
320 client_->GetMockPhysicalWebDataSource();
321 EXPECT_TRUE(data_source);
322
323 data_source->SetMetadata(CreateMetadata(1));
324
325 // Construct an AutocompleteInput to simulate user input in the omnibox input
326 // field. The provider should not generate any matches.
327 std::string text("user input");
mattreynolds08c79092016-09-14 22:00:08328 const AutocompleteInput input(
329 base::UTF8ToUTF16(text), text.length(), std::string(), GURL(),
mattreynolds5afc01692016-08-19 22:09:56330 metrics::OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS,
331 true, false, true, true, false, TestSchemeClassifier());
332 provider_->Start(input, false);
333
334 EXPECT_TRUE(provider_->matches().empty());
335}
336
mattreynolds08c79092016-09-14 22:00:08337TEST_F(PhysicalWebProviderTest, TestManyMetadataItemsCreatesOverflowItem) {
338 // Create enough metadata to guarantee an overflow item will be created.
339 const size_t metadata_count = AutocompleteProvider::kMaxMatches + 1;
340
341 // Run the test with no text in the omnibox input to simulate NTP.
342 OverflowItemTestCase(
343 CreateInputForNTP(), CreateMetadata(metadata_count), "Example title 0",
344 PhysicalWebProvider::kPhysicalWebMaxMatches, false, true);
345
346 // Run the test again with a URL in the omnibox input. An additional match
347 // should be added as a default match.
348 OverflowItemTestCase(CreateInputWithCurrentUrl("http://www.cnn.com"),
349 CreateMetadata(metadata_count), "Example title 0",
350 PhysicalWebProvider::kPhysicalWebMaxMatches + 1, true,
351 true);
352}
353
354TEST_F(PhysicalWebProviderTest, TestLongPageTitleIsTruncatedInOverflowItem) {
355 // Set a long title for the first item. The page title for this item will
356 // appear in the overflow item's content string.
357 auto metadata_list = CreateMetadata(AutocompleteProvider::kMaxMatches + 1);
358 base::DictionaryValue* metadata_item;
359 EXPECT_TRUE(metadata_list->GetDictionary(0, &metadata_item));
360 metadata_item->SetString("title", "Extra long example title 0");
361
362 OverflowItemTestCase(CreateInputForNTP(), std::move(metadata_list),
363 "Extra long exa" + std::string(gfx::kEllipsis),
364 PhysicalWebProvider::kPhysicalWebMaxMatches, false,
365 true);
366}
367
368TEST_F(PhysicalWebProviderTest, TestEmptyPageTitleInOverflowItem) {
369 // Set an empty title for the first item. Because the title is empty, we will
370 // display an alternate string in the overflow item's contents.
371 auto metadata_list = CreateMetadata(AutocompleteProvider::kMaxMatches + 1);
372 base::DictionaryValue* metadata_item;
373 EXPECT_TRUE(metadata_list->GetDictionary(0, &metadata_item));
374 metadata_item->SetString("title", "");
375
376 OverflowItemTestCase(CreateInputForNTP(), std::move(metadata_list), "",
377 PhysicalWebProvider::kPhysicalWebMaxMatches, false,
378 true);
379}
380
381TEST_F(PhysicalWebProviderTest, TestRTLPageTitleInOverflowItem) {
382 // Set a Hebrew title for the first item.
383 auto metadata_list = CreateMetadata(AutocompleteProvider::kMaxMatches + 1);
384 base::DictionaryValue* metadata_item;
385 EXPECT_TRUE(metadata_list->GetDictionary(0, &metadata_item));
386 metadata_item->SetString("title", "ויקיפדיה");
387
388 OverflowItemTestCase(CreateInputForNTP(), std::move(metadata_list),
389 "ויקיפדיה", PhysicalWebProvider::kPhysicalWebMaxMatches,
390 false, true);
mattreynolds5afc01692016-08-19 22:09:56391}
mattreynoldsc7e56a0e2016-09-27 00:11:49392
393TEST_F(PhysicalWebProviderTest, TestNoMatchesInIncognito) {
394 // Enable incognito mode
395 client_->SetOffTheRecord(true);
396
397 MockPhysicalWebDataSource* data_source =
398 client_->GetMockPhysicalWebDataSource();
399 EXPECT_TRUE(data_source);
400
401 data_source->SetMetadata(CreateMetadata(1));
402 provider_->Start(CreateInputForNTP(), false);
403
404 EXPECT_TRUE(provider_->matches().empty());
405}