chromium/chrome/browser/ash/app_list/search/ranking/answer_ranker_unittest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/app_list/search/ranking/answer_ranker.h"

#include "base/strings/string_number_conversions.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/common/icon_constants.h"
#include "chrome/browser/ash/app_list/search/test/test_result.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace app_list::test {
namespace {

bool AnswerFieldsAreSet(const std::unique_ptr<ChromeSearchResult>& result) {
  return result->display_type() == ash::SearchResultDisplayType::kAnswerCard &&
         result->multiline_title() &&
         result->icon().dimension == kAnswerCardIconDimension &&
         !result->scoring().filtered();
}

}  // namespace

class AnswerRankerTest : public testing::Test {
 public:
  Results MakeOmniboxCandidates(std::vector<double> relevances) {
    Results results;
    for (const double relevance : relevances) {
      // |id| and |normalized_relevance| must be set but are not used.
      results.push_back(std::make_unique<TestResult>(
          /*id=*/base::NumberToString(next_id++), relevance,
          /*normalized_relevance=*/0.0,
          ash::SearchResultDisplayType::kAnswerCard, false));
    }
    return results;
  }

  Results MakeShortcutCandidates(std::vector<bool> best_matches) {
    Results results;
    for (const double best_match : best_matches) {
      // |id| and |normalized_relevance| must be set but are not used.
      results.push_back(std::make_unique<TestResult>(
          /*id=*/base::NumberToString(next_id++),
          /*relevance=*/1, /*normalized_relevance=*/0.0,
          ash::SearchResultDisplayType::kList, best_match));
    }
    return results;
  }

 private:
  int next_id = 0;
};

// Tests that the best Omnibox answer is selected and all others are filtered
// out.
TEST_F(AnswerRankerTest, SelectAndFilterOmnibox) {
  ResultsMap results_map;
  results_map[ResultType::kOmnibox] = MakeOmniboxCandidates({0.3, 0.5, 0.4});

  AnswerRanker ranker;
  ranker.UpdateResultRanks(results_map, ProviderType::kOmnibox);
  ranker.OnBurnInPeriodElapsed();

  const auto& results = results_map[ResultType::kOmnibox];
  ASSERT_EQ(results.size(), 3u);

  // The highest scoring Omnibox answer is selected.
  EXPECT_TRUE(AnswerFieldsAreSet(results[1]));

  // Others are filtered out.
  EXPECT_TRUE(results[0]->scoring().filtered());
  EXPECT_TRUE(results[2]->scoring().filtered());
}

// Tests that a best match shortcut is selected.
TEST_F(AnswerRankerTest, SelectBestShortcut) {
  ResultsMap results_map;
  results_map[ResultType::kKeyboardShortcut] =
      MakeShortcutCandidates({false, true});

  AnswerRanker ranker;
  ranker.UpdateResultRanks(results_map, ProviderType::kKeyboardShortcut);
  ranker.OnBurnInPeriodElapsed();

  const auto& results = results_map[ResultType::kKeyboardShortcut];
  ASSERT_EQ(results.size(), 2u);

  // The best match shortcut is selected.
  EXPECT_TRUE(AnswerFieldsAreSet(results[1]));

  EXPECT_NE(results[0]->display_type(),
            ash::SearchResultDisplayType::kAnswerCard);
}

// Tests that no shortcut answers are selected if there are multiple best
// matches.
TEST_F(AnswerRankerTest, OnlySelectIfOneBestShortcut) {
  ResultsMap results_map;
  results_map[ResultType::kKeyboardShortcut] =
      MakeShortcutCandidates({true, true});

  AnswerRanker ranker;
  ranker.UpdateResultRanks(results_map, ProviderType::kKeyboardShortcut);
  ranker.OnBurnInPeriodElapsed();

  const auto& results = results_map[ResultType::kKeyboardShortcut];
  ASSERT_EQ(results.size(), 2u);

  // No shortcuts should be selected.
  for (const auto& result : results) {
    EXPECT_NE(result->display_type(),
              ash::SearchResultDisplayType::kAnswerCard);
  }
}

// Tests that Omnibox answers take priority over Shortcuts.
TEST_F(AnswerRankerTest, OmniboxOverShortcuts) {
  ResultsMap results_map;
  results_map[ResultType::kOmnibox] = MakeOmniboxCandidates({0.4});
  results_map[ResultType::kKeyboardShortcut] = MakeShortcutCandidates({true});

  AnswerRanker ranker;
  ranker.UpdateResultRanks(results_map, ProviderType::kKeyboardShortcut);
  ranker.UpdateResultRanks(results_map, ProviderType::kOmnibox);
  ranker.OnBurnInPeriodElapsed();

  // Shortcut candidate should not be selected.
  const auto& shortcut_results = results_map[ResultType::kKeyboardShortcut];
  ASSERT_EQ(shortcut_results.size(), 1u);
  EXPECT_NE(shortcut_results[0]->display_type(),
            ash::SearchResultDisplayType::kAnswerCard);

  // Omnibox candidate should be selected.
  const auto& omnibox_results = results_map[ResultType::kOmnibox];
  ASSERT_EQ(omnibox_results.size(), 1u);
  EXPECT_TRUE(AnswerFieldsAreSet(omnibox_results[0]));
}

// Tests that a chosen answer is not changed after burn-in.
TEST_F(AnswerRankerTest, SelectedAnswerNotChangedAfterBurnIn) {
  ResultsMap results_map;
  results_map[ResultType::kKeyboardShortcut] = MakeShortcutCandidates({true});

  AnswerRanker ranker;
  ranker.UpdateResultRanks(results_map, ProviderType::kKeyboardShortcut);
  ranker.OnBurnInPeriodElapsed();

  // The shortcut answer is selected.
  const auto& shortcut_results = results_map[ResultType::kKeyboardShortcut];
  ASSERT_EQ(shortcut_results.size(), 1u);
  EXPECT_TRUE(AnswerFieldsAreSet(shortcut_results[0]));

  // New Omnibox candidates should still be filtered out.
  results_map[ResultType::kOmnibox] = MakeOmniboxCandidates({0.5});
  ranker.UpdateResultRanks(results_map, ProviderType::kOmnibox);

  const auto& omnibox_results = results_map[ResultType::kOmnibox];
  ASSERT_EQ(omnibox_results.size(), 1u);
  EXPECT_TRUE(omnibox_results[0]->scoring().filtered());
}

}  // namespace app_list::test