chromium/chrome/browser/ash/app_list/search/omnibox/omnibox_provider_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/omnibox/omnibox_provider.h"

#include <cstddef>
#include <string>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "chrome/browser/ash/app_list/search/search_controller.h"
#include "chrome/browser/ash/app_list/search/test/test_search_controller.h"
#include "chrome/browser/ash/app_list/test/test_app_list_controller_delegate.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/fake_autocomplete_provider_client.h"
#include "components/omnibox/browser/omnibox_feature_configs.h"
#include "components/omnibox/browser/suggestion_answer.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/variations/scoped_variations_ids_provider.h"
#include "components/variations/variations_ids_provider.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/omnibox_proto/answer_type.pb.h"

namespace app_list::test {

// Note that there is necessarily a lot of overlap with unittest in the lacros
// omnibox provider unittest, since this is testing the same behavior.
namespace {

// Helper functions to populate search results.
// Currently only the ones that may affect test results are filled.
AutocompleteMatch NewOmniboxResult(const std::string& url) {
  AutocompleteMatch result;

  result.relevance = 1.0;
  result.destination_url = GURL(url);
  result.stripped_destination_url = GURL(url);
  result.contents = u"contents";
  result.description = u"description";
  result.type = AutocompleteMatchType::BOOKMARK_TITLE;

  return result;
}

AutocompleteMatch NewAnswerResult(const std::string& url,
                                  omnibox::AnswerType answer_type) {
  omnibox_feature_configs::ScopedConfigForTesting<
      omnibox_feature_configs::SuggestionAnswerMigration>
      scoped_config;
  AutocompleteMatch result;

  result.relevance = 1.0;
  result.destination_url = GURL(url);
  result.stripped_destination_url = GURL(url);
  result.contents = u"contents";
  result.description = u"description";
  if (scoped_config.Get().enabled) {
    omnibox::RichAnswerTemplate answer_template;
    answer_template.add_answers();
    result.answer_template = answer_template;
  } else {
    SuggestionAnswer answer;
    result.answer = answer;
  }
  result.answer_type = answer_type;

  return result;
}

AutocompleteMatch NewOpenTabResult(const std::string& url) {
  AutocompleteMatch result;

  result.relevance = 1.0;
  result.destination_url = GURL(url);
  result.stripped_destination_url = GURL(url);
  result.contents = u"contents";
  result.description = u"description";
  result.type = AutocompleteMatchType::OPEN_TAB;

  return result;
}

// A mock class for the AutoCompleteController.
class MockAutoCompleteController : public AutocompleteController {
 public:
  MockAutoCompleteController()
      : AutocompleteController(
            std::make_unique<FakeAutocompleteProviderClient>(),
            0) {}
  MockAutoCompleteController(const MockAutoCompleteController&) = delete;
  MockAutoCompleteController& operator=(const MockAutoCompleteController&) =
      delete;
  ~MockAutoCompleteController() override = default;

  // Do nothing when it is called by OmniboxProvider.
  void Start(const AutocompleteInput& input) override {}
};

}  // namespace

class OmniboxProviderTest : public testing::Test {
 public:
  OmniboxProviderTest() = default;
  OmniboxProviderTest(const OmniboxProviderTest&) = delete;
  OmniboxProviderTest& operator=(const OmniboxProviderTest&) = delete;
  ~OmniboxProviderTest() override = default;

  void SetUp() override {
    // Create the profile manager and an active profile.
    profile_manager_ = std::make_unique<TestingProfileManager>(
        TestingBrowserProcess::GetGlobal());
    ASSERT_TRUE(profile_manager_->SetUp());
    // The profile needs a template URL service for history Omnibox results.
    profile_ = profile_manager_->CreateTestingProfile(
        chrome::kInitialProfile,
        {TestingProfile::TestingFactory{
            TemplateURLServiceFactory::GetInstance(),
            base::BindRepeating(
                &TemplateURLServiceFactory::BuildInstanceFor)}});

    // Create client of our provider.
    search_controller_ = std::make_unique<TestSearchController>();

    // Create the object to actually test.
    list_controller_ =
        std::make_unique<::test::TestAppListControllerDelegate>();
    auto provider = std::make_unique<OmniboxProvider>(
        profile_, list_controller_.get(), /*provider_types=*/0);
    provider_ = provider.get();
    search_controller_->AddProvider(std::move(provider));

    std::unique_ptr<AutocompleteController> controller =
        std::make_unique<MockAutoCompleteController>();
    provider_->set_controller_for_test(std::move(controller));

    base::RunLoop().RunUntilIdle();
  }

