chromium/chrome/browser/ui/webui/ash/settings/search/search_handler_unittest.cc

// Copyright 2020 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/ui/webui/ash/settings/search/search_handler.h"

#include "ash/constants/ash_features.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/browser/ui/webui/ash/settings/search/mojom/search.mojom.h"
#include "chrome/browser/ui/webui/ash/settings/search/search_tag_registry.h"
#include "chrome/browser/ui/webui/ash/settings/test_support/fake_hierarchy.h"
#include "chrome/browser/ui/webui/ash/settings/test_support/fake_os_settings_sections.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/local_search_service/public/cpp/local_search_service_proxy.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"

namespace ash::settings {

namespace mojom {
using ::chromeos::settings::mojom::kPrintingDetailsSubpagePath;
using ::chromeos::settings::mojom::Section;
using ::chromeos::settings::mojom::Setting;
using ::chromeos::settings::mojom::Subpage;
}  // namespace mojom

namespace {

const bool kIsRevampEnabled =
    ash::features::IsOsSettingsRevampWayfindingEnabled();

class FakeObserver : public mojom::SearchResultsObserver {
 public:
  FakeObserver() = default;
  ~FakeObserver() override = default;

  mojo::PendingRemote<mojom::SearchResultsObserver> GenerateRemote() {
    mojo::PendingRemote<mojom::SearchResultsObserver> remote;
    receiver_.Bind(remote.InitWithNewPipeAndPassReceiver());
    return remote;
  }

  size_t num_calls() const { return num_calls_; }

 private:
  // mojom::SearchResultsObserver:
  void OnSearchResultsChanged() override { ++num_calls_; }

  size_t num_calls_ = 0;
  mojo::Receiver<mojom::SearchResultsObserver> receiver_{this};
};

// Note: Copied from printing_section.cc but does not need to stay in sync with
// it.
const std::vector<SearchConcept>& GetPrintingSearchConcepts() {
  static const base::NoDestructor<std::vector<SearchConcept>> tags({
      {IDS_OS_SETTINGS_TAG_PRINTING_ADD_PRINTER,
       mojom::kPrintingDetailsSubpagePath,
       mojom::SearchResultIcon::kPrinter,
       mojom::SearchResultDefaultRank::kMedium,
       mojom::SearchResultType::kSetting,
       {.setting = mojom::Setting::kAddPrinter}},
      {IDS_OS_SETTINGS_TAG_PRINTING_SAVED_PRINTERS,
       mojom::kPrintingDetailsSubpagePath,
       mojom::SearchResultIcon::kPrinter,
       mojom::SearchResultDefaultRank::kMedium,
       mojom::SearchResultType::kSetting,
       {.setting = mojom::Setting::kSavedPrinters}},
      {IDS_OS_SETTINGS_TAG_PRINTING,
       mojom::kPrintingDetailsSubpagePath,
       mojom::SearchResultIcon::kPrinter,
       mojom::SearchResultDefaultRank::kLow,
       mojom::SearchResultType::kSubpage,
       {.subpage = mojom::Subpage::kPrintingDetails},
       {IDS_OS_SETTINGS_TAG_PRINTING_ALT1, IDS_OS_SETTINGS_TAG_PRINTING_ALT2,
        SearchConcept::kAltTagEnd}},
  });
  return *tags;
}

// Creates a result with some default values.
mojom::SearchResultPtr CreateDummyResult() {
  const mojom::Section kSection = kIsRevampEnabled
                                      ? mojom::Section::kSystemPreferences
                                      : mojom::Section::kPrinting;

  return mojom::SearchResult::New(
      /*text=*/std::u16string(),
      /*canonical_text=*/std::u16string(), /*url=*/"",
      mojom::SearchResultIcon::kPrinter, /*relevance_score=*/0.5,
      /*hierarchy_strings=*/std::vector<std::u16string>(),
      mojom::SearchResultDefaultRank::kMedium,
      /*was_generated_from_text_match=*/false,
      mojom::SearchResultType::kSection,
      mojom::SearchResultIdentifier::NewSection(kSection));
}

}  // namespace

class SearchHandlerTest : public testing::Test {
 protected:
  SearchHandlerTest()
      : search_tag_registry_(local_search_service_proxy_.get()),
        fake_hierarchy_(&fake_sections_),
        handler_(&search_tag_registry_,
                 &fake_sections_,
                 &fake_hierarchy_,
                 local_search_service_proxy_.get()) {}
  ~SearchHandlerTest() override = default;

