chromium/components/omnibox/browser/autocomplete_result_unittest.cc

// 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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "components/omnibox/browser/autocomplete_result.h"

#include <stddef.h>

#include <iterator>
#include <memory>
#include <string>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.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_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/entity_info.pb.h"
#include "third_party/omnibox_proto/groups.pb.h"
#include "third_party/omnibox_proto/types.pb.h"
#include "ui/base/device_form_factor.h"

OmniboxEventProto;

namespace {
class FakeOmniboxAction : public OmniboxAction {};

struct AutocompleteMatchTestData {};

// Adds |count| AutocompleteMatches to |matches|.
template <typename T>
void PopulateAutocompleteMatchesFromTestData(const T* data,
                                             size_t count,
                                             ACMatches* matches) {}

// Basic match representation for testing `MaybeCullTailSuggestions()`.
// Defined externally to allow for `PrintTo()`.
struct CullTailTestMatch {};

}  // namespace

class AutocompleteResultForTesting : public AutocompleteResult {};

class AutocompleteResultTest : public testing::Test {};

AutocompleteMatch AutocompleteResultTest::PopulateAutocompleteMatch(
    const TestData& data) {}

void AutocompleteResultTest::PopulateAutocompleteMatches(const TestData* data,
                                                         size_t count,
                                                         ACMatches* matches) {}

ACMatches AutocompleteResultTest::PopulateAutocompleteMatches(
    const std::vector<TestData>& data) {}

void AutocompleteResultTest::AssertResultMatches(
    const AutocompleteResult& result,
    base::span<const TestData> expected) {}

void AutocompleteResultTest::AssertMatch(AutocompleteMatch match,
                                         const TestData& expected_match_data,
                                         int i) {}

void AutocompleteResultTest::RunTransferOldMatchesTest(const TestData* last,
                                                       size_t last_size,
                                                       const TestData* current,
                                                       size_t current_size,
                                                       const TestData* expected,
                                                       size_t expected_size) {}

void AutocompleteResultTest::RunTransferOldMatchesTest(
    const TestData* last,
    size_t last_size,
    const TestData* current,
    size_t current_size,
    const TestData* expected,
    size_t expected_size,
    AutocompleteInput input) {}

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[]) {}

// Assertion testing for AutocompleteResult::SwapMatchesWith.
TEST_F(AutocompleteResultTest, SwapMatches) {}

TEST_F(AutocompleteResultTest, AlternateNavUrl) {}

TEST_F(AutocompleteResultTest, AlternateNavUrl_IntranetRedirectPolicy) {}

// 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) {}

// 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) {}

// 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) {}

// Tests that matches are copied correctly from two distinct providers.
TEST_F(AutocompleteResultTest, TransferOldMatchesMultipleProviders) {}

// 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) {}

// Tests that transferred matches do not include the specialized match types.
TEST_F(AutocompleteResultTest, TransferOldMatchesSkipsSpecializedSuggestions) {}

// Tests that transferred matches do not include the specialized match types.
TEST_F(AutocompleteResultTest, TransferOldMatchesSkipDoneProviders) {}

// Tests that matches with empty destination URLs aren't treated as duplicates
// and culled.
TEST_F(AutocompleteResultTest, SortAndCullEmptyDestinationURLs) {}

#if !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
// Tests which remove results only work on desktop.

TEST_F(AutocompleteResultTest, SortAndCullTailSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullKeepDefaultTailSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullKeepMoreDefaultTailSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullZeroRelevanceSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullZeroRelevanceDefaultMatches) {}

#endif

TEST_F(AutocompleteResultTest, SortAndCullOnlyTailSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullNoMatchesAllowedToBeDefault) {}

TEST_F(AutocompleteResultTest, SortAndCullDuplicateSearchURLs) {}

TEST_F(AutocompleteResultTest, SortAndCullWithMatchDups) {}

TEST_F(AutocompleteResultTest, SortAndCullWithDemotionsByType) {}

TEST_F(AutocompleteResultTest, SortAndCullWithPreserveDefaultMatch) {}

TEST_F(AutocompleteResultTest, DemoteOnDeviceSearchSuggestions) {}

TEST_F(AutocompleteResultTest, DemoteByType) {}

TEST_F(AutocompleteResultTest, SortAndCullReorderForDefaultMatch) {}

// Note: DCHECKs not firing on Cast.
#if DCHECK_IS_ON()
TEST_F(AutocompleteResultTest, SortAndCullFailsWithIncorrectDefaultScheme) {}
#endif

TEST_F(AutocompleteResultTest, SortAndCullPermitSearchForSchemeMatching) {}

TEST_F(AutocompleteResultTest, SortAndCullPromoteDefaultMatch) {}

TEST_F(AutocompleteResultTest, SortAndCullPromoteUnconsecutiveMatches) {}

struct EntityTestData {};

void PopulateEntityTestCases(std::vector<EntityTestData>& test_cases,
                             ACMatches* matches) {}

TEST_F(AutocompleteResultTest, SortAndCullPreferEntities) {}

TEST_F(AutocompleteResultTest,
       SortAndCullPreferNonEntitiesForDefaultSuggestion) {}

TEST_F(AutocompleteResultTest,
       SortAndCullDontPreferNonEntityNonDefaultForDefaultSuggestion) {}

TEST_F(AutocompleteResultTest, SortAndCullPreferEntitiesFillIntoEditMustMatch) {}

TEST_F(AutocompleteResultTest,
       SortAndCullPreferEntitiesButKeepDefaultPlainMatches) {}

TEST_F(
    AutocompleteResultTest,
    SortAndCullPreferNonEntitySpecializedSearchSuggestionForDefaultSuggestion) {}

TEST_F(AutocompleteResultTest, SortAndCullPromoteDuplicateSearchURLs) {}

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
TEST_F(AutocompleteResultTest, SortAndCullFeaturedSearchBeforeStarterPack) {}
#endif

TEST_F(AutocompleteResultTest,
       GroupSuggestionsBySearchVsURLHonorsProtectedSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullMaxHistoryClusterSuggestions) {}

TEST_F(AutocompleteResultTest, SortAndCullMaxURLMatches) {}

TEST_F(AutocompleteResultTest, ConvertsOpenTabsCorrectly) {}

TEST_F(AutocompleteResultTest, AttachesPedals) {}

TEST_F(AutocompleteResultTest, DocumentSuggestionsCanMergeButNotToDefault) {}

TEST_F(AutocompleteResultTest, CalculateNumMatchesPerUrlCountTest) {}

TEST_F(AutocompleteResultTest, ClipboardSuggestionOnTopOfSearchSuggestionTest) {}

TEST_F(AutocompleteResultTest, MaybeCullTailSuggestions) {}

#if !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))

void VerifyTriggeredFeatures(
    OmniboxTriggeredFeatureService* triggered_feature_service,
    std::vector<OmniboxTriggeredFeatureService::Feature>
        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) {}

TEST_F(AutocompleteResultTest, Desktop_ZpsGroupingIPH) {}

TEST_F(AutocompleteResultTest, SplitActionsToSuggestions) {}

#endif  // !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))

#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, std::size(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());

    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) {}

#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, std::size(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);

  {
    SCOPED_TRACE("Trend suggestions are only available on iPhones");
    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());

    if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
      // Ipads should keep the default config.
      const std::array<TestData, 3> 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},
      }};
      AssertResultMatches(result, expected_data);
    } else {
      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::ActionInfo info;
          info.set_action_type(omnibox::ActionInfo_ActionType_DIRECTIONS);
          match.actions.push_back(base::MakeRefCounted<OmniboxActionInSuggest>(
              std::move(info), 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