blob: 53a963b1a1d983459b1bdf2a6ba3eefc96edb4c3 [file] [log] [blame] [edit]
// Copyright 2014 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/autocomplete_result.h"
#include <stddef.h>
#include <algorithm>
#include <array>
#include <iterator>
#include <memory>
#include <string>
#include <vector>
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gtest_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/omnibox/browser/actions/omnibox_action_in_suggest.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/fake_autocomplete_provider.h"
#include "components/omnibox/browser/fake_autocomplete_provider_client.h"
#include "components/omnibox/browser/fake_tab_matcher.h"
#include "components/omnibox/browser/intranet_redirector_state.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/browser/tab_matcher.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 "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/search_engines/search_engines_test_environment.h"
#include "components/search_engines/template_url_service.h"
#include "components/variations/variations_associated_data.h"
#include "omnibox_focus_type.pb.h"
#include "omnibox_triggered_feature_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
#include "third_party/omnibox_proto/groups.pb.h"
#include "third_party/omnibox_proto/suggest_template_info.pb.h"
#include "third_party/omnibox_proto/types.pb.h"
#include "ui/base/device_form_factor.h"
using metrics::OmniboxEventProto;
namespace {
class FakeOmniboxAction : public OmniboxAction {
public:
explicit FakeOmniboxAction(OmniboxActionId id)
: OmniboxAction(LabelStrings(u"", u"", u"", u""), GURL{}), id_(id) {}
OmniboxActionId ActionId() const override { return id_; }
private:
~FakeOmniboxAction() override = default;
OmniboxActionId id_{};
};
struct AutocompleteMatchTestData {
std::string destination_url;
AutocompleteMatch::Type type;
};
// Adds |count| AutocompleteMatches to |matches|.
template <typename T>
void PopulateAutocompleteMatchesFromTestData(const T* data,
size_t count,
ACMatches* matches) {
static_assert(std::is_base_of<AutocompleteMatchTestData, T>::value,
"T must derive from AutocompleteMatchTestData");
ASSERT_TRUE(matches != nullptr);
for (size_t i = 0; i < count; ++i) {
AutocompleteMatch match;
match.destination_url = GURL(UNSAFE_TODO(data[i]).destination_url);
match.relevance =
matches->empty() ? 1300 : (matches->back().relevance - 100);
match.allowed_to_be_default_match = true;
match.type = UNSAFE_TODO(data[i]).type;
matches->push_back(match);
}
}
// Basic match representation for testing `MaybeCullTailSuggestions()`.
// Defined externally to allow for `PrintTo()`.
struct CullTailTestMatch {
std::u16string id;
AutocompleteMatchType::Type type;
bool allowed_default;
bool operator==(const CullTailTestMatch& other) const {
return id == other.id && type == other.type &&
allowed_default == other.allowed_default;
}
// To help `EXPECT_THAT` pretty print `CullTailTestMatch`s.
friend void PrintTo(const CullTailTestMatch& match, std::ostream* os) {
*os << match.id << " " << match.type << " " << match.allowed_default;
}
};
} // namespace
class AutocompleteResultForTesting : public AutocompleteResult {
public:
using AutocompleteResult::DemoteOnDeviceSearchSuggestions;
using AutocompleteResult::matches_;
using AutocompleteResult::MaybeCullTailSuggestions;
};
class AutocompleteResultTest : public testing::Test {
public:
struct TestData {
// Used to build a URL for the AutocompleteMatch. The URL becomes
// "http://" + ('a' + |url_id|) (e.g. an ID of 2 yields "http://c").
int url_id;
// ID of the provider.
int provider_id;
// Relevance score.
int relevance;
// Allowed to be default match status.
bool allowed_to_be_default_match;
// Duplicate matches.
std::vector<AutocompleteMatch> duplicate_matches;
// Type of the match
AutocompleteMatchType::Type type{AutocompleteMatchType::SEARCH_SUGGEST};
// Suggestion Group ID for this suggestion
std::optional<omnibox::GroupId> suggestion_group_id;
// Inline autocompletion.
std::string inline_autocompletion;
IphType iph_type = IphType::kNone;
};
AutocompleteResultTest() {
variations::testing::ClearAllVariationParams();
// Create the list of mock providers. 6 is enough.
mock_provider_list_.push_back(new FakeAutocompleteProvider(
AutocompleteProvider::Type::TYPE_HISTORY_QUICK));
mock_provider_list_.push_back(new FakeAutocompleteProvider(
AutocompleteProvider::Type::TYPE_HISTORY_URL));
mock_provider_list_.push_back(
new FakeAutocompleteProvider(AutocompleteProvider::Type::TYPE_SEARCH));
mock_provider_list_.push_back(new FakeAutocompleteProvider(
AutocompleteProvider::Type::TYPE_ON_DEVICE_HEAD));
mock_provider_list_.push_back(new FakeAutocompleteProvider(
AutocompleteProvider::Type::TYPE_FEATURED_SEARCH));
mock_provider_list_.push_back(new FakeAutocompleteProvider(
AutocompleteProvider::Type::TYPE_UNSCOPED_EXTENSION));
for (const auto& provider : mock_provider_list_)
provider->done_ = false;
template_url_service().Load();
}
AutocompleteResultTest(const AutocompleteResultTest&) = delete;
AutocompleteResultTest& operator=(const AutocompleteResultTest&) = delete;
void TearDown() override { task_environment_.RunUntilIdle(); }
// Converts `TestData` to `AutocompleteMatch`.
AutocompleteMatch PopulateAutocompleteMatch(const TestData& data);
// Adds |count| AutocompleteMatches to |matches|.
void PopulateAutocompleteMatches(base::span<const TestData> data,
ACMatches* matches);
ACMatches PopulateAutocompleteMatches(const std::vector<TestData>& data);
// Asserts that |result| has |expected_count| matches matching |expected|.
void AssertResultMatches(const AutocompleteResult& result,
base::span<const TestData> expected);
void AssertMatch(AutocompleteMatch match,
const TestData& expected_match_data,
int i);
// Creates an AutocompleteResult from |last| and |current|. The two are
// merged by |TransferOldMatches| and compared by |AssertResultMatches|.
void RunTransferOldMatchesTest(base::span<const TestData> last,
base::span<const TestData> current,
const TestData* expected,
size_t expected_size);
void RunTransferOldMatchesTest(base::span<const TestData> last,
base::span<const TestData> current,
const TestData* expected,
size_t expected_size,
AutocompleteInput input);
void SortMatchesAndVerifyOrder(
const std::string& input_text,
OmniboxEventProto::PageClassification page_classification,
const ACMatches& matches,
const std::vector<size_t>& expected_order,
const AutocompleteMatchTestData data[]);
// Returns a (mock) AutocompleteProvider of given |provider_id|.
FakeAutocompleteProvider* GetProvider(int provider_id) {
EXPECT_LT(provider_id, static_cast<int>(mock_provider_list_.size()));
return mock_provider_list_[provider_id].get();
}
OmniboxTriggeredFeatureService* triggered_feature_service() {
return &triggered_feature_service_;
}
TemplateURLService& template_url_service() {
return *search_engines_test_environment_.template_url_service();
}
protected:
search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
private:
base::test::TaskEnvironment task_environment_;
OmniboxTriggeredFeatureService triggered_feature_service_;
// For every provider mentioned in TestData, we need a mock provider.
std::vector<scoped_refptr<FakeAutocompleteProvider>> mock_provider_list_;
};
AutocompleteMatch AutocompleteResultTest::PopulateAutocompleteMatch(
const TestData& data) {
AutocompleteMatch match;
match.provider = GetProvider(data.provider_id);
match.type = data.type;
match.fill_into_edit = base::NumberToString16(data.url_id);
std::string url_id(1, data.url_id + 'a');
match.destination_url = GURL("http://" + url_id);
match.relevance = data.relevance;
match.allowed_to_be_default_match = data.allowed_to_be_default_match;
match.duplicate_matches = data.duplicate_matches;
if (data.suggestion_group_id.has_value()) {
match.suggestion_group_id = data.suggestion_group_id.value();
}
match.inline_autocompletion = base::UTF8ToUTF16(data.inline_autocompletion);
match.iph_type = data.iph_type;
return match;
}
void AutocompleteResultTest::PopulateAutocompleteMatches(
base::span<const TestData> data,
ACMatches* matches) {
for (size_t i = 0; i < data.size(); ++i) {
matches->push_back(PopulateAutocompleteMatch(data[i]));
}
}
ACMatches AutocompleteResultTest::PopulateAutocompleteMatches(
const std::vector<TestData>& data) {
ACMatches matches;
for (const auto& d : data)
matches.push_back(PopulateAutocompleteMatch(d));
return matches;
}
void AutocompleteResultTest::AssertResultMatches(
const AutocompleteResult& result,
base::span<const TestData> expected) {
std::vector<int> relevances = {};
std::ranges::transform(
expected, std::back_inserter(relevances),
[](const auto& test_data) { return test_data.relevance; });
std::vector<int> expected_relevances = {};
std::ranges::transform(
result, std::back_inserter(expected_relevances),
[&](const AutocompleteMatch& match) { return match.relevance; });
EXPECT_THAT(relevances, testing::ElementsAreArray(expected_relevances));
}
void AutocompleteResultTest::AssertMatch(AutocompleteMatch match,
const TestData& expected_match_data,
int i) {
AutocompleteMatch expected_match =
PopulateAutocompleteMatch(expected_match_data);
EXPECT_EQ(expected_match.provider, match.provider) << i;
EXPECT_EQ(expected_match.type, match.type) << i;
EXPECT_EQ(expected_match.relevance, match.relevance) << i;
EXPECT_EQ(expected_match.allowed_to_be_default_match,
match.allowed_to_be_default_match)
<< i;
EXPECT_EQ(expected_match.destination_url.spec(), match.destination_url.spec())
<< i;
EXPECT_EQ(expected_match.inline_autocompletion, match.inline_autocompletion)
<< i;
}
void AutocompleteResultTest::RunTransferOldMatchesTest(
base::span<const TestData> last,
base::span<const TestData> current,
const TestData* expected,
size_t expected_size) {
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
RunTransferOldMatchesTest(last, current, expected, expected_size, input);
}
void AutocompleteResultTest::RunTransferOldMatchesTest(
base::span<const TestData> last,
base::span<const TestData> current,
const TestData* expected,
size_t expected_size,
AutocompleteInput input) {
ACMatches last_matches;
PopulateAutocompleteMatches(last, &last_matches);
AutocompleteResult last_result;
last_result.AppendMatches(last_matches);
last_result.SortAndCull(
input, &template_url_service(), triggered_feature_service(),
/*is_lens_active=*/false, /*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ACMatches current_matches;
PopulateAutocompleteMatches(current, &current_matches);
AutocompleteResult current_result;
current_result.AppendMatches(current_matches);
current_result.SortAndCull(
input, &template_url_service(), triggered_feature_service(),
/*is_lens_active=*/false, /*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
current_result.TransferOldMatches(input, &last_result);
current_result.SortAndCull(
input, &template_url_service(), triggered_feature_service(),
/*is_lens_active=*/false, /*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
AssertResultMatches(current_result, UNSAFE_TODO({expected, expected_size}));
}
void AutocompleteResultTest::SortMatchesAndVerifyOrder(
const std::string& input_text,
OmniboxEventProto::PageClassification page_classification,
const ACMatches& matches,
const std::vector<size_t>& expected_order,
const AutocompleteMatchTestData data[]) {
AutocompleteInput input(base::ASCIIToUTF16(input_text), page_classification,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
std::vector<std::string> expected;
std::ranges::transform(
expected_order, std::back_inserter(expected),
[&](size_t i) { return UNSAFE_TODO(data[i]).destination_url; });
std::vector<std::string> actual;
std::ranges::transform(
result, std::back_inserter(actual),
[](const auto& match) { return match.destination_url.spec(); });
EXPECT_THAT(actual, testing::ElementsAreArray(expected));
}
// Assertion testing for AutocompleteResult::SwapMatchesWith.
TEST_F(AutocompleteResultTest, SwapMatches) {
AutocompleteResult r1;
AutocompleteResult r2;
// Swap with empty shouldn't do anything interesting.
r1.SwapMatchesWith(&r2);
EXPECT_FALSE(r1.default_match());
EXPECT_FALSE(r2.default_match());
// Swap with a single match.
ACMatches matches;
AutocompleteMatch match;
match.relevance = 1;
match.allowed_to_be_default_match = true;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
matches.push_back(match);
r1.AppendMatches(matches);
r1.SortAndCull(input, &template_url_service(), triggered_feature_service(),
/*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_TRUE(r1.default_match());
EXPECT_EQ(&*r1.begin(), r1.default_match());
r1.SwapMatchesWith(&r2);
EXPECT_TRUE(r1.empty());
EXPECT_FALSE(r1.default_match());
ASSERT_FALSE(r2.empty());
EXPECT_TRUE(r2.default_match());
EXPECT_EQ(&*r2.begin(), r2.default_match());
}
TEST_F(AutocompleteResultTest, AlternateNavUrl) {
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
FakeAutocompleteProviderClient client;
reinterpret_cast<TestingPrefServiceSimple*>(client.GetLocalState())
->registry()
->RegisterIntegerPref(omnibox::kIntranetRedirectBehavior, 0);
// Against search matches, we should not generate an alternate nav URL, unless
// overriden by policy, tested in AlternateNavUrl_IntranetRedirectPolicy
// below.
{
AutocompleteMatch match;
match.type = AutocompleteMatchType::SEARCH_SUGGEST;
match.destination_url = GURL("http://www.foo.com/s?q=foo");
GURL alternate_nav_url =
AutocompleteResult::ComputeAlternateNavUrl(input, match, &client);
EXPECT_FALSE(alternate_nav_url.is_valid());
}
// Against matching URL matches, we should never generate an alternate nav
// URL.
{
AutocompleteMatch match;
match.type = AutocompleteMatchType::SEARCH_SUGGEST;
match.destination_url = GURL("http://a/");
GURL alternate_nav_url =
AutocompleteResult::ComputeAlternateNavUrl(input, match, &client);
EXPECT_FALSE(alternate_nav_url.is_valid());
}
}
TEST_F(AutocompleteResultTest, AlternateNavUrl_IntranetRedirectPolicy) {
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
FakeAutocompleteProviderClient client;
reinterpret_cast<TestingPrefServiceSimple*>(client.GetLocalState())
->registry()
->RegisterIntegerPref(omnibox::kIntranetRedirectBehavior, 0);
// Allow alternate nav URLs when policy allows.
{
client.GetLocalState()->SetInteger(
omnibox::kIntranetRedirectBehavior,
static_cast<int>(omnibox::IntranetRedirectorBehavior::
ENABLE_INTERCEPTION_CHECKS_AND_INFOBARS));
AutocompleteMatch match;
match.type = AutocompleteMatchType::SEARCH_SUGGEST;
match.destination_url = GURL("http://www.foo.com/s?q=foo");
GURL alternate_nav_url =
AutocompleteResult::ComputeAlternateNavUrl(input, match, &client);
EXPECT_EQ("http://a/", alternate_nav_url.spec());
}
// Disallow alternate nav URLs when policy disallows.
{
client.GetLocalState()->SetInteger(
omnibox::kIntranetRedirectBehavior,
static_cast<int>(omnibox::IntranetRedirectorBehavior::DISABLE_FEATURE));
AutocompleteMatch match;
match.type = AutocompleteMatchType::SEARCH_SUGGEST;
match.destination_url = GURL("http://www.foo.com/s?q=foo");
GURL alternate_nav_url =
AutocompleteResult::ComputeAlternateNavUrl(input, match, &client);
EXPECT_FALSE(alternate_nav_url.is_valid());
}
}
// Tests that if the new results have a lower max relevance score than last,
// any copied results have their relevance shifted down.
TEST_F(AutocompleteResultTest, TransferOldMatches) {
TestData last[] = {
{0, 1, 1000, true},
{1, 1, 500, true},
};
TestData current[] = {
{2, 1, 400, true},
};
TestData result[] = {
{2, 1, 400, true},
{1, 1, 399, false}, // transferred matches aren't allowed to be default.
};
ASSERT_NO_FATAL_FAILURE(
RunTransferOldMatchesTest(last, current, result, std::size(result)));
}
// Tests that if the new results have a lower max relevance score than last,
// any copied results have their relevance shifted down when the allowed to
// be default constraint comes into play.
TEST_F(AutocompleteResultTest, TransferOldMatchesAllowedToBeDefault) {
TestData last[] = {
{0, 1, 1300, true},
{1, 1, 1200, true},
{2, 1, 1100, true},
};
TestData current[] = {
{3, 1, 1000, false},
{4, 1, 900, true},
};
// The expected results are out of relevance order because the top-scoring
// allowed to be default match is always pulled to the top.
TestData result[] = {
{4, 1, 900, true},
{3, 1, 1000, false},
{2, 1, 899, false}, // transferred matches aren't allowed to be default.
};
ASSERT_NO_FATAL_FAILURE(
RunTransferOldMatchesTest(last, current, result, std::size(result)));
}
// Tests |TransferOldMatches()| with an |AutocompleteInput| with
// |prevent_inline_autocomplete| set to true. Noteworthy, expect that resulting
// matches must have effectively empty autocompletions; i.e. either empty
// |inline_autocompletion|, or false |allowed_to_be_default|. Tests all 12
// combinations of 1) last match has a lower or higher relevance than current
// match, 2) last match was allowed to be default, 3) last match had
// autocompletion, and 4) current match is allowed to be default.
TEST_F(AutocompleteResultTest,
TransferOldMatchesAllowedToBeDefaultWithPreventInlineAutocompletion) {
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
input.set_prevent_inline_autocomplete(true);
{
SCOPED_TRACE(
"Current matches not allowed to be default and scored higher.");
// 1) `allowed_to_be_default` should be true only for `last` matches without
// autocompletion.
// 2) `allowed_to_be_default` should not be set true for previously not
// allowed matches, even if they don't have inline autocompletion.
// 3) When `allowed_to_be_default` is false, `current` matches should be
// preferred as they're scored higher.
// clang-format off
TestData last[] = {
{0, 1, 1020, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, {}, "autocompletion"},
{1, 1, 1010, true},
{2, 1, 1000, false},
};
TestData current[] = {
{0, 2, 1520, false},
{1, 2, 1510, false},
{2, 2, 1500, false},
};
TestData result[] = {
{1, 1, 1510, true},
{0, 2, 1520, false},
{2, 2, 1500, false},
};
// clang-format on
ASSERT_NO_FATAL_FAILURE(RunTransferOldMatchesTest(
last, current, result, std::size(result), input));
}
{
SCOPED_TRACE("Current matches not allowed to be default and scored lower.");
// Similar to above, except `last` matches should be preferred in deduping
// as they're scored higher.
// clang-format off
TestData last[] = {
{0, 1, 1020, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, {}, "autocompletion"},
{1, 1, 1010, true},
{2, 1, 1000, false},
};
TestData current[] = {
// Need a high-scoring current match to avoid demoting last matches.
{3, 2, 1500, false},
{0, 2, 520, false},
{1, 2, 510, false},
{2, 2, 500, false},
};
TestData result[] = {
{1, 1, 1010, true},
{3, 2, 1500, false},
{0, 1, 1020, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, {}, "autocompletion"},
{2, 1, 1000, false},
};
// clang-format on
ASSERT_NO_FATAL_FAILURE(RunTransferOldMatchesTest(
last, current, result, std::size(result), input));
}
{
SCOPED_TRACE("Current matches allowed to be default and scored higher.");
// Deduping should prefer the `current` matches as they're both allowed to
// be default and scored higher.
// clang-format off
TestData last[] = {
{0, 1, 1020, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, {}, "autocompletion"},
{1, 1, 1010, true},
{2, 1, 1000, false},
};
TestData current[] = {
{0, 2, 1520, true},
{1, 2, 1510, true},
{2, 2, 1500, true},
};
TestData result[] = {
{0, 2, 1520, true},
{1, 2, 1510, true},
{2, 2, 1500, true},
};
// clang-format on
ASSERT_NO_FATAL_FAILURE(RunTransferOldMatchesTest(
last, current, result, std::size(result), input));
}
{
SCOPED_TRACE("Current matches allowed to be default and scored lower.");
// `last` matches still allowed to be default after transferring should be
// preferred in deduping as they're scored higher. Otherwise, `current`
// matches should be preferred in deduping as they're allowed to be default.
// clang-format off
TestData last[] = {
{0, 1, 1020, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, {}, "autocompletion"},
{1, 1, 1010, true},
{2, 1, 1000, false},
};
TestData current[] = {
// Need a high-scoring current match to avoid demoting last matches.
{3, 2, 1500, true},
{0, 2, 520, true},
{1, 2, 510, true},
{2, 2, 500, true},
};
TestData result[] = {
{3, 2, 1500, true},
{0, 2, 1020, true},
{1, 1, 1010, true},
{2, 2, 1000, true},
};
// clang-format on
ASSERT_NO_FATAL_FAILURE(RunTransferOldMatchesTest(
last, current, result, std::size(result), input));
}
}
// Tests that matches are copied correctly from two distinct providers.
TEST_F(AutocompleteResultTest, TransferOldMatchesMultipleProviders) {
TestData last[] = {
{0, 1, 1300, false}, {1, 2, 1250, true}, {2, 1, 1200, false},
{3, 2, 1150, true}, {4, 1, 1100, false},
};
TestData current[] = {
{5, 1, 1000, false},
{6, 2, 800, true},
{7, 1, 500, true},
};
// The expected results are out of relevance order because the top-scoring
// allowed to be default match is always pulled to the top.
TestData result[] = {
{6, 2, 800, true}, {5, 1, 1000, false}, {3, 2, 799, false},
{7, 1, 500, true}, {4, 1, 499, false},
};
ASSERT_NO_FATAL_FAILURE(
RunTransferOldMatchesTest(last, current, result, std::size(result)));
}
// Tests that matches are copied correctly from two distinct providers when
// one provider doesn't have a current legal default match.
TEST_F(AutocompleteResultTest,
TransferOldMatchesWithOneProviderWithoutDefault) {
TestData last[] = {
{0, 2, 1250, true}, {1, 2, 1150, true}, {2, 1, 900, false},
{3, 1, 800, false}, {4, 1, 700, false},
};
TestData current[] = {
{5, 1, 1000, true},
{6, 2, 800, false},
{7, 1, 500, true},
};
TestData result[] = {
{5, 1, 1000, true}, {1, 2, 999, false}, {6, 2, 800, false},
{4, 1, 700, false}, {7, 1, 500, true},
};
ASSERT_NO_FATAL_FAILURE(
RunTransferOldMatchesTest(last, current, result, std::size(result)));
}
// Tests that transferred matches do not include the specialized match types.
TEST_F(AutocompleteResultTest, TransferOldMatchesSkipsSpecializedSuggestions) {
TestData last[] = {
{0, 1, 1000, true, {}, AutocompleteMatchType::TILE_NAVSUGGEST},
{1, 4, 999, true, {}, AutocompleteMatchType::TILE_SUGGESTION},
{2, 2, 500, true},
};
TestData current[] = {
{3, 1, 600, true},
};
TestData result[] = {
{3, 1, 600, true},
{2, 2, 500, false},
};
ASSERT_NO_FATAL_FAILURE(
RunTransferOldMatchesTest(last, current, result, std::size(result)));
}
// Tests that transferred matches do not include the specialized match types.
TEST_F(AutocompleteResultTest, TransferOldMatchesSkipDoneProviders) {
TestData last[] = {
{0, 1, 500}, // Suggestion from done provider
{1, 2, 400}, // Suggestion for not-done provider
};
TestData current[] = {
{2, 3, 700}, // Suggestion from done provider
{3, 4, 600}, // Suggestion for not-done provider
};
TestData result[] = {
{2, 3, 700}, // New suggestion from done provider
{3, 4, 600}, // New suggestion from not-done provider
// Skip suggestion `{0, 1, 500}`.
{1, 2, 400}, // Transferred suggestion from not-done provider
};
GetProvider(1)->done_ = true;
GetProvider(3)->done_ = true;
ASSERT_NO_FATAL_FAILURE(
RunTransferOldMatchesTest(last, current, result, std::size(result)));
}
// Tests that matches with empty destination URLs aren't treated as duplicates
// and culled.
TEST_F(AutocompleteResultTest, SortAndCullEmptyDestinationURLs) {
TestData data[] = {
{1, 1, 500, true}, {0, 1, 1100, true}, {1, 1, 1000, true},
{0, 1, 1300, true}, {0, 1, 1200, true},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[1].destination_url = GURL();
matches[3].destination_url = GURL();
matches[4].destination_url = GURL();
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// Of the two results with the same non-empty destination URL, the
// lower-relevance one should be dropped. All of the results with empty URLs
// should be kept.
ASSERT_EQ(4U, result.size());
EXPECT_TRUE(result.match_at(0)->destination_url.is_empty());
EXPECT_EQ(1300, result.match_at(0)->relevance);
EXPECT_TRUE(result.match_at(1)->destination_url.is_empty());
EXPECT_EQ(1200, result.match_at(1)->relevance);
EXPECT_TRUE(result.match_at(2)->destination_url.is_empty());
EXPECT_EQ(1100, result.match_at(2)->relevance);
EXPECT_EQ("http://b/", result.match_at(3)->destination_url.spec());
EXPECT_EQ(1000, result.match_at(3)->relevance);
}
#if !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
// Tests which remove results only work on desktop.
TEST_F(AutocompleteResultTest, SortAndCullTailSuggestions) {
// clang-format off
TestData data[] = {
{1, 1, 500, true},
{2, 1, 1100, false},
{3, 1, 1000, false},
{4, 1, 1300, false},
{5, 1, 1200, false},
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// These will get sorted up, but still removed.
matches[3].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
matches[4].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(3UL, result.size());
EXPECT_NE(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(0)->type);
EXPECT_NE(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(1)->type);
EXPECT_NE(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(2)->type);
}
TEST_F(AutocompleteResultTest, SortAndCullKeepDefaultTailSuggestions) {
// clang-format off
TestData data[] = {
{1, 1, 500, true},
{2, 1, 1100, false},
{3, 1, 1000, false},
{4, 1, 1300, false},
{5, 1, 1200, false},
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Make sure that even bad tail suggestions, if the only default match,
// are kept.
matches[0].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
matches[1].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
matches[2].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(3UL, result.size());
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(0)->type);
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(1)->type);
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(2)->type);
}
TEST_F(AutocompleteResultTest, SortAndCullKeepMoreDefaultTailSuggestions) {
// clang-format off
TestData data[] = {
{1, 1, 500, true}, // Low score non-tail default
{2, 1, 1100, false}, // Tail
{3, 1, 1000, true}, // Allow a tail suggestion to be the default.
{4, 1, 1300, false}, // Tail
{5, 1, 1200, false}, // Tail
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Make sure that even a bad non-tail default suggestion is kept.
for (size_t i = 1; i < 5; ++i)
matches[i].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(5UL, result.size());
// Non-tail default must be first, regardless of score
EXPECT_NE(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(0)->type);
for (size_t i = 1; i < 5; ++i) {
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(i)->type);
EXPECT_FALSE(result.match_at(i)->allowed_to_be_default_match);
}
}
TEST_F(AutocompleteResultTest, SortAndCullZeroRelevanceSuggestions) {
// clang-format off
TestData data[] = {
{1, 1, 1000, true}, // A default non-tail suggestion.
{2, 1, 0, true}, // A no-relevance default non-tail suggestion.
{3, 1, 1100, true}, // Default tail
{4, 1, 1000, false}, // Tail
{5, 1, 1300, false}, // Tail
{6, 1, 0, false}, // No-relevance tail suggestion.
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
for (size_t i = 2; i < std::size(data); ++i)
matches[i].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(4UL, result.size());
EXPECT_NE(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(0)->type);
EXPECT_TRUE(result.match_at(0)->allowed_to_be_default_match);
for (size_t i = 1; i < 4; ++i) {
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(i)->type);
EXPECT_FALSE(result.match_at(i)->allowed_to_be_default_match);
}
}
TEST_F(AutocompleteResultTest, SortAndCullZeroRelevanceDefaultMatches) {
// clang-format off
TestData data[] = {
{1, 1, 0, true}, // A zero-relevance default non-tail suggestion.
{2, 1, 1100, true}, // Default tail
{3, 1, 1000, false}, // Tail
{4, 1, 1300, false}, // Tail
{5, 1, 0, false}, // No-relevance tail suggestion.
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
for (size_t i = 1; i < std::size(data); ++i)
matches[i].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// It should ignore the first suggestion, despite it being marked as
// allowed to be default.
EXPECT_EQ(3UL, result.size());
EXPECT_TRUE(result.match_at(0)->allowed_to_be_default_match);
for (size_t i = 0; i < 3; ++i) {
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(i)->type);
if (i > 0)
EXPECT_FALSE(result.match_at(i)->allowed_to_be_default_match);
}
}
#endif
TEST_F(AutocompleteResultTest, SortAndCullOnlyTailSuggestions) {
// clang-format off
TestData data[] = {
{1, 1, 500, true}, // Allow a bad non-tail default.
{2, 1, 1100, false}, // Tail
{3, 1, 1000, false}, // Tail
{4, 1, 1300, false}, // Tail
{5, 1, 1200, false}, // Tail
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// These will not be removed.
for (size_t i = 1; i < 5; ++i)
matches[i].type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(5UL, result.size());
EXPECT_NE(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(0)->type);
for (size_t i = 1; i < 5; ++i)
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
result.match_at(i)->type);
}
TEST_F(AutocompleteResultTest, SortAndCullNoMatchesAllowedToBeDefault) {
// clang-format off
TestData data[] = {
{1, 1, 500, false}, // Not allowed_to_be_default_match
{2, 1, 1100, false}, // Not allowed_to_be_default_match
{3, 1, 1000, false}, // Not allowed_to_be_default_match
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(std::u16string(), metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(3UL, result.size());
EXPECT_EQ(matches[1].destination_url, result.match_at(0)->destination_url);
EXPECT_EQ(matches[2].destination_url, result.match_at(1)->destination_url);
EXPECT_EQ(matches[0].destination_url, result.match_at(2)->destination_url);
for (size_t i = 0; i < 3; ++i)
EXPECT_FALSE(result.match_at(i)->allowed_to_be_default_match);
}
TEST_F(AutocompleteResultTest, SortAndCullDuplicateSearchURLs) {
// Register a template URL that corresponds to 'foo' search engine.
TemplateURLData url_data;
url_data.SetShortName(u"unittest");
url_data.SetKeyword(u"foo");
url_data.SetURL("http://www.foo.com/s?q={searchTerms}");
template_url_service().Add(std::make_unique<TemplateURL>(url_data));
TestData data[] = {
{0, 1, 1300, true}, {1, 1, 1200, true}, {2, 1, 1100, true},
{3, 1, 1000, true}, {4, 2, 900, true},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].destination_url = GURL("http://www.foo.com/s?q=foo");
matches[1].destination_url = GURL("http://www.foo.com/s?q=foo2");
matches[2].destination_url = GURL("http://www.foo.com/s?q=foo&oq=f");
matches[3].destination_url = GURL("http://www.foo.com/s?q=foo&aqs=0");
matches[4].destination_url = GURL("http://www.foo.com/");
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// We expect the 3rd and 4th results to be removed.
ASSERT_EQ(3U, result.size());
EXPECT_EQ("http://www.foo.com/s?q=foo",
result.match_at(0)->destination_url.spec());
EXPECT_EQ(1300, result.match_at(0)->relevance);
EXPECT_EQ("http://www.foo.com/s?q=foo2",
result.match_at(1)->destination_url.spec());
EXPECT_EQ(1200, result.match_at(1)->relevance);
EXPECT_EQ("http://www.foo.com/", result.match_at(2)->destination_url.spec());
EXPECT_EQ(900, result.match_at(2)->relevance);
}
TEST_F(AutocompleteResultTest, SortAndCullWithMatchDups) {
// Register a template URL that corresponds to 'foo' search engine.
TemplateURLData url_data;
url_data.SetShortName(u"unittest");
url_data.SetKeyword(u"foo");
url_data.SetURL("http://www.foo.com/s?q={searchTerms}");
template_url_service().Add(std::make_unique<TemplateURL>(url_data));
AutocompleteMatch dup_match;
dup_match.destination_url = GURL("http://www.foo.com/s?q=foo&oq=dup");
std::vector<AutocompleteMatch> dups;
dups.push_back(dup_match);
TestData data[] = {
{0, 1, 1300, true, dups}, {1, 1, 1200, true}, {2, 1, 1100, true},
{3, 1, 1000, true, dups}, {4, 2, 900, true}, {5, 1, 800, true},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].destination_url = GURL("http://www.foo.com/s?q=foo");
matches[1].destination_url = GURL("http://www.foo.com/s?q=foo2");
matches[2].destination_url = GURL("http://www.foo.com/s?q=foo&oq=f");
matches[3].destination_url = GURL("http://www.foo.com/s?q=foo&aqs=0");
matches[4].destination_url = GURL("http://www.foo.com/");
matches[5].destination_url = GURL("http://www.foo.com/s?q=foo2&oq=f");
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// Expect 3 unique results after SortAndCull().
ASSERT_EQ(3U, result.size());
// Check that 3rd and 4th result got added to the first result as duplicates
// and also duplicates of the 4th match got copied.
ASSERT_EQ(4U, result.match_at(0)->duplicate_matches.size());
const AutocompleteMatch* first_match = result.match_at(0);
EXPECT_EQ(matches[2].destination_url,
first_match->duplicate_matches.at(1).destination_url);
EXPECT_EQ(matches[3].destination_url,
first_match->duplicate_matches.at(2).destination_url);
EXPECT_EQ(dup_match.destination_url,
first_match->duplicate_matches.at(3).destination_url);
// Check that 6th result started a new list of dups for the second result.
ASSERT_EQ(1U, result.match_at(1)->duplicate_matches.size());
EXPECT_EQ(matches[5].destination_url,
result.match_at(1)->duplicate_matches.at(0).destination_url);
}
TEST_F(AutocompleteResultTest, SortAndCullWithPreserveDefaultMatch) {
auto test = [&](const std::vector<TestData>& last,
const std::vector<TestData>& current,
const std::vector<TestData>& expected) {
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
ACMatches last_matches = PopulateAutocompleteMatches(last);
AutocompleteResult last_result;
last_result.AppendMatches(last_matches);
last_result.SortAndCull(
input, &template_url_service(), triggered_feature_service(),
/*is_lens_active=*/false, /*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ACMatches current_matches = PopulateAutocompleteMatches(current);
AutocompleteResult current_result;
current_result.AppendMatches(current_matches);
// Run SortAndCull, but try to keep the first entry of last_matches on top.
current_result.SortAndCull(
input, &template_url_service(), triggered_feature_service(),
/*is_lens_active=*/false, /*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false, *last_result.match_at(0));
AssertResultMatches(current_result, expected);
};
{
SCOPED_TRACE("Lower scored default is preserved.");
std::vector<TestData> last = {
{0, 1, 500, true},
{1, 1, 400, true},
};
std::vector<TestData> current = {
{1, 1, 500, true},
{0, 1, 400, true},
};
std::vector<TestData> expected = {
{0, 1, 400, true},
{1, 1, 500, true},
};
test(last, current, expected);
}
{
SCOPED_TRACE("Don't preserve a default that no longer matches.");
std::vector<TestData> last = {
{0, 1, 500, true},
};
std::vector<TestData> current = {
{1, 1, 100, true},
};
std::vector<TestData> expected = {
{1, 1, 100, true},
};
test(last, current, expected);
}
{
SCOPED_TRACE(
"Previous default does not replace a higher scored "
"URL_WHAT_YOU_TYPED.");
std::vector<TestData> last = {
{0, 1, 500, true, {}, AutocompleteMatchType::HISTORY_URL},
{1, 1, 400, true, {}, AutocompleteMatchType::HISTORY_URL},
};
std::vector<TestData> current = {
{0, 1, 500, true, {}, AutocompleteMatchType::HISTORY_URL},
{1, 1, 400, true, {}, AutocompleteMatchType::HISTORY_URL},
{2, 1, 600, true, {}, AutocompleteMatchType::URL_WHAT_YOU_TYPED},
};
std::vector<TestData> expected = {
{2, 1, 600, true, {}, AutocompleteMatchType::URL_WHAT_YOU_TYPED},
{0, 1, 500, true, {}, AutocompleteMatchType::HISTORY_URL},
{1, 1, 400, true, {}, AutocompleteMatchType::HISTORY_URL},
};
test(last, current, expected);
}
{
SCOPED_TRACE(
"Previous default does replace a lower scored URL_WHAT_YOU_TYPED.");
std::vector<TestData> last = {
{0, 1, 500, true, {}, AutocompleteMatchType::HISTORY_URL},
{1, 1, 400, true, {}, AutocompleteMatchType::HISTORY_URL},
};
std::vector<TestData> current = {
{0, 1, 500, true, {}, AutocompleteMatchType::HISTORY_URL},
{1, 1, 400, true, {}, AutocompleteMatchType::HISTORY_URL},
{2, 1, 300, true, {}, AutocompleteMatchType::URL_WHAT_YOU_TYPED},
{3, 1, 600, true, {}, AutocompleteMatchType::HISTORY_URL},
};
std::vector<TestData> expected = {
{0, 1, 500, true, {}, AutocompleteMatchType::HISTORY_URL},
{3, 1, 600, true, {}, AutocompleteMatchType::HISTORY_URL},
{1, 1, 400, true, {}, AutocompleteMatchType::HISTORY_URL},
{2, 1, 300, true, {}, AutocompleteMatchType::URL_WHAT_YOU_TYPED},
};
test(last, current, expected);
}
}
TEST_F(AutocompleteResultTest, DemoteOnDeviceSearchSuggestions) {
// clang-format off
TestData data[] = {
{1, 1, 500, true},
{2, 2, 1100, true},
{3, 2, 1000, true},
{4, 1, 1300, true},
{5, 1, 1200, true},
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].type = AutocompleteMatchType::SEARCH_SUGGEST;
matches[1].type = AutocompleteMatchType::SEARCH_SUGGEST;
matches[2].type = AutocompleteMatchType::SEARCH_SUGGEST;
matches[3].type = AutocompleteMatchType::SEARCH_SUGGEST;
matches[4].type = AutocompleteMatchType::SEARCH_SUGGEST;
// match1, match2 are set as on device head suggestion.
matches[1].subtypes = {omnibox::SUBTYPE_OMNIBOX_OTHER,
omnibox::SUBTYPE_SUGGEST_2G_LITE};
matches[2].subtypes = {omnibox::SUBTYPE_OMNIBOX_OTHER,
omnibox::SUBTYPE_SUGGEST_2G_LITE};
matches[0].provider->type_ = AutocompleteProvider::TYPE_SEARCH;
matches[1].provider->type_ = AutocompleteProvider::TYPE_ON_DEVICE_HEAD;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
// Test setting on device suggestion relevances lower than search provider
// suggestions.
AutocompleteResultForTesting result;
result.AppendMatches(matches);
result.DemoteOnDeviceSearchSuggestions();
EXPECT_EQ(5UL, result.size());
EXPECT_NE(AutocompleteProvider::TYPE_ON_DEVICE_HEAD,
result.match_at(0)->provider->type());
EXPECT_EQ(AutocompleteProvider::TYPE_ON_DEVICE_HEAD,
result.match_at(1)->provider->type());
EXPECT_LT(result.match_at(1)->relevance, result.match_at(0)->relevance);
EXPECT_EQ(AutocompleteProvider::TYPE_ON_DEVICE_HEAD,
result.match_at(2)->provider->type());
EXPECT_LT(result.match_at(2)->relevance, result.match_at(0)->relevance);
EXPECT_NE(AutocompleteProvider::TYPE_ON_DEVICE_HEAD,
result.match_at(3)->provider->type());
EXPECT_NE(AutocompleteProvider::TYPE_ON_DEVICE_HEAD,
result.match_at(4)->provider->type());
}
TEST_F(AutocompleteResultTest, DemoteByType) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(omnibox::kGroupingFrameworkForNonZPS);
// Add some matches.
ACMatches matches;
const AutocompleteMatchTestData data[] = {
{"http://history-url/1300/", AutocompleteMatchType::HISTORY_URL},
{"http://history-title/1200/def", AutocompleteMatchType::HISTORY_TITLE},
{"http://search-what-you-typed/1100/",
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED},
{"http://search-history/1000/def", AutocompleteMatchType::SEARCH_HISTORY},
};
PopulateAutocompleteMatchesFromTestData(data, std::size(data), &matches);
// Make history-title and search-history the only default matches, so that
// they compete.
matches[0].allowed_to_be_default_match = false;
matches[2].allowed_to_be_default_match = false;
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
// Where Grouping suggestions by Search vs URL kicks in, search gets
// promoted to the top of the list.
// No demotion (e.g. omnibox input)
const std::vector<size_t> expected_natural_order{1, 2, 3, 0};
// Demoting all matches (e.g. realbox, search input)
const std::vector<size_t> expected_demoted_all_order{3, 2, 0, 1};
// TODO(manukh): Flush out test so that `expected_natural_order` and
// `expected_demoted_non_default_order` are different.
// Demoting non-default matches (e.g. realbox, URL input)
const std::vector<size_t> expected_demoted_non_default_order{1, 2, 3, 0};
#else
// Note: Android and iOS performs grouping by Search vs URL at a later stage,
// when views are built. this means the vector below will be demoted by type,
// but not rearranged by Search vs URL.
// No demotion (e.g. omnibox input)
const std::vector<size_t> expected_natural_order{1, 0, 2, 3};
// Demoting all matches (e.g. realbox, search input)
const std::vector<size_t> expected_demoted_all_order{3, 2, 0, 1};
// Demoting non-default matches (e.g. realbox, URL input)
const std::vector<size_t> expected_demoted_non_default_order{1, 2, 3, 0};
#endif
// Expect no demotion on `HOME_PAGE` inputs. Expect grouping to only non-
// default matches. History-title is the highest scoring defaultable
// suggestion, so expect it 1st. Then the rest of the matches by relevance.
{
SCOPED_TRACE("SortAndCull with HOME_PAGE classification");
SortMatchesAndVerifyOrder("a", OmniboxEventProto::HOME_PAGE, matches,
expected_natural_order, data);
}
// For `NTP_REALBOX` inputs, expect demotion to occur and affect both the
// default and non-default matches. After demotion, search-history is the
// highest defaultable match, so expect it 1st. Then the SWYT match as the
// history matches should be demoted.
{
SCOPED_TRACE("SortAndCull with NTP_REALBOX classification");
SortMatchesAndVerifyOrder("a", OmniboxEventProto::NTP_REALBOX, matches,
expected_demoted_all_order, data);
}
// Usually, the default match is exempt of demotion. Only if the input is both
// a) realbox, and b) not a URL, then demotion is considered. See
// `AutocompleteResult::FindTopMatch()`. The lower matches will be demoted in
// the realbox, regardless of whether the input is a URL.
{
SCOPED_TRACE("SortAndCull with NTP_REALBOX classification and URL input");
SortMatchesAndVerifyOrder("www.example.com", OmniboxEventProto::NTP_REALBOX,
matches, expected_demoted_non_default_order,
data);
}
}
TEST_F(AutocompleteResultTest, SortAndCullReorderForDefaultMatch) {
TestData data[] = {{0, 1, 1300, true},
{1, 1, 1200, true},
{2, 1, 1100, true},
{3, 1, 1000, true}};
TestSchemeClassifier test_scheme_classifier;
{
// Check that reorder doesn't do anything if the top result
// is already a legal default match (which is the default from
// PopulateAutocompleteMatches()).
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::HOME_PAGE,
test_scheme_classifier);
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
AssertResultMatches(result, data);
}
{
// Check that reorder swaps up a result appropriately.
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].allowed_to_be_default_match = false;
matches[1].allowed_to_be_default_match = false;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::HOME_PAGE,
test_scheme_classifier);
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(4U, result.size());
EXPECT_EQ("http://c/", result.match_at(0)->destination_url.spec());
EXPECT_EQ("http://a/", result.match_at(1)->destination_url.spec());
EXPECT_EQ("http://b/", result.match_at(2)->destination_url.spec());
EXPECT_EQ("http://d/", result.match_at(3)->destination_url.spec());
}
}
// Note: DCHECKs not firing on Cast.
#if DCHECK_IS_ON()
TEST_F(AutocompleteResultTest, SortAndCullFailsWithIncorrectDefaultScheme) {
// Scenario:
// - User navigates to a webpage whose URL looks like a scheme,
// e.g. "chrome:123" (note the colon).
// - The navigation becomes an entry in the user's URL history.
// - User types "chrome:" again.
// - The top suggestion will match the previous user entry and may be the
// "chrome:123" again, except typing ":" invokes scheme checker.
// Make sure the scheme checker is not causing trouble when the default
// suggestion is Search.
const AutocompleteMatchTestData data[] = {
{"https://chrome:123", AutocompleteMatchType::HISTORY_URL},
{"chrome://history", AutocompleteMatchType::HISTORY_URL}};
ACMatches matches;
PopulateAutocompleteMatchesFromTestData(data, std::size(data), &matches);
matches[0].allowed_to_be_default_match = true;
matches[1].allowed_to_be_default_match = true;
TestSchemeClassifier test_scheme_classifier;
AutocompleteInput input(u"chrome:", metrics::OmniboxEventProto::HOME_PAGE,
test_scheme_classifier);
AutocompleteResult result;
result.AppendMatches(matches);
EXPECT_DEATH_IF_SUPPORTED(
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false),
"");
}
#endif
TEST_F(AutocompleteResultTest, SortAndCullPermitSearchForSchemeMatching) {
// Scenario:
// - User searches for something that looks like a scheme,
// e.g. "chrome: how to do x" (note the colon).
// - The search becomes an entry in the user's history (local or remote)
// outranking any HistoryURL matches.
// - User types "chrome:" again.
// - The top suggestion will match the previous user entry and may be the
// "chrome: how to do x" again, except typing ":" invokes scheme checker.
// Make sure the scheme checker is not causing trouble when the default
// suggestion is Search.
const AutocompleteMatchTestData data[] = {
{"https://google.com/search?q=chrome:123",
AutocompleteMatchType::SEARCH_SUGGEST},
{"chrome://history", AutocompleteMatchType::HISTORY_URL}};
ACMatches matches;
PopulateAutocompleteMatchesFromTestData(data, std::size(data), &matches);
matches[0].allowed_to_be_default_match = true;
matches[1].allowed_to_be_default_match = true;
TestSchemeClassifier test_scheme_classifier;
AutocompleteInput input(u"chrome:", metrics::OmniboxEventProto::HOME_PAGE,
test_scheme_classifier);
AutocompleteResult result;
result.AppendMatches(matches);
// Must not assert.
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
}
TEST_F(AutocompleteResultTest, SortAndCullPromoteDefaultMatch) {
TestData data[] = {{0, 1, 1300, false},
{1, 1, 1200, false},
{2, 2, 1100, false},
{2, 3, 1000, false},
{2, 4, 900, true}};
TestSchemeClassifier test_scheme_classifier;
// Check that reorder swaps up a result, and promotes relevance,
// appropriately.
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::HOME_PAGE,
test_scheme_classifier);
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(3U, result.size());
EXPECT_EQ("http://c/", result.match_at(0)->destination_url.spec());
EXPECT_EQ(1100, result.match_at(0)->relevance);
EXPECT_TRUE(result.match_at(0)->allowed_to_be_default_match);
EXPECT_EQ(GetProvider(4), result.match_at(0)->provider);
EXPECT_EQ("http://a/", result.match_at(1)->destination_url.spec());
EXPECT_FALSE(result.match_at(1)->allowed_to_be_default_match);
EXPECT_EQ("http://b/", result.match_at(2)->destination_url.spec());
EXPECT_FALSE(result.match_at(2)->allowed_to_be_default_match);
}
TEST_F(AutocompleteResultTest, SortAndCullPromoteUnconsecutiveMatches) {
TestData data[] = {
{0, 1, 1300, false}, {1, 1, 1200, true}, {3, 2, 1100, false},
{2, 1, 1000, false}, {3, 3, 900, true}, {4, 1, 800, false},
{3, 4, 700, false},
};
TestSchemeClassifier test_scheme_classifier;
// Check that reorder swaps up a result, and promotes relevance,
// even for a default match that isn't the best.
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::HOME_PAGE,
test_scheme_classifier);
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(5U, result.size());
EXPECT_EQ("http://b/", result.match_at(0)->destination_url.spec());
EXPECT_EQ(1200, result.match_at(0)->relevance);
EXPECT_EQ("http://a/", result.match_at(1)->destination_url.spec());
EXPECT_EQ("http://d/", result.match_at(2)->destination_url.spec());
EXPECT_EQ(1100, result.match_at(2)->relevance);
EXPECT_EQ(GetProvider(3), result.match_at(2)->provider);
EXPECT_EQ("http://c/", result.match_at(3)->destination_url.spec());
EXPECT_EQ("http://e/", result.match_at(4)->destination_url.spec());
}
struct EntityTestData {
AutocompleteMatchType::Type type;
raw_ptr<FakeAutocompleteProvider> provider;
std::string destination_url;
int relevance;
bool allowed_to_be_default_match;
std::string fill_into_edit;
std::string inline_autocompletion;
};
void PopulateEntityTestCases(std::vector<EntityTestData>& test_cases,
ACMatches* matches) {
for (const auto& test_case : test_cases) {
AutocompleteMatch match;
match.provider = test_case.provider;
match.type = test_case.type;
match.destination_url = GURL(test_case.destination_url);
match.relevance = test_case.relevance;
match.allowed_to_be_default_match = test_case.allowed_to_be_default_match;
match.fill_into_edit = base::UTF8ToUTF16(test_case.fill_into_edit);
match.inline_autocompletion =
base::UTF8ToUTF16(test_case.inline_autocompletion);
matches->push_back(match);
}
}
TEST_F(AutocompleteResultTest, SortAndCullPreferEntities) {
// clang-format off
std::vector<EntityTestData> test_cases = {
{
AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 1100, false, "foo", ""
},
{
AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
"http://search/?q=foo", 1000, false, "foo", ""
},
{
AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 900, true, "foo", "oo"
},
// This match will be the first result but it won't affect the entity
// deduping because it has a different URL.
//
// Also keeping this as the default match allows us to test that Entities
// and plain matches are deduplicated when they are not the default match.
// See SortAndCullPreferEntitiesButKeepDefaultPlainMatches for details.
{
AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED, GetProvider(1),
"http://search/?q=bar", 1200, true, "foo", "oo"
},
};
// clang-format on
ACMatches matches;
PopulateEntityTestCases(test_cases, &matches);
AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// The first result will be the personalized suggestion.
EXPECT_EQ(2UL, result.size());
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED,
result.match_at(0)->type);
EXPECT_EQ(1200, result.match_at(0)->relevance);
// The second result will be the result of deduping the other three.
// The chosen match should be the entity suggestion and it should have been
// promoted to receive the first match's relevance and the last match's
// allowed_to_be_default_match and inline_autocompletion values.
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_ENTITY,
result.match_at(1)->type);
EXPECT_EQ(1100, result.match_at(1)->relevance);
EXPECT_TRUE(result.match_at(1)->allowed_to_be_default_match);
EXPECT_EQ(u"oo", result.match_at(1)->inline_autocompletion);
}
TEST_F(AutocompleteResultTest,
SortAndCullPreferNonEntitiesForDefaultSuggestion) {
// When the top scoring allowed_to_be_default suggestion is a search entity,
// and there is a duplicate non-entity search suggest that is also
// allowed_to_be_default, prefer the latter.
std::vector<EntityTestData> test_cases = {
{AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
"http://search/?q=foo", 1000, true},
{AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo2", 1200},
// A duplicate search suggestion should be preferred to a search entity
// suggestion, even if the former is scored lower.
{AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 900, true},
};
ACMatches matches;
PopulateEntityTestCases(test_cases, &matches);
// Simulate the search provider pre-grouping duplicate suggestions. We want
// to make sure `stripped_destination_url` is correctly appended to pre-duped
// suggestions.
matches[0].duplicate_matches.push_back(matches.back());
matches.pop_back();
// Simulate the top match having an `entity_id`.
matches[0].entity_id = "/m/012abc";
AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(result.size(), 3u);
auto* match = result.match_at(0);
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
// Should not inherit the search entity suggestion's relevance; only the
// non-dup suggestion inherits from the dup suggestions; not vice versa.
EXPECT_EQ(match->relevance, 900);
EXPECT_TRUE(match->allowed_to_be_default_match);
EXPECT_EQ(match->stripped_destination_url.spec(), "http://search/?q=foo");
// Expect that the Entity's ID was merged over to the plain match equivalent.
EXPECT_EQ(match->entity_id, "/m/012abc");
match = result.match_at(1);
// The search entity suggestion should be ranked higher than the higher
// scoring 'foo2' search suggestion. When demoting default entity suggestions,
// they are moved to position 2 rather than re-ranked according to their
// relevance.
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
EXPECT_EQ(match->relevance, 1000);
EXPECT_TRUE(match->allowed_to_be_default_match);
EXPECT_EQ(match->stripped_destination_url.spec(), "http://search/?q=foo");
EXPECT_EQ(match->entity_id, "/m/012abc");
match = result.match_at(2);
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
EXPECT_EQ(match->relevance, 1200);
EXPECT_FALSE(match->allowed_to_be_default_match);
EXPECT_EQ(match->stripped_destination_url.spec(), "http://search/?q=foo2");
EXPECT_TRUE(match->entity_id.empty());
}
TEST_F(AutocompleteResultTest,
SortAndCullDontPreferNonEntityNonDefaultForDefaultSuggestion) {
// When the top scoring allowed_to_be_default suggestion is a search entity,
// and there are no duplicate allowed_to_be_default suggestions, keep the
// search entity suggestion default.
std::vector<EntityTestData> test_cases = {
{AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
"http://search/?q=foo", 1000, true},
// A duplicate non-allowed_to_be_default search suggestion should not be
// preferred to a lower ranked search entity suggestion.
{AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 1300},
// A non-duplicate allowed_to_be_default search suggestion should not be
// preferred to a higher ranked search entity suggestion.
{AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo2", 900, true},
};
ACMatches matches;
PopulateEntityTestCases(test_cases, &matches);
AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(result.size(), 2u);
auto* match = result.match_at(0);
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
EXPECT_EQ(match->relevance, 1300);
EXPECT_TRUE(match->allowed_to_be_default_match);
match = result.match_at(1);
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
EXPECT_EQ(match->relevance, 900);
EXPECT_TRUE(match->allowed_to_be_default_match);
}
TEST_F(AutocompleteResultTest, SortAndCullPreferEntitiesFillIntoEditMustMatch) {
// clang-format off
std::vector<EntityTestData> test_cases = {
{
AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED, GetProvider(1),
"http://search/?q=foo", 1100, false, "foo", ""
},
{
AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
"http://search/?q=foo", 1000, false, "foobar", ""
},
{
AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 900, true, "foo", "oo"
},
};
// clang-format on
ACMatches matches;
PopulateEntityTestCases(test_cases, &matches);
AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// The entity suggestion won't be chosen in this case because it has a non-
// matching value for fill_into_edit.
EXPECT_EQ(1UL, result.size());
// But the final type will have the specialized Search History type, since
// that's consumed into the final match during the merge step.
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED,
result.match_at(0)->type);
EXPECT_EQ(1100, result.match_at(0)->relevance);
EXPECT_TRUE(result.match_at(0)->allowed_to_be_default_match);
EXPECT_EQ(u"oo", result.match_at(0)->inline_autocompletion);
}
TEST_F(AutocompleteResultTest,
SortAndCullPreferEntitiesButKeepDefaultPlainMatches) {
// clang-format off
std::vector<EntityTestData> test_cases = {
{
AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 1001, true, "foo", ""
},
{
AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
"http://search/?q=foo", 1000, false, "foo", ""
},
{
AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 900, true, "foo", "oo"
},
};
// clang-format on
ACMatches matches;
PopulateEntityTestCases(test_cases, &matches);
AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// The first result will be a plain match.
EXPECT_EQ(2UL, result.size());
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST, result.match_at(0)->type);
EXPECT_EQ(1001, result.match_at(0)->relevance);
// The second result will be the result of deduping the Suggest Entity with
// the third result. It should have still consumed the inline autocomplete
// and allowed_to_be_default qualities from the other two.
EXPECT_EQ(AutocompleteMatchType::SEARCH_SUGGEST_ENTITY,
result.match_at(1)->type);
EXPECT_EQ(1001, result.match_at(1)->relevance);
EXPECT_TRUE(result.match_at(1)->allowed_to_be_default_match);
EXPECT_EQ(u"oo", result.match_at(1)->inline_autocompletion);
}
TEST_F(
AutocompleteResultTest,
SortAndCullPreferNonEntitySpecializedSearchSuggestionForDefaultSuggestion) {
// When selecting among multiple duplicate non-entity suggestions, prefer
// promoting the one that is a SEARCH_SUGGEST or other "specialized" search
// suggestion.
std::vector<EntityTestData> test_cases = {
// Entity search suggestion.
{AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
"http://search/?q=foo", 1200, true},
// Duplicate non-entity SEARCH_SUGGEST suggestion should be preferred
// above the other options (even when scoring lower).
{AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
"http://search/?q=foo", 1000, true},
// Duplicate non-entity match which is neither a SEARCH_SUGGEST nor a
// "specialized" search suggestion.
{AutocompleteMatchType::SEARCH_HISTORY, GetProvider(1),
"http://search/?q=foo", 1100, true},
};
ACMatches matches;
PopulateEntityTestCases(test_cases, &matches);
// Simulate the search provider pre-grouping duplicate suggestions.
matches[0].duplicate_matches.push_back(matches.back());
matches.pop_back();
matches[0].duplicate_matches.push_back(matches.back());
matches.pop_back();
AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(result.size(), 2u);
auto* match = result.match_at(0);
// The non-entity SEARCH_SUGGEST suggestion should be promoted as the top
// match.
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
EXPECT_TRUE(match->allowed_to_be_default_match);
match = result.match_at(1);
// The entity search suggestion should have been demoted.
EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
EXPECT_TRUE(match->allowed_to_be_default_match);
EXPECT_EQ(match->duplicate_matches.size(), 1u);
// The non-entity match which is neither a SEARCH_SUGGEST nor a "specialized"
// search suggestion should remain in |duplicate_matches| (i.e. it's not
// promoted as the top match).
EXPECT_EQ(match->duplicate_matches.at(0).type,
AutocompleteMatchType::SEARCH_HISTORY);
EXPECT_TRUE(match->duplicate_matches.at(0).allowed_to_be_default_match);
}
TEST_F(AutocompleteResultTest, SortAndCullPromoteDuplicateSearchURLs) {
// Register a template URL that corresponds to 'foo' search engine.
TemplateURLData url_data;
url_data.SetShortName(u"unittest");
url_data.SetKeyword(u"foo");
url_data.SetURL("http://www.foo.com/s?q={searchTerms}");
template_url_service().Add(std::make_unique<TemplateURL>(url_data));
TestData data[] = {
{0, 1, 1300, false}, {1, 1, 1200, true}, {2, 1, 1100, true},
{3, 1, 1000, true}, {4, 2, 900, true},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Note that 0, 2 and 3 will compare equal after stripping.
matches[0].destination_url = GURL("http://www.foo.com/s?q=foo");
matches[1].destination_url = GURL("http://www.foo.com/s?q=foo2");
matches[2].destination_url = GURL("http://www.foo.com/s?q=foo&oq=f");
matches[3].destination_url = GURL("http://www.foo.com/s?q=foo&aqs=0");
matches[4].destination_url = GURL("http://www.foo.com/");
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// We expect the 3rd and 4th results to be removed.
ASSERT_EQ(3U, result.size());
EXPECT_EQ("http://www.foo.com/s?q=foo&oq=f",
result.match_at(0)->destination_url.spec());
EXPECT_EQ(1300, result.match_at(0)->relevance);
EXPECT_EQ("http://www.foo.com/s?q=foo2",
result.match_at(1)->destination_url.spec());
EXPECT_EQ(1200, result.match_at(1)->relevance);
EXPECT_EQ("http://www.foo.com/", result.match_at(2)->destination_url.spec());
EXPECT_EQ(900, result.match_at(2)->relevance);
}
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, SortAndCullFeaturedSearchBeforeStarterPack) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
omnibox::kUIExperimentMaxAutocompleteMatches,
{{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "5"}});
TestData data[] = {
{1, 1, 500, false, {}, AutocompleteMatchType::STARTER_PACK},
{2, 1, 500, false, {}, AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH},
{3, 1, 500, false, {}, AutocompleteMatchType::STARTER_PACK},
{4, 1, 500, false, {}, AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH},
{5, 2, 900, true, {}, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(u"@", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(5U, AutocompleteResult::GetMaxMatches(/*is_zero_suggest=*/false));
const std::array<TestData, 5> expected_data{{
// Default match ranks higher.
{5, 2, 900, true, {}, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED},
// Featured enterprise search.
{2, 1, 500, false, {}, AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH},
{4, 1, 500, false, {}, AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH},
// Starter pack after featured search.
{1, 1, 500, false, {}, AutocompleteMatchType::STARTER_PACK},
{3, 1, 500, false, {}, AutocompleteMatchType::STARTER_PACK},
}};
AssertResultMatches(result, expected_data);
}
#endif
TEST_F(AutocompleteResultTest,
GroupSuggestionsBySearchVsURLHonorsProtectedSuggestions) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
omnibox::kUIExperimentMaxAutocompleteMatches,
{{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "7"}});
TestData data[] = {
{0, 2, 400, true, {}, AutocompleteMatchType::HISTORY_TITLE},
{1, 1, 800, false, {}, AutocompleteMatchType::CLIPBOARD_URL},
{2, 1, 700, false, {}, AutocompleteMatchType::TILE_NAVSUGGEST},
{3, 1, 600, false, {}, AutocompleteMatchType::TILE_SUGGESTION},
{4, 1, 1000, false, {}, AutocompleteMatchType::HISTORY_URL},
{5, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
{6, 1, 800, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResultForTesting result;
result.AppendMatches(matches);
result.GroupSuggestionsBySearchVsURL(std::next(result.matches_.begin()),
result.matches_.end());
auto expected_data = std::to_array<TestData>({
{0, 2, 400, true, {}, AutocompleteMatchType::HISTORY_TITLE},
{1, 1, 800, false, {}, AutocompleteMatchType::CLIPBOARD_URL},
{2, 1, 700, false, {}, AutocompleteMatchType::TILE_NAVSUGGEST},
{3, 1, 600, false, {}, AutocompleteMatchType::TILE_SUGGESTION},
{5, 1, 900, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
{6, 1, 800, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
{4, 1, 1000, false, {}, AutocompleteMatchType::HISTORY_URL},
});
AssertResultMatches(result, base::span<TestData>(expected_data)
.first(AutocompleteResult::GetMaxMatches()));
}
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, GroupSuggestionsByExtension) {
const auto group1 = omnibox::GROUP_UNSCOPED_EXTENSION_1;
const auto group2 = omnibox::GROUP_UNSCOPED_EXTENSION_2;
TestData data[] = {
{0, 5, 400, false, {}, AutocompleteMatchType::HISTORY_TITLE, group1},
{1, 1, 800, false, {}, AutocompleteMatchType::CLIPBOARD_URL},
{2, 1, 700, false, {}, AutocompleteMatchType::TILE_NAVSUGGEST},
{3, 5, 600, false, {}, AutocompleteMatchType::HISTORY_TITLE, group2},
{4, 5, 1000, false, {}, AutocompleteMatchType::HISTORY_TITLE, group1},
{5, 2, 900, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
{6, 5, 800, false, {}, AutocompleteMatchType::HISTORY_TITLE, group2},
};
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1].set_section(
omnibox::SECTION_UNSCOPED_EXTENSION_1);
suggestion_groups_map[group2].set_section(
omnibox::SECTION_UNSCOPED_EXTENSION_2);
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResultForTesting result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
TestData expected_data[] = {
{5, 2, 900, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
{1, 1, 800, false, {}, AutocompleteMatchType::CLIPBOARD_URL},
{2, 1, 700, false, {}, AutocompleteMatchType::TILE_NAVSUGGEST},
{4, 5, 1000, false, {}, AutocompleteMatchType::HISTORY_TITLE, group1},
{0, 5, 400, false, {}, AutocompleteMatchType::HISTORY_TITLE, group1},
{6, 5, 800, false, {}, AutocompleteMatchType::HISTORY_TITLE, group2},
{3, 5, 600, false, {}, AutocompleteMatchType::HISTORY_TITLE, group2},
};
AssertResultMatches(result, expected_data);
}
TEST_F(AutocompleteResultTest, SortAndCullMaxHistoryClusterSuggestions) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(omnibox::kGroupingFrameworkForNonZPS);
// Should limit history cluster suggestions to 1, even if there are no
// alternative suggestions to display.
ACMatches matches;
const AutocompleteMatchTestData data[] = {
{"url_1", AutocompleteMatchType::HISTORY_CLUSTER},
{"url_2", AutocompleteMatchType::HISTORY_CLUSTER},
{"url_3", AutocompleteMatchType::HISTORY_CLUSTER},
};
PopulateAutocompleteMatchesFromTestData(data, std::size(data), &matches);
for (auto& m : matches)
m.allowed_to_be_default_match = false;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result.match_at(0)->type, AutocompleteMatchType::HISTORY_CLUSTER);
}
#endif
TEST_F(AutocompleteResultTest, SortAndCullMaxURLMatches) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{{omnibox::kUIExperimentMaxAutocompleteMatches,
{{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, "6"}}},
{omnibox::kOmniboxMaxURLMatches,
{{OmniboxFieldTrial::kOmniboxMaxURLMatchesParam, "3"}}}},
{omnibox::kGroupingFrameworkForNonZPS});
EXPECT_TRUE(OmniboxFieldTrial::IsMaxURLMatchesFeatureEnabled());
EXPECT_EQ(OmniboxFieldTrial::GetMaxURLMatches(), 3u);
// Case 1: Eject URL match for a search.
// Does not apply to Android and iOS which picks top N matches and performs
// group by search vs URL separately (Adaptive Suggestions).
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
{
ACMatches matches;
const AutocompleteMatchTestData data[] = {
{"http://search-what-you-typed/",
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED},
{"http://search-history/", AutocompleteMatchType::SEARCH_HISTORY},
{"http://history-url/", AutocompleteMatchType::HISTORY_URL},
{"http://history-title/", AutocompleteMatchType::HISTORY_TITLE},
{"http://url-what-you-typed/",
AutocompleteMatchType::URL_WHAT_YOU_TYPED},
{"http://clipboard-url/", AutocompleteMatchType::CLIPBOARD_URL},
{"http://search-suggest/", AutocompleteMatchType::SEARCH_SUGGEST},
};
PopulateAutocompleteMatchesFromTestData(data, std::size(data), &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// Expect the search suggest to be moved about URL suggestions due to
// the logic which groups searches and URLs together.
auto expected_types = std::to_array<AutocompleteMatchType::Type>({
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED,
AutocompleteMatchType::SEARCH_HISTORY,
AutocompleteMatchType::SEARCH_SUGGEST,
AutocompleteMatchType::HISTORY_URL,
AutocompleteMatchType::HISTORY_TITLE,
AutocompleteMatchType::URL_WHAT_YOU_TYPED,
});
EXPECT_EQ(result.size(), AutocompleteResult::GetMaxMatches());
for (size_t i = 0; i < result.size(); ++i)
EXPECT_EQ(result.match_at(i)->type, expected_types[i]);
}
#endif
// Case 2: Do not eject URL match because there's no replacement.
{
ACMatches matches;
const AutocompleteMatchTestData data[] = {
{"http://search-what-you-typed/",
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED},
{"http://search-history/", AutocompleteMatchType::SEARCH_HISTORY},
{"http://history-url/", AutocompleteMatchType::HISTORY_URL},
{"http://history-title/", AutocompleteMatchType::HISTORY_TITLE},
{"http://url-what-you-typed/",
AutocompleteMatchType::URL_WHAT_YOU_TYPED},
{"http://clipboard-url/", AutocompleteMatchType::CLIPBOARD_URL},
{"http://bookmark-title/", AutocompleteMatchType::BOOKMARK_TITLE},
};
PopulateAutocompleteMatchesFromTestData(data, std::size(data), &matches);
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(result.size(), AutocompleteResult::GetMaxMatches());
auto expected_types = std::to_array<AutocompleteMatchType::Type>({
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED,
AutocompleteMatchType::SEARCH_HISTORY,
AutocompleteMatchType::HISTORY_URL,
AutocompleteMatchType::HISTORY_TITLE,
AutocompleteMatchType::URL_WHAT_YOU_TYPED,
AutocompleteMatchType::CLIPBOARD_URL,
});
for (size_t i = 0; i < result.size(); ++i)
EXPECT_EQ(result.match_at(i)->type, expected_types[i]) << i;
}
}
TEST_F(AutocompleteResultTest, ConvertsOpenTabsCorrectly) {
AutocompleteResult result;
ACMatches matches;
AutocompleteMatch match;
match.destination_url = GURL("http://this-site-matches.com");
matches.push_back(match);
match.destination_url = GURL("http://other-site-matches.com");
match.description = u"Some Other Site";
matches.push_back(match);
match.destination_url = GURL("http://doesnt-match.com");
match.description = std::u16string();
matches.push_back(match);
result.AppendMatches(matches);
// Have IsTabOpenWithURL() return true for some URLs.
FakeAutocompleteProviderClient client;
static_cast<FakeTabMatcher&>(const_cast<TabMatcher&>(client.GetTabMatcher()))
.set_url_substring_match("matches");
result.ConvertOpenTabMatches(&client, nullptr);
EXPECT_TRUE(result.match_at(0)->has_tab_match.value_or(false));
EXPECT_TRUE(result.match_at(1)->has_tab_match.value_or(false));
EXPECT_FALSE(result.match_at(2)->has_tab_match.value_or(false));
}
TEST_F(AutocompleteResultTest, AttachesPedals) {
FakeAutocompleteProviderClient client;
std::unordered_map<OmniboxPedalId, scoped_refptr<OmniboxPedal>> pedals;
const auto add = [&](OmniboxPedal* pedal) {
pedals.insert(
std::make_pair(pedal->PedalId(), base::WrapRefCounted(pedal)));
};
add(new TestOmniboxPedalClearBrowsingData());
client.set_pedal_provider(
std::make_unique<OmniboxPedalProvider>(client, std::move(pedals)));
EXPECT_NE(nullptr, client.GetPedalProvider());
AutocompleteResult result;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
// Populate result with test matches.
{
ACMatches matches;
struct TestData : AutocompleteMatchTestData {
std::string contents;
TestData(std::string url,
AutocompleteMatch::Type type,
std::string contents)
: AutocompleteMatchTestData{url, type}, contents(contents) {}
};
const auto data = std::to_array<TestData>({
{"http://clear-history/", AutocompleteMatchType::SEARCH_SUGGEST,
"clear history"},
{"http://search-what-you-typed/",
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED, "search what you typed"},
{"http://search-history/", AutocompleteMatchType::SEARCH_HISTORY,
"search history"},
{"http://history-url/", AutocompleteMatchType::HISTORY_URL,
"history url"},
{"http://history-title/", AutocompleteMatchType::HISTORY_TITLE,
"history title"},
{"http://url-what-you-typed/",
AutocompleteMatchType::URL_WHAT_YOU_TYPED, "url what you typed"},
{"http://clipboard-url/", AutocompleteMatchType::CLIPBOARD_URL,
"clipboard url"},
{"http://bookmark-title/", AutocompleteMatchType::BOOKMARK_TITLE,
"bookmark title"},
{"http://entity-clear-history/",
AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, "clear history"},
});
PopulateAutocompleteMatchesFromTestData(data.data(), std::size(data),
&matches);
for (size_t i = 0; i < std::size(data); i++) {
matches[i].contents = base::UTF8ToUTF16(data[i].contents);
}
result.AppendMatches(matches);
}
// Attach |pedal| to result matches where appropriate.
result.AttachPedalsToMatches(input, client);
// Ensure the entity suggestion doesn't get a pedal even though its contents
// form a concept match.
ASSERT_TRUE(std::prev(result.end())->actions.empty());
// The same concept-matching contents on a non-entity suggestion gets a pedal.
ASSERT_TRUE(!result.begin()->actions.empty());
// Also ensure pedal can be retrieved with generic predicate.
ASSERT_EQ(nullptr,
std::prev(result.end())->GetActionWhere([](const auto& action) {
return true;
}));
ASSERT_NE(nullptr, result.begin()->GetActionWhere([](const auto& action) {
const auto* pedal = OmniboxPedal::FromAction(action.get());
return pedal && pedal->PedalId() == OmniboxPedalId::CLEAR_BROWSING_DATA;
}));
// Android & iOS avoid attaching tab-switch actions by design.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
// Include a tab-switch action, which is common and shouldn't prevent
// pedals from attaching to the same match. The first match has a URL
// that triggers tab-switch action attachment with this fake matcher.
static_cast<FakeTabMatcher&>(const_cast<TabMatcher&>(client.GetTabMatcher()))
.set_url_substring_match("clear-history");
result.match_at(0)->actions.clear();
result.match_at(0)->has_tab_match.reset();
result.ConvertOpenTabMatches(&client, &input);
EXPECT_EQ(result.match_at(0)->actions.size(), 1u);
EXPECT_EQ(result.match_at(0)->GetActionAt(0u)->ActionId(),
OmniboxActionId::TAB_SWITCH);
result.AttachPedalsToMatches(input, client);
EXPECT_EQ(result.match_at(0)->actions.size(), 2u);
ASSERT_NE(nullptr, result.match_at(0)->GetActionWhere([](const auto& action) {
const auto* pedal = OmniboxPedal::FromAction(action.get());
return pedal && pedal->PedalId() == OmniboxPedalId::CLEAR_BROWSING_DATA;
}));
#endif
}
TEST_F(AutocompleteResultTest, DocumentSuggestionsCanMergeButNotToDefault) {
// Types are populated below to avoid introducing a new test data creation
// process.
TestData data[] = {
{1, 4, 500, false}, // DOCUMENT result for url [1].
{1, 1, 1100, false}, // HISTORY result for url [1], higher priority.
{2, 4, 600, false}, // DOCUMENT result for [2].
{2, 1, 1200, true}, // HISTORY result for url [2], higher priority,
// Can be default.
{3, 4, 1000, false}, // DOCUMENT result for [3], higher priority
{3, 1, 400, false}, // HISTORY result for url [3].
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].type = AutocompleteMatchType::DOCUMENT_SUGGESTION;
static_cast<FakeAutocompleteProvider*>(matches[0].provider)->type_ =
AutocompleteProvider::Type::TYPE_DOCUMENT;
matches[1].type = AutocompleteMatchType::HISTORY_URL;
matches[2].type = AutocompleteMatchType::DOCUMENT_SUGGESTION;
static_cast<FakeAutocompleteProvider*>(matches[2].provider)->type_ =
AutocompleteProvider::Type::TYPE_DOCUMENT;
matches[3].type = AutocompleteMatchType::HISTORY_URL;
matches[4].type = AutocompleteMatchType::DOCUMENT_SUGGESTION;
static_cast<FakeAutocompleteProvider*>(matches[4].provider)->type_ =
AutocompleteProvider::Type::TYPE_DOCUMENT;
matches[5].type = AutocompleteMatchType::HISTORY_URL;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// We expect three results:
// The document result for [1] may override the history result.
// The document result for [2] cannot override a potentially-default result.
// The document result for [3] is already higher-priority.
EXPECT_EQ(result.size(), 3u);
// First result should be the default with its original top-ranking score.
EXPECT_EQ(result.match_at(0)->relevance, 1200);
EXPECT_EQ(AutocompleteMatchType::HISTORY_URL, result.match_at(0)->type);
EXPECT_TRUE(result.match_at(0)->allowed_to_be_default_match);
// Second result should be a document result with elevated score.
// The second DOCUMENT result is deduped and effectively dropped.
EXPECT_EQ(result.match_at(1)->relevance, 1100);
EXPECT_EQ(AutocompleteMatchType::DOCUMENT_SUGGESTION,
result.match_at(1)->type);
EXPECT_FALSE(result.match_at(1)->allowed_to_be_default_match);
// Third result should be a document with original score. The history result
// it duped against is lower-priority.
EXPECT_EQ(result.match_at(2)->relevance, 1000);
EXPECT_EQ(AutocompleteMatchType::DOCUMENT_SUGGESTION,
result.match_at(2)->type);
EXPECT_FALSE(result.match_at(2)->allowed_to_be_default_match);
}
TEST_F(AutocompleteResultTest, CalculateNumMatchesPerUrlCountTest) {
CompareWithDemoteByType<AutocompleteMatch> comparison_object(
metrics::OmniboxEventProto::OTHER);
enum SuggestionType { search, url };
auto test = [comparison_object](std::string base_limit,
std::string url_cutoff,
std::string increased_limit,
std::vector<SuggestionType> types,
size_t expected_num_matches) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{{omnibox::kDynamicMaxAutocomplete,
{{OmniboxFieldTrial::kDynamicMaxAutocompleteUrlCutoffParam,
url_cutoff},
{OmniboxFieldTrial::kDynamicMaxAutocompleteIncreasedLimitParam,
increased_limit}}},
{omnibox::kUIExperimentMaxAutocompleteMatches,
{{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, base_limit}}}},
{});
ACMatches matches;
for (auto type : types) {
AutocompleteMatch m;
m.relevance = 100;
if (type)
m.type = AutocompleteMatchType::URL_WHAT_YOU_TYPED;
matches.push_back(m);
}
const size_t num_matches = AutocompleteResult::CalculateNumMatches(
false, AutocompleteInput::FeaturedKeywordMode::kFalse, matches,
comparison_object);
EXPECT_EQ(num_matches, expected_num_matches);
};
test("2", "0", "4", {search}, 1);
test("2", "0", "4", {search, search, search, search, search}, 4);
test("2", "0", "4", {search, search, search, search, url}, 4);
test("2", "0", "4", {search, search, search, url, search}, 3);
test("2", "0", "4", {search, search, url, search, search}, 2);
test("2", "0", "4", {search, url, search, search, search}, 2);
test("2", "1", "4", {search, url, search, search, search}, 4);
test("2", "1", "4", {search, url, search, url, search}, 3);
}
TEST_F(AutocompleteResultTest,
CalculateNumMatchesPerUrlCountWithUnscopedExtensions) {
CompareWithDemoteByType<AutocompleteMatch> comparison_object(
metrics::OmniboxEventProto::OTHER);
enum SuggestionType { search, url };
auto test = [this, comparison_object](
std::string base_limit, std::string url_cutoff,
std::string increased_limit,
std::vector<std::pair<SuggestionType, int>>
suggestion_and_provider_type,
size_t expected_num_matches) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{{omnibox::kDynamicMaxAutocomplete,
{{OmniboxFieldTrial::kDynamicMaxAutocompleteUrlCutoffParam,
url_cutoff},
{OmniboxFieldTrial::kDynamicMaxAutocompleteIncreasedLimitParam,
increased_limit}}},
{omnibox::kUIExperimentMaxAutocompleteMatches,
{{OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam, base_limit}}}},
{});
ACMatches matches;
for (auto pair : suggestion_and_provider_type) {
AutocompleteMatch m;
m.relevance = 100;
if (pair.first) {
m.type = AutocompleteMatchType::URL_WHAT_YOU_TYPED;
}
m.provider = GetProvider(pair.second);
matches.push_back(m);
}
const size_t num_matches = AutocompleteResult::CalculateNumMatches(
false, AutocompleteInput::FeaturedKeywordMode::kFalse, matches,
comparison_object);
EXPECT_EQ(num_matches, expected_num_matches);
};
test("2", "0", "4", {{search, 2}, {search, 2}, {search, 5}}, 3);
test("2", "0", "4",
{{search, 2}, {search, 5}, {search, 2}, {search, 2}, {search, 2}}, 5);
test("2", "1", "4",
{{search, 2}, {url, 2}, {search, 5}, {search, 2}, {url, 2}}, 4);
test("2", "1", "3",
{{search, 2}, {search, 5}, {search, 5}, {search, 2}, {url, 2}}, 5);
test("2", "2", "4",
{{search, 5}, {url, 5}, {search, 5}, {url, 2}, {search, 2}, {search, 2}},
6);
test("2", "1", "4",
{{search, 5}, {search, 5}, {search, 2}, {url, 2}, {url, 2}, {url, 2}},
4);
}
TEST_F(AutocompleteResultTest, ClipboardSuggestionOnTopOfSearchSuggestionTest) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(omnibox::kGroupingFrameworkForNonZPS);
// clang-format off
TestData data[] = {
{1, 1, 500, false},
{2, 2, 1100, false},
{3, 2, 1000, false},
{4, 1, 1300, false},
{5, 1, 1500, false},
};
// clang-format on
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].type = AutocompleteMatchType::SEARCH_SUGGEST;
static_cast<FakeAutocompleteProvider*>(matches[0].provider)->type_ =
AutocompleteProvider::Type::TYPE_ZERO_SUGGEST_LOCAL_HISTORY;
matches[1].type = AutocompleteMatchType::SEARCH_SUGGEST;
static_cast<FakeAutocompleteProvider*>(matches[1].provider)->type_ =
AutocompleteProvider::Type::TYPE_ZERO_SUGGEST_LOCAL_HISTORY;
matches[2].type = AutocompleteMatchType::SEARCH_SUGGEST;
static_cast<FakeAutocompleteProvider*>(matches[2].provider)->type_ =
AutocompleteProvider::Type::TYPE_ZERO_SUGGEST_LOCAL_HISTORY;
matches[3].type = AutocompleteMatchType::SEARCH_SUGGEST;
static_cast<FakeAutocompleteProvider*>(matches[3].provider)->type_ =
AutocompleteProvider::Type::TYPE_ZERO_SUGGEST_LOCAL_HISTORY;
matches[4].type = AutocompleteMatchType::CLIPBOARD_URL;
static_cast<FakeAutocompleteProvider*>(matches[4].provider)->type_ =
AutocompleteProvider::Type::TYPE_CLIPBOARD;
AutocompleteInput input(u"", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
EXPECT_EQ(result.size(), 5u);
EXPECT_EQ(result.match_at(0)->relevance, 1500);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_URL, result.match_at(0)->type);
}
TEST_F(AutocompleteResultTest, MaybeCullTailSuggestions) {
auto test = [&](std::vector<CullTailTestMatch> input_matches) {
ACMatches matches;
std::ranges::transform(input_matches, std::back_inserter(matches),
[&](const CullTailTestMatch& test_match) {
AutocompleteMatch match;
match.contents = test_match.id;
match.type = test_match.type;
match.allowed_to_be_default_match =
test_match.allowed_default;
match.relevance = 1000;
return match;
});
auto page_classification = metrics::OmniboxEventProto::PageClassification::
OmniboxEventProto_PageClassification_OTHER;
AutocompleteResultForTesting::MaybeCullTailSuggestions(
&matches, {page_classification});
std::vector<CullTailTestMatch> output_matches;
std::ranges::transform(
matches, std::back_inserter(output_matches), [](const auto& match) {
return CullTailTestMatch{match.contents, match.type,
match.allowed_to_be_default_match};
});
return output_matches;
};
// When there are no suggestions, should return no suggestions.
EXPECT_THAT(test({}), testing::ElementsAre());
// T = tail, N = non-tail; D = default-able
CullTailTestMatch t{u"T", AutocompleteMatchType::SEARCH_SUGGEST_TAIL, false};
CullTailTestMatch n{u"N", AutocompleteMatchType::SEARCH_SUGGEST, false};
CullTailTestMatch td{u"TD", AutocompleteMatchType::SEARCH_SUGGEST_TAIL, true};
CullTailTestMatch nd{u"ND", AutocompleteMatchType::SEARCH_SUGGEST, true};
// When there are only non-tail suggestions, no suggestions should be culled.
EXPECT_THAT(test({n, n}), testing::ElementsAre(n, n));
EXPECT_THAT(test({nd, nd}), testing::ElementsAre(nd, nd));
EXPECT_THAT(test({nd, n}), testing::ElementsAre(nd, n));
// When there are only tail suggestions, no suggestions should be culled.
EXPECT_THAT(test({t, t}), testing::ElementsAre(t, t));
EXPECT_THAT(test({td, td}), testing::ElementsAre(td, td));
EXPECT_THAT(test({td, t}), testing::ElementsAre(td, t));
// When there is exactly 1 non-tail suggestions and it is default-able, tail
// suggestions should not be culled. But they should be prevented from being
// default.
// A tail suggest that was originally default-able (`td`), but was
// prevented from being default.
CullTailTestMatch tdp{u"TD", AutocompleteMatchType::SEARCH_SUGGEST_TAIL,
false};
EXPECT_THAT(test({nd, t, t}), testing::ElementsAre(nd, t, t));
EXPECT_THAT(test({nd, td, td}), testing::ElementsAre(nd, tdp, tdp));
EXPECT_THAT(test({nd, td, t}), testing::ElementsAre(nd, tdp, t));
// When there is exactly 1 non-tail suggestions and it is not default-able,
// either it or the tail suggestions should be culled, depending on if there
// is a default-able tail suggestion.
EXPECT_THAT(test({n, t, t}), testing::ElementsAre(n));
EXPECT_THAT(test({n, td, td}), testing::ElementsAre(td, td));
EXPECT_THAT(test({n, td, t}), testing::ElementsAre(td, t));
// When there are multiple non-tail suggestions, and at least 1 is
// default-able, tail suggestions should be culled.
EXPECT_THAT(test({nd, n, t}), testing::ElementsAre(nd, n));
EXPECT_THAT(test({nd, n, td, td}), testing::ElementsAre(nd, n));
EXPECT_THAT(test({nd, nd, td, t}), testing::ElementsAre(nd, nd));
// When there are multiple non-tail suggestions, and none of them are
// default-able, either they or the tail suggestions should be called,
// depending on if there is a default-able tail suggestion.
EXPECT_THAT(test({n, n, t, t}), testing::ElementsAre(n, n));
EXPECT_THAT(test({n, n, td, td}), testing::ElementsAre(td, td));
EXPECT_THAT(test({n, n, td, t}), testing::ElementsAre(td, t));
// When there are both history cluster and tail suggestions, history cluster
// suggestions should be hidden.
// A history cluster suggestion.
CullTailTestMatch h{u"H", AutocompleteMatchType::HISTORY_CLUSTER, false};
EXPECT_THAT(test({nd, td, t, h}), testing::ElementsAre(nd, tdp, t));
}
#if !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
// Tests zps grouping for most visited sites and backfills with different
// suggestion limits.
TEST_F(AutocompleteResultTest, Desktop_MostVisitedSitesGrouping) {
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::OmniboxZpsSuggestionLimit>
scoped_config;
scoped_config.Get().enabled = true;
scoped_config.Get().max_suggestions = 8U;
scoped_config.Get().max_url_suggestions = 4U;
scoped_config.Get().max_search_suggestions = 4U;
// Suggestion groups have the omnibox::SECTION_DEFAULT by default.
const auto group1 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group2 = omnibox::GROUP_MOST_VISITED;
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
// Set up input for zero-prefix suggestions from the srp omnibox.
AutocompleteInput omnibox_srp_zps_input(
u"",
metrics::OmniboxEventProto::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT,
TestSchemeClassifier());
omnibox_srp_zps_input.set_focus_type(
metrics::OmniboxFocusType::INTERACTION_FOCUS);
{
SCOPED_TRACE("Query from omnibox in srp");
TestData data[] = {
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{5, 1, 450, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{6, 1, 440, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{7, 1, 430, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{8, 1, 420, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{9, 1, 410, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(omnibox_srp_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// There should be 8 total suggestions, 4 from the group 1 and 4 from group
// 2. Group 2 should follow group 1 since this is a search results page.
const std::array<TestData, 8> expected_data{{
{5, 1, 500, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{6, 1, 490, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{7, 1, 480, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{8, 1, 470, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{0, 1, 450, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{1, 1, 440, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{2, 1, 430, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{3, 1, 420, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
}};
AssertResultMatches(result, expected_data);
}
// Lower max suggestion and max url suggestion amount.
scoped_config.Get().max_suggestions = 6U;
scoped_config.Get().max_url_suggestions = 2U;
// This shouldn't be applied since search results backfill url results.
scoped_config.Get().max_search_suggestions = 2U;
// Set up input for zero-prefix suggestions from a web page.
AutocompleteInput web_zps_input(u"", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
web_zps_input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
{
SCOPED_TRACE("Query from web page");
TestData data[] = {
{0, 1, 500, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{1, 1, 490, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{2, 1, 480, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{3, 1, 470, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{8, 1, 420, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{9, 1, 410, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(web_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// There should be 4 total suggestions, 2 from the group 1 and 2 from group
// 2. Group 1 should follow group 2 since this is a web page.
const std::array<TestData, 4> expected_data{{
{0, 1, 500, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{1, 1, 490, false, {}, AutocompleteMatchType::HISTORY_URL, group2},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_HISTORY, group1},
}};
AssertResultMatches(result, expected_data);
}
}
void VerifyTriggeredFeatures(
OmniboxTriggeredFeatureService* triggered_feature_service,
std::vector<OmniboxTriggeredFeatureService::Feature>
expected_triggered_features) {
OmniboxTriggeredFeatureService::Features features_triggered;
OmniboxTriggeredFeatureService::Features features_triggered_in_session;
triggered_feature_service->RecordToLogs(&features_triggered,
&features_triggered_in_session);
triggered_feature_service->ResetSession();
EXPECT_THAT(features_triggered,
testing::UnorderedElementsAreArray(expected_triggered_features));
EXPECT_THAT(features_triggered_in_session,
testing::UnorderedElementsAreArray(expected_triggered_features));
}
// NOTE: The tests below verify the behavior with the Grouping Framework for ZPS
// enabled. Suggestion groups only make sense within the Grouping Framework.
TEST_F(AutocompleteResultTest, Desktop_TwoColumnRealbox) {
auto remote_secondary_zps_feature =
metrics::OmniboxEventProto_Feature_REMOTE_SECONDARY_ZERO_SUGGEST;
const auto group1 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group2 = omnibox::GROUP_TRENDS;
const auto group3 = omnibox::GROUP_PREVIOUS_SEARCH_RELATED_ENTITY_CHIPS;
TestData data[] = {
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Suggestion groups have the omnibox::SECTION_DEFAULT and
// omnibox::GroupConfig_SideType_DEFAULT_PRIMARY by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
suggestion_groups_map[group3].set_side_type(
omnibox::GroupConfig_SideType_SECONDARY);
// Set up input for zero-prefix suggestions from the omnibox.
AutocompleteInput omnibox_zps_input(
u"",
metrics::OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS,
TestSchemeClassifier());
omnibox_zps_input.set_focus_type(
metrics::OmniboxFocusType::INTERACTION_FOCUS);
{
SCOPED_TRACE("Query from omnibox");
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{},
{omnibox::kGroupingFrameworkForNonZPS, omnibox::kWebUIOmniboxPopup});
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(omnibox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
const std::array<TestData, 5> expected_data{{
// Previous search related suggestion chips are not permitted in the
// omnibox even when the feature is enabled.
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
}};
AssertResultMatches(result, expected_data);
// Verify that the secondary zero-prefix suggestions were not triggered.
VerifyTriggeredFeatures(triggered_feature_service(), {});
}
{
SCOPED_TRACE("Query from WebUI omnibox");
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{{omnibox::kWebUIOmniboxPopup, {}}},
{omnibox::kGroupingFrameworkForNonZPS});
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(omnibox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
const std::array<TestData, 8> expected_data{{
// Previous search related suggestion chips are permitted in the omnibox
// when the WebUI omnibox popup feature is enabled.
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
}};
AssertResultMatches(result, expected_data);
// Verify that the secondary zero-prefix suggestions were triggered.
VerifyTriggeredFeatures(triggered_feature_service(),
{remote_secondary_zps_feature});
}
// Set up input for zero-prefix suggestions from the realbox.
AutocompleteInput realbox_zps_input(
u"", metrics::OmniboxEventProto::NTP_REALBOX, TestSchemeClassifier());
realbox_zps_input.set_focus_type(
metrics::OmniboxFocusType::INTERACTION_FOCUS);
{
SCOPED_TRACE("Query from realbox");
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{},
{omnibox::kGroupingFrameworkForNonZPS, omnibox::kWebUIOmniboxPopup});
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(realbox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
const std::array<TestData, 8> expected_data{{
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
}};
AssertResultMatches(result, expected_data);
// Verify that the secondary zero-prefix suggestions were triggered.
VerifyTriggeredFeatures(triggered_feature_service(),
{remote_secondary_zps_feature});
}
{
SCOPED_TRACE("Query from realbox - no secondary matches");
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{},
{omnibox::kGroupingFrameworkForNonZPS, omnibox::kWebUIOmniboxPopup});
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
// Clear the SideType_SECONDARY from the 3rd group.
result.suggestion_groups_map_[group3].clear_side_type();
result.AppendMatches(matches);
result.SortAndCull(realbox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
const std::array<TestData, 5> expected_data{{
// Previous search related suggestion chips not permitted when their
// `SideType` is not SideType_Secondary.
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
}};
AssertResultMatches(result, expected_data);
// Verify that the secondary zero-prefix suggestions were not triggered.
VerifyTriggeredFeatures(triggered_feature_service(), {});
}
}
TEST_F(AutocompleteResultTest, Desktop_ZpsGroupingIPH) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kStarterPackIPH);
const auto group1 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group2 = omnibox::GROUP_ZERO_SUGGEST_IN_PRODUCT_HELP;
TestData data[] = {
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{8,
4,
420,
false,
{},
AutocompleteMatchType::NULL_RESULT_MESSAGE,
group2,
"",
IphType::kFeaturedEnterpriseSiteSearch},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Suggestion groups have the omnibox::SECTION_DEFAULT by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
// Set up input for zero-prefix suggestions from the omnibox.
AutocompleteInput omnibox_zps_input(
u"",
metrics::OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS,
TestSchemeClassifier());
omnibox_zps_input.set_focus_type(
metrics::OmniboxFocusType::INTERACTION_FOCUS);
{
SCOPED_TRACE("Query from omnibox - with IPH");
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(omnibox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// There should be 8 total suggestions, including the IPH suggestion.
// With the IPH suggestion present, the 8th group1 suggestion should be
// culled in favor of the IPH suggestion.
const std::array<TestData, 8> expected_data{{
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{8,
4,
420,
false,
{},
AutocompleteMatchType::NULL_RESULT_MESSAGE,
group2},
}};
AssertResultMatches(result, expected_data);
}
// Set up input for zero-prefix suggestions from the realbox.
AutocompleteInput realbox_zps_input(
u"", metrics::OmniboxEventProto::NTP_REALBOX, TestSchemeClassifier());
realbox_zps_input.set_focus_type(
metrics::OmniboxFocusType::INTERACTION_FOCUS);
{
SCOPED_TRACE("Query from realbox");
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(realbox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// The IPH suggestion should not be shown in the Realbox, even if it's
// present in the list of matches.
const std::array<TestData, 8> expected_data{{
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
}};
AssertResultMatches(result, expected_data);
}
{
SCOPED_TRACE("Query from omnibox - without IPH");
// Remove the IPH suggestion from the list of matches.
matches.clear();
PopulateAutocompleteMatches(UNSAFE_TODO({data, std::size(data) - 1}),
&matches);
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(omnibox_zps_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
// There should be 8 total suggestions, including the IPH suggestion.
// With the IPH suggestion not present, all suggestion slots should be
// filled with group1 suggestions.
const std::array<TestData, 8> expected_data{{
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{7, 1, 430, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
}};
AssertResultMatches(result, expected_data);
}
}
TEST_F(AutocompleteResultTest, SplitActionsToSuggestions) {
FakeAutocompleteProviderClient client;
std::unordered_map<OmniboxPedalId, scoped_refptr<OmniboxPedal>> pedals;
const auto add = [&](OmniboxPedal* pedal) {
pedals.insert(
std::make_pair(pedal->PedalId(), base::WrapRefCounted(pedal)));
};
add(new TestOmniboxPedalClearBrowsingData());
client.set_pedal_provider(
std::make_unique<OmniboxPedalProvider>(client, std::move(pedals)));
EXPECT_NE(nullptr, client.GetPedalProvider());
AutocompleteResult result;
AutocompleteInput input(u"a", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
{
ACMatches matches;
struct TestData : AutocompleteMatchTestData {
std::string contents;
TestData(std::string url,
AutocompleteMatch::Type type,
std::string contents)
: AutocompleteMatchTestData{url, type}, contents(contents) {}
};
const auto data = std::to_array<TestData>({
{"http://clear-history/", AutocompleteMatchType::SEARCH_SUGGEST,
"clear history"},
{"http://search-what-you-typed/",
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED, "search what you typed"},
{"http://search-history/", AutocompleteMatchType::SEARCH_HISTORY,
"search history"},
{"http://history-url/", AutocompleteMatchType::HISTORY_URL,
"history url"},
});
PopulateAutocompleteMatchesFromTestData(data.data(), std::size(data),
&matches);
for (size_t i = 0; i < std::size(data); i++) {
matches[i].contents = base::UTF8ToUTF16(data[i].contents);
}
result.AppendMatches(matches);
}
// First, the pedal is attached as normal.
result.AttachPedalsToMatches(input, client);
EXPECT_TRUE(!result.begin()->actions.empty());
EXPECT_EQ(nullptr, result.match_at(1)->takeover_action);
EXPECT_EQ(result.size(), 4u);
// Then pedals are split out to dedicated suggestions with takeover action.
// Note that by design, number of results is not changed.
result.SplitActionsToSuggestions();
EXPECT_TRUE(result.begin()->actions.empty());
EXPECT_NE(nullptr, result.match_at(1)->takeover_action);
EXPECT_EQ(result.size(), 4u);
// Now for an artifically exaggerated case with two pedals on one match,
// which doesn't happen naturally but is useful for testing the method.
static_cast<FakeTabMatcher&>(const_cast<TabMatcher&>(client.GetTabMatcher()))
.set_url_substring_match("clear-history");
result.AttachPedalsToMatches(input, client);
EXPECT_EQ(result.match_at(0)->actions.size(), 1u);
result.match_at(0)->has_tab_match.reset();
result.ConvertOpenTabMatches(&client, &input);
EXPECT_EQ(result.match_at(0)->actions.size(), 2u);
EXPECT_EQ(result.match_at(0)->GetActionAt(1u)->ActionId(),
OmniboxActionId::TAB_SWITCH);
result.match_at(0)->actions.push_back(result.match_at(0)->GetActionAt(0u));
EXPECT_EQ(result.match_at(0)->actions.size(), 3u);
// We have three actions: pedal, tab-switch, pedal. Split and ensure
// both pedals became dedicated suggestions. The first one from above
// is still there and is not affected by splitting again.
result.SplitActionsToSuggestions();
EXPECT_EQ(result.match_at(0)->actions.size(), 1u);
EXPECT_EQ(result.match_at(0)->GetActionAt(0u)->ActionId(),
OmniboxActionId::TAB_SWITCH);
EXPECT_EQ(result.match_at(1)->takeover_action->ActionId(),
OmniboxActionId::PEDAL);
EXPECT_EQ(result.match_at(2)->takeover_action->ActionId(),
OmniboxActionId::PEDAL);
EXPECT_EQ(result.match_at(3)->takeover_action->ActionId(),
OmniboxActionId::PEDAL);
EXPECT_EQ(result.size(), 4u);
}
#endif // !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
TEST_F(AutocompleteResultTest, BadDestinationUrls) {
AutocompleteMatch empty_url_match;
empty_url_match.destination_url = GURL();
AutocompleteMatch invalid_scheme_match;
invalid_scheme_match.destination_url = GURL("bad_scheme");
AutocompleteMatch invalid_url_match;
invalid_url_match.destination_url = GURL("https://?k=v");
ACMatches invalid_matches;
invalid_matches.push_back(empty_url_match);
invalid_matches.push_back(invalid_scheme_match);
invalid_matches.push_back(invalid_url_match);
AutocompleteResult result;
EXPECT_DCHECK_DEATH(result.AppendMatches(invalid_matches));
// Result set of matches should remain empty.
EXPECT_TRUE(result.empty());
}
#if BUILDFLAG(IS_ANDROID)
TEST_F(AutocompleteResultTest, Android_InspireMe) {
const auto group1 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group2 = omnibox::GROUP_TRENDS;
const auto group3 = omnibox::GROUP_PREVIOUS_SEARCH_RELATED;
TestData data[] = {
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{5, 1, 450, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{6, 1, 440, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Suggestion groups have the omnibox::SECTION_DEFAULT and
// omnibox::GroupConfig_SideType_DEFAULT_PRIMARY by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
suggestion_groups_map[group3];
// Set up input for zero-prefix suggestions.
AutocompleteInput zero_input(u"", metrics::OmniboxEventProto::NTP,
TestSchemeClassifier());
zero_input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
// NOTE:
// The tests below verify the behavior with the Grouping Framework for ZPS
// enabled. This is intentional: Suggestion Groups make no sense outside of
// the grouping framework.
{
SCOPED_TRACE("Inspire Me Passes Only Trending Queries");
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(zero_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
const std::array<TestData, 5> expected_data{{
// Default suggestion comes 1st.
{1, 1, 490, true, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
// Other types include all of the Inspire Me queries.
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
}};
AssertResultMatches(result, expected_data);
}
}
#endif // BUILDFLAG(IS_ANDROID)
TEST_F(AutocompleteResultTest, Android_UndedupTopSearch) {
scoped_refptr<FakeAutocompleteProvider> provider =
new FakeAutocompleteProvider(AutocompleteProvider::Type::TYPE_SEARCH);
// 4 different matches to cover variety of scenarios.
// Matches are recognized by their type and actions presence.
// `search` is marked as a duplicate of both `entity` matches.
AutocompleteMatch what_you_typed(
provider.get(), 1, false, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED);
AutocompleteMatch search(provider.get(), 1, false,
AutocompleteMatchType::SEARCH_SUGGEST);
search.allowed_to_be_default_match = true;
AutocompleteMatch entity_without_action(
provider.get(), 1, false, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
AutocompleteMatch entity_with_action(
provider.get(), 1, false, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
entity_with_action.actions.push_back(base::MakeRefCounted<FakeOmniboxAction>(
OmniboxActionId::ACTION_IN_SUGGEST));
entity_with_action.duplicate_matches.push_back(search);
entity_without_action.duplicate_matches.push_back(search);
struct UndedupTestData {
std::string test_name;
std::vector<AutocompleteMatch> input;
std::vector<AutocompleteMatch> expected_result;
} test_cases[]{
{"no op with no matches", {}, {}},
{"no op with no entities / 1", {what_you_typed}, {what_you_typed}},
{"no op with no entities / 2",
{what_you_typed, search},
{what_you_typed, search}},
{"no op with entities with no actions",
{what_you_typed, entity_without_action},
{what_you_typed, entity_without_action}},
{"no op with entities with actions at low positions",
{what_you_typed, entity_with_action},
{what_you_typed, entity_with_action}},
{"no rotation when promotion is enabled with no actions at top position",
{entity_without_action},
{search, entity_without_action}},
{"rotation when promotion is enabled with actions at top position",
{entity_with_action},
{entity_with_action, search}},
};
// Crete matches following the `input_matches_and_actions` input.
// The input specifies what type of OMNIBOX_ACTION should be added to every
// individual match.
// Once done, run the trimming and verify that the output contains exactly the
// matches we want to see.
for (const auto& test_case : test_cases) {
auto result = test_case.input;
AutocompleteResult::UndedupTopSearchEntityMatch(&result);
EXPECT_EQ(result.size(), test_case.expected_result.size());
for (size_t index = 0u; index < result.size(); ++index) {
const auto& found_match = result[index];
const auto& expect_match = test_case.expected_result[index];
EXPECT_EQ(found_match.type, expect_match.type)
<< "at index " << index
<< " while testing variant: " << test_case.test_name;
EXPECT_EQ(found_match.actions.size(), expect_match.actions.size())
<< "at index " << index
<< " while testing variant: " << test_case.test_name;
for (size_t action_index = 0u; action_index < found_match.actions.size();
++action_index) {
EXPECT_EQ(found_match.actions[action_index]->ActionId(),
expect_match.actions[action_index]->ActionId())
<< "action " << action_index << " at index " << index
<< " while testing variant: " << test_case.test_name;
}
}
}
}
#if BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, IOS_InspireMe) {
const auto group1 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group2 = omnibox::GROUP_TRENDS;
TestData data[] = {
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
// Suggestion groups have the omnibox::SECTION_DEFAULT and
// omnibox::GroupConfig_SideType_DEFAULT_PRIMARY by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
// Set up input for zero-prefix suggestions.
AutocompleteInput zero_input(u"", metrics::OmniboxEventProto::NTP,
TestSchemeClassifier());
zero_input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
base::test::ScopedFeatureList feature_list;
AutocompleteResult result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(zero_input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
const std::array<TestData, 5> expected_data{{
{0, 1, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{1, 1, 490, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{2, 1, 480, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group1},
{3, 1, 470, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 1, 460, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
}};
AssertResultMatches(result, expected_data);
}
#endif
#if (BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
TEST_F(AutocompleteResultTest, Mobile_TrimOmniboxActions) {
scoped_refptr<FakeAutocompleteProvider> provider =
new FakeAutocompleteProvider(AutocompleteProvider::Type::TYPE_SEARCH);
using OmniboxActionId::ACTION_IN_SUGGEST;
using OmniboxActionId::ANSWER_ACTION;
using OmniboxActionId::PEDAL;
using OmniboxActionId::UNKNOWN;
const std::set<OmniboxActionId> all_actions_to_test{ACTION_IN_SUGGEST, PEDAL};
struct FilterOmniboxActionsTestData {
std::string test_name;
std::vector<std::vector<OmniboxActionId>> input_matches_and_actions;
std::vector<std::vector<OmniboxActionId>> result_matches_and_actions_zps;
std::vector<std::vector<OmniboxActionId>> result_matches_and_actions_typed;
bool include_url = false;
} test_cases[]{
{"No actions attached to matches",
{{}, {}, {}, {}},
{{}, {}, {}, {}},
{{}, {}, {}, {}}},
{"Pedals shown only in top three slots",
{{PEDAL}, {PEDAL}, {PEDAL}, {PEDAL}},
// ZPS
{{PEDAL}, {PEDAL}, {PEDAL}, {}},
// Typed
{{PEDAL}, {PEDAL}, {PEDAL}, {}}},
{"Actions are shown only in first position",
{{ACTION_IN_SUGGEST},
{ACTION_IN_SUGGEST},
{ACTION_IN_SUGGEST},
{ACTION_IN_SUGGEST}},
// ZPS
{{}, {}, {}, {}},
// Typed
{{ACTION_IN_SUGGEST}, {}, {}, {}}},
{"Actions are promoted over Pedals; positions dictate preference",
{{ACTION_IN_SUGGEST, PEDAL},
{ACTION_IN_SUGGEST, PEDAL},
{ACTION_IN_SUGGEST, PEDAL},
{ACTION_IN_SUGGEST, PEDAL}},
// ZPS
{{PEDAL}, {PEDAL}, {PEDAL}, {}},
// Typed
{{ACTION_IN_SUGGEST}, {PEDAL}, {PEDAL}, {}}},
{"Actions are promoted over History clusters; positions dictate "
"preference",
{{ACTION_IN_SUGGEST, PEDAL},
{ACTION_IN_SUGGEST, PEDAL},
{ACTION_IN_SUGGEST, PEDAL},
{ACTION_IN_SUGGEST, PEDAL}},
// ZPS
{{PEDAL}, {PEDAL}, {PEDAL}, {}},
// Typed
{{ACTION_IN_SUGGEST}, {PEDAL}, {PEDAL}, {}}},
{"Answer actions promoted over pedals; can go in any position",
{{ANSWER_ACTION, PEDAL},
{ANSWER_ACTION, PEDAL},
{ANSWER_ACTION, PEDAL},
{ANSWER_ACTION, PEDAL}},
// ZPS
{{ANSWER_ACTION}, {ANSWER_ACTION}, {ANSWER_ACTION}, {ANSWER_ACTION}},
// Typed
{{ANSWER_ACTION}, {ANSWER_ACTION}, {ANSWER_ACTION}, {ANSWER_ACTION}}},
{"Answer actions suppressed when there are urls",
{{PEDAL, ANSWER_ACTION},
{ANSWER_ACTION},
{ANSWER_ACTION},
{ANSWER_ACTION}},
// ZPS
{{PEDAL}, {}, {}, {}},
// Typed
{{PEDAL}, {}, {}, {}},
/* include_url= */ true},
};
// Crete matches following the `input_matches_and_actions` input.
// The input specifies what type of OMNIBOX_ACTION should be added to every
// individual match.
// Once done, run the trimming and verify that the output contains exactly the
// matches we want to see.
auto run_test = [&](const FilterOmniboxActionsTestData& data) {
// Create AutocompleteResult from the test data
AutocompleteResult zps_result;
AutocompleteResult typed_result;
for (const auto& actions : data.input_matches_and_actions) {
AutocompleteMatch match(
provider.get(), 1, false,
data.include_url ? AutocompleteMatchType::URL_WHAT_YOU_TYPED
: AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
for (auto& action_id : actions) {
if (action_id == OmniboxActionId::ACTION_IN_SUGGEST) {
omnibox::SuggestTemplateInfo::TemplateAction action;
action.set_action_type(
omnibox::
SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS);
match.actions.push_back(base::MakeRefCounted<OmniboxActionInSuggest>(
std::move(action), std::nullopt));
} else {
match.actions.push_back(
base::MakeRefCounted<FakeOmniboxAction>(action_id));
}
}
zps_result.AppendMatches({match});
typed_result.AppendMatches({match});
}
auto check_results =
[&](AutocompleteResult& result,
std::vector<std::vector<OmniboxActionId>> expected_actions) {
// Check results.
EXPECT_EQ(result.size(), expected_actions.size())
<< "while testing variant: " << data.test_name;
for (size_t index = 0u; index < result.size(); ++index) {
const auto* match = result.match_at(index);
const auto& expected_actions_at_position = expected_actions[index];
EXPECT_EQ(match->actions.size(),
expected_actions_at_position.size());
for (size_t action_index = 0u;
action_index < expected_actions_at_position.size();
++action_index) {
EXPECT_EQ(expected_actions_at_position[action_index],
match->actions[action_index]->ActionId())
<< "match " << index << "action " << action_index
<< " while testing variant: " << data.test_name;
}
}
};
// Run the trimmer. ZPS, then typed.
zps_result.TrimOmniboxActions(true);
check_results(zps_result, data.result_matches_and_actions_zps);
typed_result.TrimOmniboxActions(false);
check_results(typed_result, data.result_matches_and_actions_typed);
};
for (const auto& test_case : test_cases) {
run_test(test_case);
}
}
#endif
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, ContextualSearchAblateOthers) {
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::ContextualSearch>
contextual_scoped_config;
contextual_scoped_config.Get().contextual_zps_limit = 3U;
contextual_scoped_config.Get().show_open_lens_action = true;
contextual_scoped_config.Get()
.contextual_suggestions_ablate_others_when_present = true;
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::OmniboxZpsSuggestionLimit>
zps_scoped_config;
zps_scoped_config.Get().enabled = true;
zps_scoped_config.Get().max_search_suggestions = 4U;
zps_scoped_config.Get().max_url_suggestions = 4U;
zps_scoped_config.Get().max_suggestions = 8U;
const auto group1 = omnibox::GROUP_MOST_VISITED;
const auto group2 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group3 = omnibox::GROUP_CONTEXTUAL_SEARCH;
TestData data[] = {
{0, 1, 1000, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{1, 1, 900, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{2, 2, 800, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{3, 2, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 2, 600, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{5, 2, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
};
// Suggestion groups have the omnibox::SECTION_DEFAULT by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
suggestion_groups_map[group3];
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
for (AutocompleteMatch& m : matches) {
if (m.suggestion_group_id && m.suggestion_group_id.value() == group3) {
m.subtypes.insert(omnibox::SuggestSubtype::SUBTYPE_CONTEXTUAL_SEARCH);
}
}
AutocompleteInput input(u"", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
AutocompleteResultForTesting result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/true,
/*mia_enabled=*/false);
// Non-contextual search & URL suggestions should have been ablated.
const std::array<TestData, 2> expected_data{{
{4, 2, 600, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{5, 2, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
}};
AssertResultMatches(result, expected_data);
}
TEST_F(AutocompleteResultTest, ContextualSearchAblateOthers_AblateSearchOnly) {
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::ContextualSearch>
contextual_scoped_config;
contextual_scoped_config.Get().contextual_zps_limit = 3U;
contextual_scoped_config.Get().show_open_lens_action = true;
contextual_scoped_config.Get()
.contextual_suggestions_ablate_others_when_present = true;
contextual_scoped_config.Get().contextual_suggestions_ablate_search_only =
true;
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::OmniboxZpsSuggestionLimit>
zps_scoped_config;
zps_scoped_config.Get().enabled = true;
zps_scoped_config.Get().max_search_suggestions = 4U;
zps_scoped_config.Get().max_url_suggestions = 4U;
zps_scoped_config.Get().max_suggestions = 8U;
const auto group1 = omnibox::GROUP_MOST_VISITED;
const auto group2 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group3 = omnibox::GROUP_CONTEXTUAL_SEARCH;
TestData data[] = {
{0, 1, 1000, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{1, 1, 900, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{2, 2, 800, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{3, 2, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 2, 600, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{5, 2, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
};
// Suggestion groups have the omnibox::SECTION_DEFAULT by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
suggestion_groups_map[group3];
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
for (AutocompleteMatch& m : matches) {
if (m.suggestion_group_id && m.suggestion_group_id.value() == group3) {
m.subtypes.insert(omnibox::SuggestSubtype::SUBTYPE_CONTEXTUAL_SEARCH);
}
}
AutocompleteInput input(u"", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
AutocompleteResultForTesting result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/true,
/*mia_enabled=*/false);
// Only non-contextual search suggestions should have been ablated.
const std::array<TestData, 4> expected_data{{
{0, 1, 1000, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{1, 1, 900, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{4, 2, 600, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{5, 2, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
}};
AssertResultMatches(result, expected_data);
}
TEST_F(AutocompleteResultTest, ContextualSearchAblateOthers_AblateUrlOnly) {
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::ContextualSearch>
contextual_scoped_config;
contextual_scoped_config.Get().contextual_zps_limit = 3U;
contextual_scoped_config.Get().show_open_lens_action = true;
contextual_scoped_config.Get()
.contextual_suggestions_ablate_others_when_present = true;
contextual_scoped_config.Get().contextual_suggestions_ablate_url_only = true;
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::OmniboxZpsSuggestionLimit>
zps_scoped_config;
zps_scoped_config.Get().enabled = true;
zps_scoped_config.Get().max_search_suggestions = 4U;
zps_scoped_config.Get().max_url_suggestions = 4U;
zps_scoped_config.Get().max_suggestions = 8U;
const auto group1 = omnibox::GROUP_MOST_VISITED;
const auto group2 = omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST;
const auto group3 = omnibox::GROUP_CONTEXTUAL_SEARCH;
TestData data[] = {
{0, 1, 1000, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{1, 1, 900, false, {}, AutocompleteMatchType::HISTORY_URL, group1},
{2, 2, 800, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{3, 2, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 2, 600, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{5, 2, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
};
// Suggestion groups have the omnibox::SECTION_DEFAULT by default.
omnibox::GroupConfigMap suggestion_groups_map;
suggestion_groups_map[group1];
suggestion_groups_map[group2];
suggestion_groups_map[group3];
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
for (AutocompleteMatch& m : matches) {
if (m.suggestion_group_id && m.suggestion_group_id.value() == group3) {
m.subtypes.insert(omnibox::SuggestSubtype::SUBTYPE_CONTEXTUAL_SEARCH);
}
}
AutocompleteInput input(u"", metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
AutocompleteResultForTesting result;
result.MergeSuggestionGroupsMap(suggestion_groups_map);
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/true,
/*mia_enabled=*/false);
// Only URL suggestions should have been ablated.
const std::array<TestData, 4> expected_data{{
{2, 2, 800, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{3, 2, 700, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group2},
{4, 2, 600, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
{5, 2, 500, false, {}, AutocompleteMatchType::SEARCH_SUGGEST, group3},
}};
AssertResultMatches(result, expected_data);
}
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, AttachAimAction) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kOmniboxAimShortcutTypedState);
TestData data[] = {
{0, 1, 1300, true, {}, AutocompleteMatchType::SEARCH_SUGGEST},
{1, 1, 1200, false, {}, AutocompleteMatchType::SEARCH_SUGGEST},
};
ACMatches matches;
PopulateAutocompleteMatches(data, &matches);
matches[0].contents = u"eligible for aim action";
AutocompleteInput input(u"eligible for aim action",
metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
AutocompleteResult result;
result.AppendMatches(matches);
result.SortAndCull(input, &template_url_service(),
triggered_feature_service(), /*is_lens_active=*/false,
/*can_show_contextual_suggestions=*/false,
/*mia_enabled*/ false);
ASSERT_EQ(2U, result.size());
EXPECT_TRUE(result.match_at(0)->actions.empty());
EXPECT_TRUE(result.match_at(1)->actions.empty());
result.AttachAimAction(&template_url_service());
EXPECT_EQ(1U, result.match_at(0)->actions.size());
const auto* action_in_suggest =
OmniboxActionInSuggest::FromAction(result.match_at(0)->actions[0].get());
ASSERT_TRUE(action_in_suggest);
EXPECT_EQ(action_in_suggest->template_action.action_type(),
omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM);
EXPECT_TRUE(result.match_at(1)->actions.empty());
}
#endif // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, AttachContextualSearchOpenLensActionToMatches) {
AutocompleteResult result;
ACMatches matches;
// Match 1: Contextual search suggestion with Lens action.
AutocompleteMatch match1;
match1.subtypes.insert(omnibox::SuggestSubtype::SUBTYPE_CONTEXTUAL_SEARCH);
match1.suggest_template = omnibox::SuggestTemplateInfo();
auto* action1 = match1.suggest_template->add_action_suggestions();
action1->set_action_type(
omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS);
matches.push_back(match1);
// Match 2: Contextual search suggestion without Lens action.
AutocompleteMatch match2;
match2.subtypes.insert(omnibox::SuggestSubtype::SUBTYPE_CONTEXTUAL_SEARCH);
matches.push_back(match2);
// Match 3: Non-contextual search suggestion with Lens action.
AutocompleteMatch match3;
match3.suggest_template = omnibox::SuggestTemplateInfo();
auto* action3 = match3.suggest_template->add_action_suggestions();
action3->set_action_type(
omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS);
matches.push_back(match3);
// Match 4: Non-contextual search suggestion without Lens action.
AutocompleteMatch match4;
matches.push_back(match4);
result.AppendMatches(matches);
result.AttachContextualSearchOpenLensActionToMatches();
ASSERT_EQ(4u, result.size());
// Match 1 should have the takeover action.
EXPECT_TRUE(result.match_at(0)->takeover_action);
EXPECT_EQ(result.match_at(0)->takeover_action->ActionId(),
OmniboxActionId::CONTEXTUAL_SEARCH_OPEN_LENS);
// Others should not.
EXPECT_FALSE(result.match_at(1)->takeover_action);
EXPECT_FALSE(result.match_at(2)->takeover_action);
EXPECT_FALSE(result.match_at(3)->takeover_action);
}