blob: 692cddae65ea539fdacf2dbe7934c2e5dde02206 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/search_suggestion_parser.h"
#include "base/json/json_reader.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "testing/gtest/include/gtest/gtest.h"
////////////////////////////////////////////////////////////////////////////////
// DeserializeJsonData:
TEST(SearchSuggestionParserTest, DeserializeNonListJsonIsInvalid) {
std::string json_data = "{}";
std::unique_ptr<base::Value> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_FALSE(result);
}
TEST(SearchSuggestionParserTest, DeserializeMalformedJsonIsInvalid) {
std::string json_data = "} malformed json {";
std::unique_ptr<base::Value> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_FALSE(result);
}
TEST(SearchSuggestionParserTest, DeserializeJsonData) {
std::string json_data = R"([{"one": 1}])";
base::Optional<base::Value> manifest_value =
base::JSONReader::Read(json_data);
ASSERT_TRUE(manifest_value);
std::unique_ptr<base::Value> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_TRUE(result);
ASSERT_EQ(*manifest_value, *result);
}
TEST(SearchSuggestionParserTest, DeserializeWithXssiGuard) {
// For XSSI protection, non-json may precede the actual data.
// Parsing fails at: v v
std::string json_data = R"([non-json [prefix [{"one": 1}])";
// Parsing succeeds at: ^
std::unique_ptr<base::Value> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_TRUE(result);
// Specifically, we precede JSON with )]}'\n.
json_data = ")]}'\n[{\"one\": 1}]";
result = SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_TRUE(result);
}
TEST(SearchSuggestionParserTest, DeserializeWithTrailingComma) {
// The comma in this string makes this badly formed JSON, but we explicitly
// allow for this error in the JSON data.
std::string json_data = R"([{"one": 1},])";
std::unique_ptr<base::Value> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_TRUE(result);
}
////////////////////////////////////////////////////////////////////////////////
// ExtractJsonData:
// TODO(crbug.com/831283): Add some ExtractJsonData tests.
////////////////////////////////////////////////////////////////////////////////
// ParseSuggestResults:
TEST(SearchSuggestionParserTest, ParseEmptyValueIsInvalid) {
base::Value root_val;
AutocompleteInput input;
TestSchemeClassifier scheme_classifier;
int default_result_relevance = 0;
bool is_keyword_result = false;
SearchSuggestionParser::Results results;
ASSERT_FALSE(SearchSuggestionParser::ParseSuggestResults(
root_val, input, scheme_classifier, default_result_relevance,
is_keyword_result, &results));
}
TEST(SearchSuggestionParserTest, ParseNonSuggestionValueIsInvalid) {
std::string json_data = R"([{"one": 1}])";
base::Optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
AutocompleteInput input;
TestSchemeClassifier scheme_classifier;
int default_result_relevance = 0;
bool is_keyword_result = false;
SearchSuggestionParser::Results results;
ASSERT_FALSE(SearchSuggestionParser::ParseSuggestResults(
*root_val, input, scheme_classifier, default_result_relevance,
is_keyword_result, &results));
}
TEST(SearchSuggestionParserTest, ParseSuggestResults) {
std::string json_data = R"([
"chris",
["christmas", "christopher doe"],
["", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:fieldtrialtriggered": true,
"google:suggestdetail": [{
}, {
"a": "American author",
"dc": "#424242",
"i": "http://example.com/a.png",
"q": "gs_ssp=abc",
"t": "Christopher Doe"
}],
"google:suggestrelevance": [607, 606],
"google:suggesttype": ["QUERY", "ENTITY"],
"google:verbatimrelevance": 851
}])";
base::Optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(base::ASCIIToUTF16("chris"),
metrics::OmniboxEventProto::NTP, scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
*root_val, input, scheme_classifier, /*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// We have "google:suggestrelevance".
ASSERT_EQ(true, results.relevances_from_server);
// We have "google:fieldtrialtriggered".
ASSERT_EQ(true, results.field_trial_triggered);
// The "google:verbatimrelevance".
ASSERT_EQ(851, results.verbatim_relevance);
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(base::ASCIIToUTF16("christmas"), suggestion_result.suggestion());
ASSERT_EQ(base::ASCIIToUTF16(""), suggestion_result.annotation());
// This entry has no image.
ASSERT_EQ("", suggestion_result.image_dominant_color());
ASSERT_EQ(GURL(), suggestion_result.image_url());
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(base::ASCIIToUTF16("christopher doe"),
suggestion_result.suggestion());
ASSERT_EQ(base::ASCIIToUTF16("American author"),
suggestion_result.annotation());
ASSERT_EQ("#424242", suggestion_result.image_dominant_color());
ASSERT_EQ(GURL("http://example.com/a.png"), suggestion_result.image_url());
}
}
TEST(SearchSuggestionParserTest, SuggestClassification) {
SearchSuggestionParser::SuggestResult result(
base::ASCIIToUTF16("foobar"), AutocompleteMatchType::SEARCH_SUGGEST, 0,
false, 400, true, base::string16());
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
// Nothing should be bolded for ZeroSuggest classified input.
result.ClassifyMatchContents(true, base::string16());
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
const ACMatchClassifications kNone = {
{0, AutocompleteMatch::ACMatchClassification::NONE}};
EXPECT_EQ(kNone, result.match_contents_class());
// Test a simple case of bolding half the text.
result.ClassifyMatchContents(false, base::ASCIIToUTF16("foo"));
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
const ACMatchClassifications kHalfBolded = {
{0, AutocompleteMatch::ACMatchClassification::NONE},
{3, AutocompleteMatch::ACMatchClassification::MATCH}};
EXPECT_EQ(kHalfBolded, result.match_contents_class());
// Test the edge case that if we forbid bolding all, and then reclassifying
// would otherwise bold-all, we leave the existing classifications alone.
// This is weird, but it's in the function contract, and is useful for
// flicker-free search suggestions as the user types.
result.ClassifyMatchContents(false, base::ASCIIToUTF16("apple"));
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
EXPECT_EQ(kHalfBolded, result.match_contents_class());
// And finally, test the case where we do allow bolding-all.
result.ClassifyMatchContents(true, base::ASCIIToUTF16("apple"));
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
const ACMatchClassifications kBoldAll = {
{0, AutocompleteMatch::ACMatchClassification::MATCH}};
EXPECT_EQ(kBoldAll, result.match_contents_class());
}
TEST(SearchSuggestionParserTest, NavigationClassification) {
TestSchemeClassifier scheme_classifier;
SearchSuggestionParser::NavigationResult result(
scheme_classifier, GURL("https://news.google.com/"),
AutocompleteMatchType::Type::NAVSUGGEST, 0, base::string16(),
std::string(), false, 400, true, base::ASCIIToUTF16("google"));
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
const ACMatchClassifications kBoldMiddle = {
{0, AutocompleteMatch::ACMatchClassification::URL},
{5, AutocompleteMatch::ACMatchClassification::URL |
AutocompleteMatch::ACMatchClassification::MATCH},
{11, AutocompleteMatch::ACMatchClassification::URL}};
EXPECT_EQ(kBoldMiddle, result.match_contents_class());
// Reclassifying in a way that would cause bold-none if it's disallowed should
// do nothing.
result.CalculateAndClassifyMatchContents(
false, base::ASCIIToUTF16("term not found"));
EXPECT_EQ(kBoldMiddle, result.match_contents_class());
// Test the allow bold-nothing case too.
result.CalculateAndClassifyMatchContents(
true, base::ASCIIToUTF16("term not found"));
const ACMatchClassifications kAnnotateUrlOnly = {
{0, AutocompleteMatch::ACMatchClassification::URL}};
EXPECT_EQ(kAnnotateUrlOnly, result.match_contents_class());
// Nothing should be bolded for ZeroSuggest classified input.
result.CalculateAndClassifyMatchContents(true, base::string16());
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
const ACMatchClassifications kNone = {
{0, AutocompleteMatch::ACMatchClassification::NONE}};
EXPECT_EQ(kNone, result.match_contents_class());
}
TEST(SearchSuggestionParserTest, ParseHeaderTexts) {
std::string json_data = R"([
"",
["los angeles", "san diego", "las vegas", "san francisco"],
["history", "", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:headertexts":{
"a":{
"40007":"Not recommended for you",
"40008":"Recommended for you"
}
},
"google:suggestdetail":[
{
},
{
"zl":40007
},
{
"zl":40008
},
{
"zl":40009
}
],
"google:suggestrelevance": [607, 606, 605, 604],
"google:suggesttype": ["PERSONALIZED_QUERY", "QUERY", "QUERY", "QUERY"]
}])";
base::Optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(base::ASCIIToUTF16(""),
metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
*root_val, input, scheme_classifier, /*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(base::ASCIIToUTF16("los angeles"),
suggestion_result.suggestion());
// This suggestion has no header text.
ASSERT_EQ(base::ASCIIToUTF16(""), suggestion_result.header());
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(base::ASCIIToUTF16("san diego"), suggestion_result.suggestion());
ASSERT_EQ(base::ASCIIToUTF16("Not recommended for you"),
suggestion_result.header());
}
{
const auto& suggestion_result = results.suggest_results[2];
ASSERT_EQ(base::ASCIIToUTF16("las vegas"), suggestion_result.suggestion());
ASSERT_EQ(base::ASCIIToUTF16("Recommended for you"),
suggestion_result.header());
}
{
const auto& suggestion_result = results.suggest_results[3];
ASSERT_EQ(base::ASCIIToUTF16("san francisco"),
suggestion_result.suggestion());
// This suggestion has no header text.
ASSERT_EQ(base::ASCIIToUTF16(""), suggestion_result.header());
}
}