  void TearDown() override {
    provider_ = nullptr;
    search_controller_.reset();
    list_controller_.reset();
    profile_ = nullptr;
    profile_manager_->DeleteTestingProfile(chrome::kInitialProfile);
  }

  void ProduceResults(const AutocompleteResult& results) {
    provider_->PopulateFromACResult(std::move(results));
    base::RunLoop().RunUntilIdle();
  }

  // Starts a search and waits for the query to be sent
  void StartSearch(const std::u16string& query) {
    search_controller_->StartSearch(query);
    base::RunLoop().RunUntilIdle();
  }

  void DisableWebSearch() {
    ScopedDictPrefUpdate pref_update(
        profile_->GetPrefs(), ash::prefs::kLauncherSearchCategoryControlStatus);

    pref_update->Set(ash::GetAppListControlCategoryName(ControlCategory::kWeb),
                     false);
  }

 protected:
  std::unique_ptr<TestSearchController> search_controller_;

 private:
  content::BrowserTaskEnvironment task_environment_;
  variations::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
      variations::VariationsIdsProvider::Mode::kUseSignedInState};
  std::unique_ptr<AppListControllerDelegate> list_controller_;

  std::unique_ptr<TestingProfileManager> profile_manager_;
  raw_ptr<TestingProfile> profile_;

  raw_ptr<OmniboxProvider> provider_;
};

// Test that results each instantiate a Chrome search result.
TEST_F(OmniboxProviderTest, Basic) {
  StartSearch(u"query");

  std::vector<AutocompleteMatch> to_produce;
  AutocompleteResult result;

  to_produce.emplace_back(NewOmniboxResult("https://example.com/result"));
  to_produce.emplace_back(NewAnswerResult(
      "https://example.com/answer", omnibox::AnswerType::ANSWER_TYPE_WEATHER));
  to_produce.emplace_back(NewOpenTabResult("https://example.com/open_tab"));
  result.AppendMatches(to_produce);
  ProduceResults(std::move(result));

  // Results always appear after answer and open tab entries.
  ASSERT_EQ(3u, search_controller_->last_results().size());
  EXPECT_EQ("omnibox_answer://https://example.com/answer",
            search_controller_->last_results()[0]->id());
  EXPECT_EQ("opentab://https://example.com/open_tab",
            search_controller_->last_results()[1]->id());
  EXPECT_EQ("https://example.com/result",
            search_controller_->last_results()[2]->id());
}

// Test that newly-produced results supersede previous results.
TEST_F(OmniboxProviderTest, NewResults) {
  StartSearch(u"query");

  // Produce one result.
  std::vector<AutocompleteMatch> to_produce;
  AutocompleteResult result;

  to_produce.emplace_back(NewOpenTabResult("https://example.com/open_tab_1"));
  result.AppendMatches(to_produce);
  ProduceResults(std::move(result));

  // Then produce another.
  StartSearch(u"query");

  to_produce.clear();
  AutocompleteResult new_result;
  to_produce.emplace_back(NewOpenTabResult("https://example.com/open_tab_2"));
  new_result.AppendMatches(to_produce);
  ProduceResults(std::move(new_result));

  // Only newest result should be stored.
  ASSERT_EQ(1u, search_controller_->last_results().size());
  EXPECT_EQ("opentab://https://example.com/open_tab_2",
            search_controller_->last_results()[0]->id());
}