  // testing::Test:
  void SetUp() override {
    handler_.BindInterface(handler_remote_.BindNewPipeAndPassReceiver());

    const mojom::Section kSection = kIsRevampEnabled
                                        ? mojom::Section::kSystemPreferences
                                        : mojom::Section::kPrinting;

    fake_hierarchy_.AddSubpageMetadata(
        IDS_SETTINGS_PRINTING_CUPS_PRINTERS, kSection,
        mojom::Subpage::kPrintingDetails, mojom::SearchResultIcon::kPrinter,
        mojom::SearchResultDefaultRank::kMedium,
        mojom::kPrintingDetailsSubpagePath);
    fake_hierarchy_.AddSettingMetadata(kSection, mojom::Setting::kAddPrinter);
    fake_hierarchy_.AddSettingMetadata(kSection,
                                       mojom::Setting::kSavedPrinters);

    handler_remote_->Observe(observer_.GenerateRemote());
    handler_remote_.FlushForTesting();
  }

  void AddSearchTags(const std::vector<SearchConcept>& search_tags) {
    SearchTagRegistry::ScopedTagUpdater updater =
        search_tag_registry_.StartUpdate();
    updater.AddSearchTags(search_tags);
  }

  void RemoveSearchTags(const std::vector<SearchConcept>& search_tags) {
    SearchTagRegistry::ScopedTagUpdater updater =
        search_tag_registry_.StartUpdate();
    updater.RemoveSearchTags(search_tags);
  }

  std::vector<mojom::SearchResultPtr> Search(
      const std::u16string& query,
      uint32_t max_num_results,
      mojom::ParentResultBehavior parent_result_behavior) {
    base::test::TestFuture<std::vector<mojom::SearchResultPtr>> future;
    handler_remote_->Search(query, max_num_results, parent_result_behavior,
                            future.GetCallback());
    return future.Take();
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<local_search_service::LocalSearchServiceProxy>
      local_search_service_proxy_ =
          std::make_unique<local_search_service::LocalSearchServiceProxy>(
              /*for_testing=*/true);
  SearchTagRegistry search_tag_registry_;
  FakeOsSettingsSections fake_sections_;
  FakeHierarchy fake_hierarchy_;
  SearchHandler handler_;
  mojo::Remote<mojom::SearchHandler> handler_remote_;
  FakeObserver observer_;
};

TEST_F(SearchHandlerTest, AddAndRemove) {
  // Add printing search tags to registry and search for "Print".
  AddSearchTags(GetPrintingSearchConcepts());
  handler_remote_.FlushForTesting();
  task_environment_.RunUntilIdle();
  EXPECT_EQ(1u, observer_.num_calls());

  std::vector<mojom::SearchResultPtr> search_results;

  // 3 results should be available for a "Print" query.
  search_results =
      Search(u"Print",
             /*max_num_results=*/3u,
             mojom::ParentResultBehavior::kDoNotIncludeParentResults);
  EXPECT_EQ(search_results.size(), 3u);

  // Limit results to 1 max and ensure that only 1 result is returned.
  search_results =
      Search(u"Print",
             /*max_num_results=*/1u,
             mojom::ParentResultBehavior::kDoNotIncludeParentResults);
  EXPECT_EQ(search_results.size(), 1u);

  // Search for a query which should return no results.
  search_results =
      Search(u"QueryWithNoResults",
             /*max_num_results=*/3u,
             mojom::ParentResultBehavior::kDoNotIncludeParentResults);
  EXPECT_TRUE(search_results.empty());

  // Remove printing search tags to registry and verify that no results are
  // returned for "Printing".
  RemoveSearchTags(GetPrintingSearchConcepts());
  search_results =
      Search(u"Print",
             /*max_num_results=*/3u,
             mojom::ParentResultBehavior::kDoNotIncludeParentResults);
  EXPECT_TRUE(search_results.empty());
  EXPECT_EQ(2u, observer_.num_calls());
}

TEST_F(SearchHandlerTest, UrlModification) {
  // Add printing search tags to registry and search for "Saved".
  AddSearchTags(GetPrintingSearchConcepts());
  std::vector<mojom::SearchResultPtr> search_results =
      Search(u"Saved",
             /*max_num_results=*/3u,
             mojom::ParentResultBehavior::kDoNotIncludeParentResults);

  // Only the "saved printers" item should be returned.
  EXPECT_EQ(search_results.size(), 1u);

  // The URL should have bee modified according to the FakeOsSettingSection
  // scheme.
  const std::string kPrefix = kIsRevampEnabled
                                  ? std::string("kSystemPreferences::")
                                  : std::string("kPrinting::");

  EXPECT_EQ(kPrefix + mojom::kPrintingDetailsSubpagePath,
            search_results[0]->url_path_with_parameters);
}

TEST_F(SearchHandlerTest, AltTagMatch) {
  // Add printing search tags to registry.
  AddSearchTags(GetPrintingSearchConcepts());

  // Search for "CUPS". The IDS_OS_SETTINGS_TAG_PRINTING result has an alternate
  // tag "CUPS" (referring to the Unix printing protocol), so we should receive
  // one match.
  std::vector<mojom::SearchResultPtr> search_results =
      Search(u"CUPS",
             /*max_num_results=*/3u,
             mojom::ParentResultBehavior::kDoNotIncludeParentResults);
  EXPECT_EQ(search_results.size(), 1u);

  // Verify the result text and canonical restult text.
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_OS_SETTINGS_TAG_PRINTING_ALT2),
            search_results[0]->text);
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_OS_SETTINGS_TAG_PRINTING),
            search_results[0]->canonical_text);
}

