blob: 20a13db218b2d88c938772625512693e0b8ede0d [file] [log] [blame] [edit]
// Copyright 2018 The Chromium Authors
// 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 <optional>
#include <sstream>
#include "base/base64.h"
#include "base/feature_list.h"
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/omnibox_proto/answer_data.pb.h"
#include "third_party/omnibox_proto/answer_type.pb.h"
#include "third_party/omnibox_proto/entity_info.pb.h"
#include "third_party/omnibox_proto/navigational_intent.pb.h"
#include "third_party/omnibox_proto/rich_answer_template.pb.h"
#include "third_party/omnibox_proto/rich_suggest_template.pb.h"
#include "third_party/omnibox_proto/suggest_template_info.pb.h"
namespace {
std::string SerializeAndEncodeProto(
const google::protobuf::MessageLite& proto) {
std::string serialized_proto_info;
proto.SerializeToString(&serialized_proto_info);
return base::Base64Encode(serialized_proto_info);
}
std::string NavigationalIntentsToJSON(
std::vector<omnibox::NavigationalIntent> nav_intents) {
std::stringstream ss;
ss << "[";
for (size_t i = 0; i < nav_intents.size(); ++i) {
if (i > 0) {
ss << ", ";
}
ss << static_cast<int>(nav_intents[i]);
}
ss << "]";
return ss.str();
}
// (Rudimentary) mechanism comparing two protobuf MessageLite objects.
// This mechanism should be sufficient as long as compared objects don't host
// any maps.
// TODO(ender): Improve the mechanism to be smarter about checking individual
// fields and their values.
bool ProtosAreEqual(const google::protobuf::MessageLite& actual,
const google::protobuf::MessageLite& expected) {
return (actual.GetTypeName() == expected.GetTypeName()) &&
(actual.SerializeAsString() == expected.SerializeAsString());
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// DeserializeJsonData:
TEST(SearchSuggestionParserTest, DeserializeNonListJsonIsInvalid) {
std::string json_data = "{}";
std::optional<base::Value::List> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_FALSE(result);
}
TEST(SearchSuggestionParserTest, DeserializeMalformedJsonIsInvalid) {
std::string json_data = "} malformed json {";
std::optional<base::Value::List> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_FALSE(result);
}
TEST(SearchSuggestionParserTest, DeserializeJsonData) {
std::string json_data = R"([{"one": 1}])";
std::optional<base::Value> manifest_value = base::JSONReader::Read(json_data);
ASSERT_TRUE(manifest_value);
std::optional<base::Value::List> 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::optional<base::Value::List> 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::optional<base::Value::List> result =
SearchSuggestionParser::DeserializeJsonData(json_data);
ASSERT_TRUE(result);
}
////////////////////////////////////////////////////////////////////////////////
// ExtractJsonData:
// TODO(crbug.com/41382281): Add some ExtractJsonData tests.
////////////////////////////////////////////////////////////////////////////////
// ParseSuggestResults:
TEST(SearchSuggestionParserTest, ParseEmptyValueIsInvalid) {
base::Value::List 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}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
AutocompleteInput input;
TestSchemeClassifier scheme_classifier;
int default_result_relevance = 0;
bool is_keyword_result = false;
SearchSuggestionParser::Results results;
ASSERT_FALSE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier, default_result_relevance,
is_keyword_result, &results));
}
TEST(SearchSuggestionParserTest, ParseSuggestResults) {
omnibox::EntityInfo entity_info;
entity_info.set_annotation("American author");
entity_info.set_dominant_color("#424242");
entity_info.set_image_url("http://example.com/a.png");
entity_info.set_suggest_search_parameters("gs_ssp=abc");
entity_info.set_name("Christopher Doe");
entity_info.set_entity_id("/m/065xxm");
std::string json_data =
R"([
"chris",
["christmas", "christopher doe", "chr.com"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:fieldtrialtriggered": true,
"google:suggestdetail": [{}, {
"google:entityinfo": ")" +
SerializeAndEncodeProto(entity_info) +
R"("
}, {}],
"google:suggestnavintents": )" +
NavigationalIntentsToJSON({omnibox::NAV_INTENT_MEDIUM,
omnibox::NAV_INTENT_LOW,
omnibox::NAV_INTENT_HIGH}) +
R"(,
"google:suggesteventid": "-223372036854775808",
"google:suggestrelevance": [607, 606, 605],
"google:suggesttype": ["QUERY", "ENTITY", "NAVIGATION"],
"google:verbatimrelevance": 851,
"google:experimentstats": [
{"2":"0:67","4":10001},
{"2":"54:67","4":10002},
{"2":"0:54","4":10003}
]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"chris", metrics::OmniboxEventProto::NTP,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), 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);
ASSERT_EQ(2U, results.suggest_results.size());
ASSERT_EQ(1U, results.navigation_results.size());
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(u"christmas", suggestion_result.suggestion());
ASSERT_EQ(u"", suggestion_result.annotation());
// This entry has no entity data
ASSERT_TRUE(ProtosAreEqual(suggestion_result.entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_EQ(suggestion_result.navigational_intent(),
omnibox::NAV_INTENT_MEDIUM);
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(u"christopher doe", suggestion_result.suggestion());
ASSERT_EQ(u"American author", suggestion_result.annotation());
ASSERT_EQ("/m/065xxm", suggestion_result.entity_info().entity_id());
ASSERT_EQ("#424242", suggestion_result.entity_info().dominant_color());
ASSERT_EQ("http://example.com/a.png",
suggestion_result.entity_info().image_url());
ASSERT_EQ(suggestion_result.navigational_intent(), omnibox::NAV_INTENT_LOW);
}
{
const auto& navigation_result = results.navigation_results[0];
ASSERT_EQ(GURL(u"http://chr.com"), navigation_result.url());
ASSERT_EQ(navigation_result.navigational_intent(),
omnibox::NAV_INTENT_HIGH);
}
ASSERT_EQ(3U, results.experiment_stats_v2s.size());
{
const auto& experiment_stats_v2 = results.experiment_stats_v2s[0];
ASSERT_EQ(10001, experiment_stats_v2.type_int());
ASSERT_EQ("0:67", experiment_stats_v2.string_value());
}
{
const auto& experiment_stats_v2 = results.experiment_stats_v2s[1];
ASSERT_EQ(10002, experiment_stats_v2.type_int());
ASSERT_EQ("54:67", experiment_stats_v2.string_value());
}
{
const auto& experiment_stats_v2 = results.experiment_stats_v2s[2];
ASSERT_EQ(10003, experiment_stats_v2.type_int());
ASSERT_EQ("0:54", experiment_stats_v2.string_value());
}
ASSERT_EQ(1U, results.gws_event_id_hashes.size());
int64_t expected = -223372036854775808;
ASSERT_EQ(expected, results.gws_event_id_hashes[0]);
}
// Tests that prerender hints can be parsed correctly.
TEST(SearchSuggestionParserTest, ParsePrerenderSuggestion) {
std::string json_data = R"([
"pre",
["prefetch","prerender"],
["", ""],
[],
{
"google:clientdata": {
"pre": 1
}
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"pre", metrics::OmniboxEventProto::BLANK,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(u"prefetch", suggestion_result.suggestion());
EXPECT_FALSE(suggestion_result.should_prerender());
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(u"prerender", suggestion_result.suggestion());
EXPECT_TRUE(suggestion_result.should_prerender());
}
}
// Tests that both prefetch and prerender hints can be parsed correctly.
TEST(SearchSuggestionParserTest, ParseBothPrefetchAndPrerenderSuggestion) {
std::string json_data = R"([
"pre",
["prefetch","prerender"],
["", ""],
[],
{
"google:clientdata": {
"phi": 0,
"pre": 1
}
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"pre", metrics::OmniboxEventProto::BLANK,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(u"prefetch", suggestion_result.suggestion());
EXPECT_FALSE(suggestion_result.should_prerender());
EXPECT_TRUE(suggestion_result.should_prefetch());
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(u"prerender", suggestion_result.suggestion());
EXPECT_TRUE(suggestion_result.should_prerender());
EXPECT_FALSE(suggestion_result.should_prefetch());
}
}
TEST(SearchSuggestionParserTest, SuggestClassification) {
SearchSuggestionParser::SuggestResult result(
u"foobar", AutocompleteMatchType::SEARCH_SUGGEST, omnibox::TYPE_QUERY, {},
false, omnibox::NAV_INTENT_NONE, 400, true, std::u16string());
AutocompleteMatch::ValidateClassifications(result.match_contents(),
result.match_contents_class());
// Nothing should be bolded for ZeroSuggest classified input.
result.ClassifyMatchContents(true, std::u16string());
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, u"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, u"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, u"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, omnibox::TYPE_NAVIGATION, {},
std::u16string(), std::string(), false, omnibox::NAV_INTENT_HIGH, 400,
true, u"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, u"term not found");
EXPECT_EQ(kBoldMiddle, result.match_contents_class());
// Test the allow bold-nothing case too.
result.CalculateAndClassifyMatchContents(true, u"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, std::u16string());
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, ParseSuggestionGroupInfo) {
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
{
omnibox::GroupsInfo groups_info;
auto* group_configs_map = groups_info.mutable_group_configs();
auto& group_config_1 = (*group_configs_map)
[omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS];
group_config_1.set_header_text("Related Entities");
auto& group_config_2 = (*group_configs_map)[omnibox::GROUP_TRENDS];
group_config_2.set_header_text("Trending Searches");
group_config_2.set_visibility(omnibox::GroupConfig_Visibility_HIDDEN);
std::string json_data = R"([
"",
["los angeles", "san diego", "las vegas", "san francisco"],
["", "history", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:groupsinfo": ")" +
SerializeAndEncodeProto(groups_info) + R"(",
"google:suggestdetail":[
{
},
{
"zl":10001
},
{
"zl":10002
},
{
"zl":40000
}
],
"google:suggestrelevance": [607, 606, 605, 604],
"google:suggesttype": ["QUERY", "PERSONALIZED_QUERY", "QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Ensure suggestion groups are correctly parsed from the serialized proto.
ASSERT_EQ(2U, results.suggestion_groups_map.size());
const auto& group_1 =
results.suggestion_groups_map
[omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS];
ASSERT_EQ("Related Entities", group_1.header_text());
ASSERT_EQ(omnibox::GroupConfig_Visibility_DEFAULT_VISIBLE,
group_1.visibility());
ASSERT_EQ(omnibox::SECTION_REMOTE_ZPS_1, group_1.section());
const auto& group_2 = results.suggestion_groups_map[omnibox::GROUP_TRENDS];
ASSERT_EQ("Trending Searches", group_2.header_text());
ASSERT_EQ(omnibox::GroupConfig_Visibility_HIDDEN, group_2.visibility());
ASSERT_EQ(omnibox::SECTION_REMOTE_ZPS_2, group_2.section());
// Ensure suggestion group IDs are correctly set in the suggestions.
ASSERT_EQ(4U, results.suggest_results.size());
ASSERT_EQ(u"los angeles", results.suggest_results[0].suggestion());
// This suggestion does not belong to a group.
ASSERT_EQ(std::nullopt, results.suggest_results[0].suggestion_group_id());
ASSERT_EQ(u"san diego", results.suggest_results[1].suggestion());
ASSERT_EQ(omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS,
*results.suggest_results[1].suggestion_group_id());
ASSERT_EQ(u"las vegas", results.suggest_results[2].suggestion());
ASSERT_EQ(omnibox::GROUP_TRENDS,
*results.suggest_results[2].suggestion_group_id());
ASSERT_EQ(u"san francisco", results.suggest_results[3].suggestion());
ASSERT_EQ(omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST,
results.suggest_results[3].suggestion_group_id());
}
{
omnibox::GroupsInfo groups_info;
auto* group_configs_map = groups_info.mutable_group_configs();
// Group 1
auto& group_config_1 = (*group_configs_map)[omnibox::GROUP_TRENDS];
group_config_1.set_header_text("Trending Searches");
group_config_1.set_visibility(omnibox::GroupConfig_Visibility_HIDDEN);
// Group 2
auto& group_config_2 = (*group_configs_map)
[omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS];
group_config_2.set_header_text("Related Entities");
// Group 3
auto& group_config_3 =
(*group_configs_map)[omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST];
group_config_3.set_header_text("Recent Searches");
// Group 4
auto& group_config_4 =
(*group_configs_map)[static_cast<omnibox::GroupId>(101)];
group_config_4.set_header_text("Unrecognized Suggestions");
std::string json_data = R"([
"",
["los angeles", "san diego", "las vegas", "san francisco", "sacramento"],
["", "history", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:groupsinfo": ")" +
SerializeAndEncodeProto(groups_info) + R"(",
"google:suggestdetail":[
{
},
{
"zl":10002
},
{
"zl":10001
},
{
"zl":102
},
{
"zl":101
}
],
"google:suggestrelevance": [607, 606, 605, 604, 603],
"google:suggesttype": ["QUERY", "QUERY", "QUERY", "QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Ensure group configs are correctly parsed from the serialized proto.
// group configs with invalid or unrecognized group IDs are dropped.
ASSERT_EQ(3U, results.suggestion_groups_map.size());
// Group 1
const auto& group_1 =
results.suggestion_groups_map.at(omnibox::GROUP_TRENDS);
ASSERT_EQ("Trending Searches", group_1.header_text());
ASSERT_EQ(omnibox::GroupConfig_Visibility_HIDDEN, group_1.visibility());
ASSERT_EQ(omnibox::SECTION_REMOTE_ZPS_1, group_1.section());
// Group 2
const auto& group_2 = results.suggestion_groups_map.at(
omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS);
ASSERT_EQ("Related Entities", group_2.header_text());
ASSERT_EQ(omnibox::GroupConfig_Visibility_DEFAULT_VISIBLE,
group_2.visibility());
ASSERT_EQ(omnibox::SECTION_REMOTE_ZPS_2, group_2.section());
// Group 3
const auto& group_3 = results.suggestion_groups_map.at(
omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST);
ASSERT_EQ("Recent Searches", group_3.header_text());
ASSERT_EQ(omnibox::SECTION_REMOTE_ZPS_3, group_3.section());
// Ensure suggestion group IDs are correctly set in the suggestions.
ASSERT_EQ(5U, results.suggest_results.size());
ASSERT_EQ(u"los angeles", results.suggest_results[0].suggestion());
// This suggestion does not belong to a group.
ASSERT_EQ(std::nullopt, results.suggest_results[0].suggestion_group_id());
ASSERT_EQ(u"san diego", results.suggest_results[1].suggestion());
ASSERT_EQ(omnibox::GROUP_TRENDS,
*results.suggest_results[1].suggestion_group_id());
ASSERT_EQ(u"las vegas", results.suggest_results[2].suggestion());
ASSERT_EQ(omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS,
*results.suggest_results[2].suggestion_group_id());
ASSERT_EQ(u"san francisco", results.suggest_results[3].suggestion());
// This suggestion belongs to an unrecognized group.
ASSERT_EQ(omnibox::GROUP_INVALID,
results.suggest_results[3].suggestion_group_id());
ASSERT_EQ(u"sacramento", results.suggest_results[4].suggestion());
// This suggestion belongs to an unrecognized group.
ASSERT_EQ(omnibox::GROUP_INVALID,
results.suggest_results[4].suggestion_group_id());
}
}
TEST(SearchSuggestionParserTest, ParseSuggestionEntityInfo) {
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"the m", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
// Parse EntityInfo data from properly encoded (base64) proto field.
{
omnibox::EntityInfo first_entity_info;
first_entity_info.set_annotation("2022 film");
first_entity_info.set_dominant_color("#424242");
first_entity_info.set_image_url(
"https://encrypted-tbn0.gstatic.com/"
"images?q=the+menu");
first_entity_info.set_suggest_search_parameters(
"gs_ssp=eJzj4tVP1zc0LCwoKssryyg3YPTiKMlIVchNzSsFAGrSCGQ");
first_entity_info.set_name("The Menu");
first_entity_info.set_entity_id("/g/11qprvnvhw");
omnibox::EntityInfo second_entity_info;
second_entity_info.set_annotation("Thriller series");
second_entity_info.set_dominant_color("#283e75");
second_entity_info.set_image_url(
"https://encrypted-tbn0.gstatic.com/"
"images?q=the+midnight+club");
second_entity_info.set_suggest_search_parameters(
"gs_ssp=eJzj4tVP1zc0zMqrNCvJNkwyYPQSLMlIVcjNTMnLTM8oUUjOKU0CALmyCz8");
second_entity_info.set_name("The Midnight Club");
second_entity_info.set_entity_id("/g/11jny6tk1b");
std::string json_data = R"([
"the m",
["the menu", "the menu", "the midnight club"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{},
{
"google:entityinfo": ")" +
SerializeAndEncodeProto(first_entity_info) +
R"("
},
{
"google:entityinfo": ")" +
SerializeAndEncodeProto(second_entity_info) +
R"("
}
],
"google:suggestrelevance": [701, 700, 553],
"google:suggestsubtypes": [
[512, 433, 131, 355],
[131, 433, 512],
[512, 433]
],
"google:suggesttype": ["QUERY", "ENTITY", "ENTITY"],
"google:verbatimrelevance": 851
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(3U, results.suggest_results.size());
// For each suggestion, verify that the JSON fields were correctly parsed.
ASSERT_EQ(u"the menu", results.suggest_results[0].suggestion());
ASSERT_EQ(u"", results.suggest_results[0].annotation());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[0].entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_TRUE(results.suggest_results[0].entity_info().image_url().empty());
// Empty "t" value from server results in suggestion being used instead.
ASSERT_EQ(u"the menu", results.suggest_results[0].match_contents());
ASSERT_EQ(u"the menu", results.suggest_results[1].suggestion());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[1].entity_info(),
first_entity_info));
ASSERT_EQ(u"the midnight club", results.suggest_results[2].suggestion());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[2].entity_info(),
second_entity_info));
}
// Parse EntityInfo data from garbled proto field.
{
std::string json_data = R"([
"the m",
["the menu", "the menu", "the midnight club"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{},
{
"google:entityinfo": "<< invalid format >>"
},
{
"google:entityinfo": "<< invalid format >>"
}
],
"google:suggestrelevance": [701, 700, 553],
"google:suggestsubtypes": [
[512, 433, 131, 355],
[131, 433, 512],
[512, 433]
],
"google:suggesttype": ["QUERY", "ENTITY", "ENTITY"],
"google:verbatimrelevance": 851
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(3U, results.suggest_results.size());
// For each suggestion, verify that the JSON fields were correctly parsed.
ASSERT_EQ(u"the menu", results.suggest_results[0].suggestion());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[0].entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_EQ(u"", results.suggest_results[0].annotation());
// Empty "t" value from server results in suggestion being used instead.
ASSERT_EQ(u"the menu", results.suggest_results[0].match_contents());
ASSERT_EQ(u"the menu", results.suggest_results[1].suggestion());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[1].entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_EQ(u"the midnight club", results.suggest_results[2].suggestion());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[2].entity_info(),
omnibox::EntityInfo::default_instance()));
}
}
TEST(SearchSuggestionParserTest, ParseSuggestionTemplateInfo) {
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::SuggestionAnswerMigration>
scoped_config;
scoped_config.Get().enabled = true;
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"weather los",
metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
// Test behavior with template present; template is set from decoding
// "google:templateinfo" field.
{
// Setup RichAnswerTemplate with answer data.
omnibox::RichSuggestTemplate suggest_template;
omnibox::RichAnswerTemplate* answer_template =
suggest_template.mutable_rich_answer_template();
omnibox::AnswerData* answer_data = answer_template->add_answers();
answer_data->mutable_headline()->set_text("weather los angeles");
answer_data->mutable_subhead()->set_text("68F Fri - Los Angeles, CA");
answer_data->mutable_image()->set_url("//www.gstatic.com/images/image.png");
std::string json_data =
R"([
"weather los",
["weather los angeles", "weather los angeles ca", "weather los alamitos"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"ansa": {
"l": [{"il": {"t": [{"t": "weather new york", "tt": 8}]}},
{"il": {"at": {"t": "Fri - New York, NY","tt": 19},
"i": {"d": "//www.gstatic.com/images/image.png", "t": 3},
"t": [{"t": "50F", "tt": 18}]}}]
},
"ansb": "8",
"google:templateinfo": ")" +
SerializeAndEncodeProto(suggest_template) +
R"("
},
{},
{}
],
"google:suggestrelevance": [1252, 1251, 1250],
"google:suggestsubtypes": [
[512, 433],
[512],
[512]
],
"google:suggesttype": ["QUERY", "QUERY", "QUERY"],
"google:verbatimrelevance": 851
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Ensure the correct suggestion has RichAnswerTemplate info and is
// correctly parsed.
ASSERT_EQ(3U, results.suggest_results.size());
ASSERT_EQ(results.suggest_results[0].answer_type(),
omnibox::ANSWER_TYPE_WEATHER);
ASSERT_EQ(results.suggest_results[1].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[2].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_TRUE(results.suggest_results[0].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[1].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[2].answer_template().has_value());
// Protos should initially not be equal because there is formatting done to
// a template's URL after decoding "google:templateinfo".
ASSERT_FALSE(
ProtosAreEqual(results.suggest_results[0].answer_template().value(),
*answer_template));
// Change `answer_data` image URL to formatted version to reflect formatting
// done when parsing results. Now the protos should be equal.
answer_data->mutable_image()->set_url(
"https://www.gstatic.com/images/image.png");
ASSERT_TRUE(
ProtosAreEqual(results.suggest_results[0].answer_template().value(),
*answer_template));
}
// Test behavior with no template present; template is set from parsing "ansa"
// JSON field.
{
std::string json_data = R"([
"weather los",
["weather los angeles", "weather los angeles ca", "weather los alamitos"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"ansa": {
"l": [{"il": {"t": [{"t": "weather los angeles", "tt": 8}]}},
{"il": {"at": {"t": "Fri - Los Angeles, CA","tt": 19},
"i": {"d": "//www.gstatic.com/images/image.png", "t": 3},
"t": [{"t": "68F", "tt": 18}]}}]
},
"ansb": "8"
},
{},
{}
],
"google:suggestrelevance": [1300, 602, 601],
"google:suggestsubtypes": [
[512, 433, 131, 457],
[512,402],
[512,402]
],
"google:suggesttype": ["QUERY", "QUERY", "QUERY"],
"google:verbatimrelevance": 1300
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Ensure the correct suggestion has RichAnswerTemplate info and is
// correctly parsed.
ASSERT_EQ(3U, results.suggest_results.size());
ASSERT_EQ(results.suggest_results[0].answer_type(),
omnibox::ANSWER_TYPE_WEATHER);
ASSERT_EQ(results.suggest_results[1].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[2].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_TRUE(results.suggest_results[0].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[1].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[2].answer_template().has_value());
omnibox::AnswerData answer_data =
results.suggest_results[0].answer_template()->answers(0);
// The first image line in "ansa" is equivalent to AnswerData's headline and
// second image line is equivalent to subhead.
EXPECT_EQ(answer_data.headline().text(), "weather los angeles");
EXPECT_EQ(answer_data.subhead().text(), "68F Fri - Los Angeles, CA");
EXPECT_EQ(answer_data.image().url(),
"https://www.gstatic.com/images/image.png");
}
{
// Fallback to JSON parsing when decoding RichAnswerTemplate fails.
std::string json_data = R"([
"weather los",
["weather los angeles", "weather los angeles ca", "weather los alamitos"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"ansa": {
"l": [{"il": {"t": [{"t": "weather los angeles", "tt": 8}]}},
{"il": {"at": {"t": "Fri - Los Angeles, CA","tt": 19},
"i": {"d": "//www.gstatic.com/images/image.png", "t": 3},
"t": [{"t": "68F", "tt": 18}]}}]
},
"ansb": "8",
"google:templateinfo": "<< invalid format >>"
},
{},
{}
],
"google:suggestrelevance": [1300, 602, 601],
"google:suggestsubtypes": [
[512, 433, 131, 457],
[512,402],
[512,402]
],
"google:suggesttype": ["QUERY", "QUERY", "QUERY"],
"google:verbatimrelevance": 1300
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Ensure the correct suggestion has RichAnswerTemplate info and is
// correctly parsed.
ASSERT_EQ(3U, results.suggest_results.size());
ASSERT_EQ(results.suggest_results[0].answer_type(),
omnibox::ANSWER_TYPE_WEATHER);
ASSERT_EQ(results.suggest_results[1].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[2].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_TRUE(results.suggest_results[0].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[1].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[2].answer_template().has_value());
omnibox::AnswerData answer_data =
results.suggest_results[0].answer_template()->answers(0);
// The first image line in "ansa" is equivalent to AnswerData's headline and
// second image line is equivalent to subhead.
EXPECT_EQ(answer_data.headline().text(), "weather los angeles");
EXPECT_EQ(answer_data.subhead().text(), "68F Fri - Los Angeles, CA");
EXPECT_EQ(answer_data.image().url(),
"https://www.gstatic.com/images/image.png");
}
// Test behavior with template present but has no answers.
{
// Setup RichAnswerTemplate.
omnibox::RichSuggestTemplate suggest_template;
omnibox::RichAnswerTemplate* answer_template =
suggest_template.mutable_rich_answer_template();
ASSERT_TRUE(answer_template->answers_size() == 0);
std::string json_data =
R"([
"weather los",
["weather los angeles", "weather los angeles ca", "weather los alamitos"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"ansb": "8",
"google:templateinfo": ")" +
SerializeAndEncodeProto(suggest_template) +
R"("
},
{},
{}
],
"google:suggestrelevance": [1300, 602, 601],
"google:suggestsubtypes": [
[512, 433, 131, 457],
[512,402],
[512,402]
],
"google:suggesttype": ["QUERY", "QUERY", "QUERY"],
"google:verbatimrelevance": 1300
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Results do not have a RichAnswerTemplate populated because of the lack of
// answers.
ASSERT_EQ(3U, results.suggest_results.size());
ASSERT_EQ(results.suggest_results[0].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[1].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[2].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_FALSE(results.suggest_results[0].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[1].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[2].answer_template().has_value());
}
// Test behavior when template is present but answer type is invalid.
{
// Setup RichAnswerTemplate with answer data.
omnibox::RichSuggestTemplate suggest_template;
omnibox::RichAnswerTemplate* answer_template =
suggest_template.mutable_rich_answer_template();
omnibox::AnswerData* answer_data = answer_template->add_answers();
answer_data->mutable_headline()->set_text("weather los angeles");
answer_data->mutable_subhead()->set_text("68F Fri - Los Angeles, CA");
answer_data->mutable_image()->set_url("//www.gstatic.com/images/image.png");
std::string json_data =
R"([
"weather los",
["weather los angeles", "weather los angeles ca", "weather los alamitos"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"ansa": {
"l": [{"il": {"t": [{"t": "weather los angeles", "tt": 8}]}},
{"il": {"at": {"t": "Fri - Los Angeles, CA","tt": 19},
"i": {"d": "//www.gstatic.com/images/image.png", "t": 3},
"t": [{"t": "68F", "tt": 18}]}}]
},
"ansb": "20",
"google:templateinfo": ")" +
SerializeAndEncodeProto(suggest_template) +
R"("
},
{},
{}
],
"google:suggestrelevance": [1252, 1251, 1250],
"google:suggestsubtypes": [
[512, 433],
[512],
[512]
],
"google:suggesttype": ["QUERY", "QUERY", "QUERY"],
"google:verbatimrelevance": 851
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Results should not have RichAnswerTemplate populated.
ASSERT_EQ(3U, results.suggest_results.size());
ASSERT_EQ(results.suggest_results[0].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[1].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[2].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_FALSE(results.suggest_results[0].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[1].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[2].answer_template().has_value());
}
}
TEST(SearchSuggestionParserTest, ParseSuggestionTemplateInfoCounterfactual) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{{omnibox::kOmniboxAnswerActions,
{{OmniboxFieldTrial::kAnswerActionsCounterfactual.name, "true"}}}},
/*disabled_features=*/{});
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::SuggestionAnswerMigration>
scoped_config;
scoped_config.Get().enabled = true;
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"weather los",
metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
{
// Setup RichAnswerTemplate with answer data.
omnibox::RichSuggestTemplate suggest_template;
omnibox::RichAnswerTemplate* answer_template =
suggest_template.mutable_rich_answer_template();
omnibox::AnswerData* answer_data = answer_template->add_answers();
answer_data->mutable_headline()->set_text("weather los angeles");
answer_data->mutable_subhead()->set_text("68F Fri - Los Angeles, CA");
answer_data->mutable_image()->set_url("//www.gstatic.com/images/image.png");
answer_template->mutable_enhancements()
->add_enhancements()
->set_display_text("7 day forecast");
std::string json_data =
R"([
"weather los",
["weather los angeles", "weather los angeles ca", "weather los alamitos"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"ansa": {
"l": [{"il": {"t": [{"t": "weather new york", "tt": 8}]}},
{"il": {"at": {"t": "Fri - New York, NY","tt": 19},
"i": {"d": "//www.gstatic.com/images/image.png", "t": 3},
"t": [{"t": "50F", "tt": 18}]}}]
},
"ansb": "8",
"google:templateinfo": ")" +
SerializeAndEncodeProto(suggest_template) +
R"("
},
{},
{}
],
"google:suggestrelevance": [1252, 1251, 1250],
"google:suggestsubtypes": [
[512, 433],
[512],
[512]
],
"google:suggesttype": ["QUERY", "QUERY", "QUERY"],
"google:verbatimrelevance": 851
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
// Ensure the correct suggestion has RichAnswerTemplate info and is
// correctly parsed.
ASSERT_EQ(3U, results.suggest_results.size());
ASSERT_EQ(results.suggest_results[0].answer_type(),
omnibox::ANSWER_TYPE_WEATHER);
ASSERT_EQ(results.suggest_results[1].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_EQ(results.suggest_results[2].answer_type(),
omnibox::ANSWER_TYPE_UNSPECIFIED);
ASSERT_TRUE(results.suggest_results[0].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[1].answer_template().has_value());
ASSERT_FALSE(results.suggest_results[2].answer_template().has_value());
omnibox::AnswerData parsed_answer_data =
results.suggest_results[0].answer_template()->answers(0);
// The first image line in "ansa" is equivalent to AnswerData's headline and
// second image line is equivalent to subhead.
EXPECT_EQ(parsed_answer_data.headline().text(), "weather new york");
EXPECT_EQ(parsed_answer_data.subhead().text(), "50F Fri - New York, NY");
EXPECT_EQ(parsed_answer_data.image().url(),
"https://www.gstatic.com/images/image.png");
ASSERT_TRUE(results.suggest_results[0]
.answer_template()
->enhancements()
.enhancements()
.empty());
}
}
TEST(SearchSuggestionParserTest, ParseValidTypes) {
std::string json_data = R"([
"",
["one", "two", "three", "four", "five"],
["", "", "", "", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": [[], [], [], [], []],
"google:suggestrelevance": [607, 606, 605, 604, 603, 602],
"google:suggesttype": ["QUERY", "ENTITY", "CATEGORICAL_QUERY", 1, "UNKNOWN"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(5u, results.suggest_results.size());
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(u"one", suggestion_result.suggestion());
ASSERT_EQ(AutocompleteMatchType::SEARCH_SUGGEST, suggestion_result.type());
ASSERT_EQ(omnibox::TYPE_QUERY, suggestion_result.suggest_type());
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(u"two", suggestion_result.suggestion());
ASSERT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_ENTITY,
suggestion_result.type());
ASSERT_EQ(omnibox::TYPE_ENTITY, suggestion_result.suggest_type());
}
{
const auto& suggestion_result = results.suggest_results[2];
ASSERT_EQ(u"three", suggestion_result.suggestion());
ASSERT_EQ(base::FeatureList::IsEnabled(omnibox::kCategoricalSuggestions)
? AutocompleteMatchType::SEARCH_SUGGEST_ENTITY
: AutocompleteMatchType::SEARCH_SUGGEST,
suggestion_result.type());
ASSERT_EQ(omnibox::TYPE_CATEGORICAL_QUERY,
suggestion_result.suggest_type());
}
{
const auto& suggestion_result = results.suggest_results[3];
ASSERT_EQ(u"four", suggestion_result.suggestion());
ASSERT_EQ(AutocompleteMatchType::SEARCH_SUGGEST, suggestion_result.type());
ASSERT_EQ(omnibox::TYPE_QUERY, suggestion_result.suggest_type());
}
{
const auto& suggestion_result = results.suggest_results[4];
ASSERT_EQ(u"five", suggestion_result.suggestion());
ASSERT_EQ(AutocompleteMatchType::SEARCH_SUGGEST, suggestion_result.type());
ASSERT_EQ(omnibox::TYPE_QUERY, suggestion_result.suggest_type());
}
}
TEST(SearchSuggestionParserTest, ParseValidSubtypes) {
std::string json_data = R"([
"",
["one", "two", "three", "four"],
["", "", "", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": [[1], [21, 22], [31, 32, 33], [44]],
"google:suggestrelevance": [607, 606, 605, 604],
"google:suggesttype": ["QUERY", "QUERY", "QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
{
const auto& suggestion_result = results.suggest_results[0];
ASSERT_EQ(u"one", suggestion_result.suggestion());
ASSERT_THAT(suggestion_result.subtypes(), testing::ElementsAre(1));
}
{
const auto& suggestion_result = results.suggest_results[1];
ASSERT_EQ(u"two", suggestion_result.suggestion());
ASSERT_THAT(suggestion_result.subtypes(), testing::ElementsAre(21, 22));
}
{
const auto& suggestion_result = results.suggest_results[2];
ASSERT_EQ(u"three", suggestion_result.suggestion());
ASSERT_THAT(suggestion_result.subtypes(), testing::ElementsAre(31, 32, 33));
}
{
const auto& suggestion_result = results.suggest_results[3];
ASSERT_EQ(u"four", suggestion_result.suggestion());
ASSERT_THAT(suggestion_result.subtypes(), testing::ElementsAre(44));
}
}
TEST(SearchSuggestionParserTest, IgnoresExcessiveSubtypeEntries) {
using testing::ElementsAre;
std::string json_data = R"([
"",
["one", "two"],
["", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": [[1], [2], [3]],
"google:suggestrelevance": [607, 606],
"google:suggesttype": ["QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_THAT(results.suggest_results[0].subtypes(), testing::ElementsAre(1));
ASSERT_THAT(results.suggest_results[1].subtypes(), testing::ElementsAre(2));
}
TEST(SearchSuggestionParserTest, IgnoresMissingSubtypeEntries) {
using testing::ElementsAre;
std::string json_data = R"([
"",
["one", "two", "three"],
["", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": [[1, 7]],
"google:suggestrelevance": [607, 606],
"google:suggesttype": ["QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_THAT(results.suggest_results[0].subtypes(),
testing::ElementsAre(1, 7));
ASSERT_TRUE(results.suggest_results[1].subtypes().empty());
ASSERT_TRUE(results.suggest_results[2].subtypes().empty());
}
TEST(SearchSuggestionParserTest, IgnoresUnexpectedSubtypeValues) {
using testing::ElementsAre;
std::string json_data = R"([
"",
["one", "two", "three", "four", "five"],
["", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": [[1, { "a":true} ], ["2", 7], 3, {}, [12]],
"google:suggestrelevance": [607, 606, 605, 604, 603],
"google:suggesttype": ["QUERY", "QUERY", "QUERY", "QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_THAT(results.suggest_results[0].subtypes(), testing::ElementsAre(1));
ASSERT_THAT(results.suggest_results[1].subtypes(), testing::ElementsAre(7));
ASSERT_TRUE(results.suggest_results[2].subtypes().empty());
ASSERT_TRUE(results.suggest_results[3].subtypes().empty());
ASSERT_THAT(results.suggest_results[4].subtypes(), testing::ElementsAre(12));
}
TEST(SearchSuggestionParserTest, IgnoresSubtypesIfNotAList) {
using testing::ElementsAre;
std::string json_data = R"([
"",
["one", "two"],
["", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": { "a": 1, "b": 2 },
"google:suggestrelevance": [607, 606],
"google:suggesttype": ["QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_TRUE(results.suggest_results[0].subtypes().empty());
ASSERT_TRUE(results.suggest_results[1].subtypes().empty());
}
TEST(SearchSuggestionParserTest, SubtypesWithEmptyArraysAreValid) {
using testing::ElementsAre;
std::string json_data = R"([
"",
["one", "two"],
["", ""],
[],
{
"google:clientdata": { "bpc": false, "tlw": false },
"google:suggestsubtypes": [[], [3]],
"google:suggestrelevance": [607, 606],
"google:suggesttype": ["QUERY", "QUERY"]
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_TRUE(results.suggest_results[0].subtypes().empty());
ASSERT_THAT(results.suggest_results[1].subtypes(), testing::ElementsAre(3));
}
TEST(SearchSuggestionParserTest, BadAnswersFailGracefully) {
// clang-format off
std::vector<std::string> cases = {
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":{"l":[{"il":{"t":[{"t":"w","tt":4}]}},{"il":{"i":"","t":[{"t":"3","tt":1}]}}]},"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":{"l":[{"il":{"t":[]}},{"il":{"i":"","t":[[]]}}]},"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":{"l":[{"il":{"t":[]}},{"il":{"i":"","t":[[0]]}}]},"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":{"l":[{"il":{"t":[]}},{"il":{"i":"","t":[""]}}]},"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":{"l":[{"il":{"t":[{"t":"w","tt":4}]}},{"il":{"i":"","t":[{"t":"3","tt":1}]}}]},"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":[],"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":{},"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":0,"ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":"","ansb":"0"}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":"","ansb":{}}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":"","ansb":0}]}])",
R"(["",[""],[],[],{"google:suggestdetail":[{"ansa":"","ansb":[]}]}])",
R"(["",[""],[],[],{"google:suggestdetail":""}])",
R"(["",[""],[],[],{"google:suggestdetail":0}])",
R"(["",[""],[],[],{"google:suggestdetail":{}}])",
};
// clang-format on
auto test = [](std::vector<std::string> cases) {
for (std::string json_data : cases) {
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
}
};
{
SCOPED_TRACE(
"Attempting to parse suggest results and populate SuggestionAnswer");
test(cases);
}
{
SCOPED_TRACE(
"Attempting to parse suggest results and populate RichAnswerTemplate");
// Test with kOmniboxSuggestionAnswerMigration, which will attempt to
// populate omnibox::RichAnswerTemplate instead of SuggestionAnswer.
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::SuggestionAnswerMigration>
scoped_config;
scoped_config.Get().enabled = true;
test(cases);
}
}
TEST(SearchSuggestionParserTest, ParseCalculatorSuggestion) {
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"1 + 1", metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
omnibox::EntityInfo entity_info;
entity_info.set_annotation("Song");
entity_info.set_dominant_color("#424242");
entity_info.set_image_url("https://encrypted-tbn0.gstatic.com/images?q=song");
entity_info.set_suggest_search_parameters(
"gs_ssp=eJzj4tFP1zcsNjAzMykwKDZg9GI1VNBWMAQAOlEEsA");
entity_info.set_name("1+1");
entity_info.set_entity_id("/g/1s0664p0s");
const std::string json_data = R"([
"1 + 1",
[
"1 + 1",
"= 2",
"1 + 1"
],
["", "Calculator", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{},
{},
{
"google:entityinfo": ")" +
SerializeAndEncodeProto(entity_info) +
R"("
}
],
"google:suggestrelevance": [1300, 1252, 1250],
"google:suggestsubtypes": [
[512, 355],
[],
[512]
],
"google:suggesttype": [
"QUERY",
"CALCULATOR",
"ENTITY"
],
"google:verbatimrelevance": 1300
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(3U, results.suggest_results.size());
// Most fields for a verbatim suggestion should be empty.
ASSERT_EQ(u"1 + 1", results.suggest_results[0].suggestion());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[0].entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_EQ(u"", results.suggest_results[0].annotation());
ASSERT_EQ(u"1 + 1", results.suggest_results[0].match_contents());
// Calculator suggestions should have specific values for the |suggestion|,
// |match_contents|, and |annotation| fields.
#if !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
ASSERT_EQ(u"2", results.suggest_results[1].suggestion());
ASSERT_EQ(u"", results.suggest_results[1].annotation());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[1].entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_EQ(u"1 + 1 = 2", results.suggest_results[1].match_contents());
#else
ASSERT_EQ(u"2", results.suggest_results[1].suggestion());
ASSERT_EQ(u"", results.suggest_results[1].annotation());
ASSERT_TRUE(ProtosAreEqual(results.suggest_results[1].entity_info(),
omnibox::EntityInfo::default_instance()));
ASSERT_EQ(u"= 2", results.suggest_results[1].match_contents());
#endif
// Entity data should be correctly sourced as usual.
ASSERT_EQ(u"1 + 1", results.suggest_results[2].suggestion());
ASSERT_EQ(u"Song", results.suggest_results[2].annotation());
ASSERT_EQ("#424242",
results.suggest_results[2].entity_info().dominant_color());
ASSERT_EQ("https://encrypted-tbn0.gstatic.com/images?q=song",
results.suggest_results[2].entity_info().image_url());
ASSERT_EQ(
"gs_ssp=eJzj4tFP1zcsNjAzMykwKDZg9GI1VNBWMAQAOlEEsA",
results.suggest_results[2].entity_info().suggest_search_parameters());
ASSERT_EQ(u"1+1", results.suggest_results[2].match_contents());
ASSERT_EQ("/g/1s0664p0s",
results.suggest_results[2].entity_info().entity_id());
}
TEST(SearchSuggestionParserTest, ParseTailSuggestion) {
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(u"hobbit hole for sale in ",
metrics::OmniboxEventProto::NTP_REALBOX,
scheme_classifier);
const std::string json_data = R"([
"hobbit hole for sale in ",
[
"hobbit hole for sale in california"
],
[
""
],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{
"mp": "… ",
"t": "in california"
}
],
"google:suggestrelevance": [
601
],
"google:suggestsubtypes": [
[
160
]
],
"google:suggesttype": [
"TAIL"
],
"google:verbatimrelevance": 851
}
])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(1U, results.suggest_results.size());
ASSERT_EQ(u"hobbit hole for sale in california",
results.suggest_results[0].suggestion());
ASSERT_EQ(u"in california", results.suggest_results[0].match_contents());
ASSERT_EQ(u"… ", results.suggest_results[0].match_contents_prefix());
}
TEST(SearchSuggestionParserTest, ParseSuggestTemplateFromSuggestResults) {
TestSchemeClassifier scheme_classifier;
AutocompleteInput input(
u"",
metrics::OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS,
scheme_classifier);
// Parse SuggestTemplateInfo data from properly encoded (base64) proto field.
{
omnibox::SuggestTemplateInfo suggest_template_info;
suggest_template_info.set_style(omnibox::SuggestTemplateInfo::DEFAULT);
suggest_template_info.set_type_icon(
omnibox::SuggestTemplateInfo_IconType_SEARCH_LOOP_WITH_SPARKLE);
suggest_template_info.mutable_primary_text()->set_text(
"Washington Wizards");
suggest_template_info.mutable_secondary_text()->set_text("MIA");
omnibox::SuggestTemplateInfo::Image* image =
suggest_template_info.mutable_image();
image->set_url("http://example.com/a.png");
image->set_dominant_color("#233875");
image->set_type(omnibox::SuggestTemplateInfo::Image::TYPE_LARGE);
(*suggest_template_info.mutable_default_search_parameters())["gs_ssp"] =
"abc";
omnibox::SuggestTemplateInfo::TemplateAction* template_action =
suggest_template_info.add_action_suggestions();
template_action->set_action_uri("chrome://newtab/");
template_action->set_action_type(
omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS);
template_action->set_display_text("New Tab");
(*template_action->mutable_search_parameters())["action_param1"] =
"action_value1";
std::string json_data = base::StrCat({
R"([
"",
["the menu", "washington wizards", "the midnight club"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{},
{
"google:suggesttemplate": ")",
SerializeAndEncodeProto(suggest_template_info),
R"("
},
{}
],
"google:suggestrelevance": [701, 700, 553],
"google:suggestsubtypes": [
[512, 433, 131, 355],
[131, 433, 512],
[512, 433]
],
"google:suggesttype": ["QUERY", "ENTITY", "QUERY"],
"google:verbatimrelevance": 851
}])"});
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(3U, results.suggest_results.size());
// For each suggestion, verify that the JSON fields were correctly parsed.
// The first suggestion suggest template should not be populated.
ASSERT_EQ(u"the menu", results.suggest_results[0].suggestion());
ASSERT_EQ(u"", results.suggest_results[0].annotation());
ASSERT_FALSE(
results.suggest_results[0].suggest_template_info().has_value());
ASSERT_EQ(u"washington wizards", results.suggest_results[1].suggestion());
ASSERT_TRUE(ProtosAreEqual(
results.suggest_results[1].suggest_template_info().value(),
suggest_template_info));
// SuggestResult contents and annotation should be updated to reflect that
// of the SuggestTemplateInfo.
ASSERT_EQ(u"Washington Wizards",
results.suggest_results[1].match_contents());
ASSERT_EQ(u"MIA", results.suggest_results[1].annotation());
}
// Parse EntityInfo data from garbled proto field.
{
std::string json_data = R"([
"",
["the menu", "the menu", "the midnight club"],
["", "", ""],
[],
{
"google:clientdata": {
"bpc": false,
"tlw": false
},
"google:suggestdetail": [
{},
{
"google:suggesttemplate": "<< invalid format >>"
},
{}
],
"google:suggestrelevance": [701, 700, 553],
"google:suggestsubtypes": [
[512, 433, 131, 355],
[131, 433, 512],
[512, 433]
],
"google:suggesttype": ["QUERY", "ENTITY", "QUERY"],
"google:verbatimrelevance": 851
}])";
std::optional<base::Value> root_val = base::JSONReader::Read(json_data);
ASSERT_TRUE(root_val);
ASSERT_TRUE(root_val.value().is_list());
SearchSuggestionParser::Results results;
ASSERT_TRUE(SearchSuggestionParser::ParseSuggestResults(
root_val->GetList(), input, scheme_classifier,
/*default_result_relevance=*/400,
/*is_keyword_result=*/false, &results));
ASSERT_EQ(3U, results.suggest_results.size());
// For each suggestion, verify that the JSON fields were correctly parsed.
ASSERT_EQ(u"the menu", results.suggest_results[0].suggestion());
ASSERT_FALSE(
results.suggest_results[0].suggest_template_info().has_value());
// SuggestTemplateInfo should not be populated if it was not parsed
// correctly.
ASSERT_EQ(u"the menu", results.suggest_results[1].suggestion());
ASSERT_FALSE(
results.suggest_results[0].suggest_template_info().has_value());
}
}