// Test that invalid URLs aren't accepted.
TEST_F(OmniboxProviderTest, BadUrls) {
  StartSearch(u"query");

  // All results have bad URLs.
  std::vector<AutocompleteMatch> to_produce;
  AutocompleteResult result;

  to_produce.emplace_back(NewOmniboxResult(""));
  to_produce.emplace_back(
      NewAnswerResult("badscheme", omnibox::AnswerType::ANSWER_TYPE_WEATHER));
  to_produce.emplace_back(NewOpenTabResult("http://?k=v"));
  result.AppendMatches(to_produce);
  ProduceResults(std::move(result));

  // None of the results should be accepted.
  EXPECT_TRUE(search_controller_->last_results().empty());
}

// Test that results with the same URL are deduplicated in the correct order.
TEST_F(OmniboxProviderTest, Deduplicate) {
  StartSearch(u"query");

  // A result that has the same URL as another result, but is a history (i.e.
  // higher-priority) type.
  auto history_result = NewOmniboxResult("https://example.com/result_1");
  history_result.contents = u"history";
  history_result.description = u"history description";
  history_result.type = AutocompleteMatchType::SEARCH_HISTORY;

  std::vector<AutocompleteMatch> to_produce;
  AutocompleteResult result;

  to_produce.emplace_back(NewOmniboxResult("https://example.com/result_2"));
  to_produce.emplace_back(NewOmniboxResult("https://example.com/result_1"));
  to_produce.emplace_back(std::move(history_result));

  result.AppendMatches(to_produce);
  ProduceResults(std::move(result));

  // Only the higher-priority (i.e. history) result for URL 1 should be kept.
  ASSERT_EQ(2u, search_controller_->last_results().size());
  EXPECT_EQ("https://example.com/result_1",
            search_controller_->last_results()[0]->id());
  EXPECT_EQ(u"history", search_controller_->last_results()[0]->title());
  EXPECT_EQ("https://example.com/result_2",
            search_controller_->last_results()[1]->id());
}

// Test that results aren't created for URLs for which there are other
// specialist producers.
TEST_F(OmniboxProviderTest, UnhandledUrls) {
  StartSearch(u"query");

  // Drive URLs aren't handled (_unless_ they are open tabs pointing to the
  // Drive website), and file URLs aren't handled.
  std::vector<AutocompleteMatch> to_produce;
  AutocompleteResult result;

  to_produce.emplace_back(NewOmniboxResult("https://drive.google.com/doc1"));
  to_produce.emplace_back(
      NewAnswerResult("https://docs.google.com/doc2",
                      omnibox::AnswerType::ANSWER_TYPE_FINANCE));
  to_produce.emplace_back(NewOpenTabResult("https://drive.google.com/doc1"));
  to_produce.emplace_back(NewOpenTabResult("https://docs.google.com/doc2"));
  to_produce.emplace_back(NewOpenTabResult("file:///docs/doc3"));
  result.AppendMatches(to_produce);
  ProduceResults(std::move(result));

  ASSERT_EQ(2u, search_controller_->last_results().size());
  EXPECT_EQ("opentab://https://drive.google.com/doc1",
            search_controller_->last_results()[0]->id());
  EXPECT_EQ("opentab://https://docs.google.com/doc2",
            search_controller_->last_results()[1]->id());
}

// Test that all non-answer results are filtered if web search is disabled in
// search control.
TEST_F(OmniboxProviderTest, WebSearchControl) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitWithFeatures(
      {ash::features::kLauncherSearchControl,
       ash::features::kFeatureManagementLocalImageSearch},
      {});
  DisableWebSearch();

  StartSearch(u"query");

  std::vector<AutocompleteMatch> to_produce;
  AutocompleteResult result;

  to_produce.emplace_back(NewOmniboxResult("https://example.com/result"));
  to_produce.emplace_back(NewAnswerResult(
      "https://example.com/answer", omnibox::AnswerType::ANSWER_TYPE_WEATHER));
  to_produce.emplace_back(NewOpenTabResult("https://example.com/open_tab"));
  result.AppendMatches(to_produce);
  ProduceResults(std::move(result));

  // Only answer result is returned.
  ASSERT_EQ(1u, search_controller_->last_results().size());
  EXPECT_EQ("omnibox_answer://https://example.com/answer",
            search_controller_->last_results()[0]->id());
}

}  // namespace app_list::test