TEST_F(SearchHandlerTest, AllowParentResult) {
  // Add printing search tags to registry.
  AddSearchTags(GetPrintingSearchConcepts());

  // Search for "Saved", which should only apply to the "saved printers" item.
  // Pass the kAllowParentResults flag, which should also cause its parent
  // subpage item to be returned.
  std::vector<mojom::SearchResultPtr> search_results = Search(
      u"Saved",
      /*max_num_results=*/3u, mojom::ParentResultBehavior::kAllowParentResults);
  EXPECT_EQ(search_results.size(), 2u);
  EXPECT_FALSE(search_results[1]->was_generated_from_text_match);
}

TEST_F(SearchHandlerTest, DefaultRank) {
  // Add printing search tags to registry.
  AddSearchTags(GetPrintingSearchConcepts());

  // Search for "Print". Only the IDS_OS_SETTINGS_TAG_PRINTING result
  // contains the word "Printing", but the other results have the similar word
  // "Printer". Thus, "Printing" has a higher relevance score.
  std::vector<mojom::SearchResultPtr> search_results = Search(
      u"Print",
      /*max_num_results=*/3u, mojom::ParentResultBehavior::kAllowParentResults);
  EXPECT_EQ(search_results.size(), 3u);

  // Since the IDS_OS_SETTINGS_TAG_PRINTING result has a default rank of kLow,
  // it should be the *last* result returned even though it has a higher
  // relevance score.
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_OS_SETTINGS_TAG_PRINTING),
            search_results[2]->text);
}

// Regression test for https://crbug.com/1090184.
TEST_F(SearchHandlerTest, CompareSearchResults) {
  // Create two equal dummy results.
  mojom::SearchResultPtr a = CreateDummyResult();
  mojom::SearchResultPtr b = CreateDummyResult();

  // CompareSearchResults() returns whether |a| < |b|; since they are equal, it
  // should return false regardless of the order of parameters.
  EXPECT_FALSE(SearchHandler::CompareSearchResults(a, b));
  EXPECT_FALSE(SearchHandler::CompareSearchResults(b, a));

  // Differ only on default rank.
  a = CreateDummyResult();
  a->default_rank = mojom::SearchResultDefaultRank::kLow;
  b = CreateDummyResult();
  b->default_rank = mojom::SearchResultDefaultRank::kHigh;

  // Comparison value should differ.
  EXPECT_NE(SearchHandler::CompareSearchResults(b, a),
            SearchHandler::CompareSearchResults(a, b));

  // Differ only on relevance score.
  a = CreateDummyResult();
  a->relevance_score = 0;
  b = CreateDummyResult();
  b->relevance_score = 1;

  // Comparison value should differ.
  EXPECT_NE(SearchHandler::CompareSearchResults(b, a),
            SearchHandler::CompareSearchResults(a, b));

  // Differ only on type.
  a = CreateDummyResult();
  a->type = mojom::SearchResultType::kSection;
  b = CreateDummyResult();
  b->type = mojom::SearchResultType::kSubpage;

  // Comparison value should differ.
  EXPECT_NE(SearchHandler::CompareSearchResults(b, a),
            SearchHandler::CompareSearchResults(a, b));
}

}  // namespace ash::settings