chromium/chrome/browser/ash/crosapi/search_controller_factory_ash_unittest.cc

// Copyright 2024 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/crosapi/search_controller_factory_ash.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/auto_reset.h"
#include "base/check.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/crosapi/search_controller_ash.h"
#include "chrome/browser/ui/ash/picker/picker_lacros_omnibox_search_provider.h"
#include "chromeos/crosapi/mojom/launcher_search.mojom-forward.h"
#include "chromeos/crosapi/mojom/launcher_search.mojom-shared.h"
#include "chromeos/crosapi/mojom/launcher_search.mojom.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace crosapi {
namespace {

using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::Optional;
using ::testing::Pointee;

// TODO: b/326147929 - Share this code with `crosapi::SearchControllerAsh` unit
// tests.
class TestMojomSearchController : public mojom::SearchController {
 public:
  explicit TestMojomSearchController(bool bookmarks,
                                     bool history,
                                     bool open_tabs)
      : bookmarks_(bookmarks), history_(history), open_tabs_(open_tabs) {}
  ~TestMojomSearchController() override = default;

  void Bind(mojo::PendingReceiver<mojom::SearchController> receiver) {
    receiver_.Bind(std::move(receiver));
  }

  void RunUntilSearch() {
    base::test::TestFuture<void> waiter;
    base::AutoReset<base::RepeatingClosure> quit_loop(
        &search_callback_, waiter.GetRepeatingCallback());
    EXPECT_TRUE(waiter.Wait());
  }

  void ProduceResults(
      mojom::SearchStatus status,
      std::optional<std::vector<mojom::SearchResultPtr>> results) {
    publisher_->OnSearchResultsReceived(status, std::move(results));
  }

  const std::u16string& last_query() { return last_query_; }
  bool bookmarks() const { return bookmarks_; }
  bool history() const { return history_; }
  bool open_tabs() const { return open_tabs_; }

 private:
  // mojom::SearchController overrides:
  void Search(const std::u16string& query, SearchCallback callback) override {
    last_query_ = query;

    publisher_.reset();
    std::move(callback).Run(publisher_.BindNewEndpointAndPassReceiver());

    search_callback_.Run();
  }

  base::RepeatingClosure search_callback_ = base::DoNothing();

  mojo::Receiver<mojom::SearchController> receiver_{this};
  mojo::AssociatedRemote<mojom::SearchResultsPublisher> publisher_;
  bool bookmarks_;
  bool history_;
  bool open_tabs_;
  std::u16string last_query_;
};

class TestMojomSearchControllerFactory : public mojom::SearchControllerFactory {
 public:
  TestMojomSearchControllerFactory() = default;
  ~TestMojomSearchControllerFactory() override = default;

  mojo::PendingRemote<mojom::SearchControllerFactory> BindToRemote() {
    return receiver_.BindNewPipeAndPassRemote();
  }

  void RunUntilCreateSearchController() {
    base::test::TestFuture<void> waiter;
    base::AutoReset<base::RepeatingClosure> quit_loop(
        &create_search_controller_callback_, waiter.GetRepeatingCallback());
    EXPECT_TRUE(waiter.Wait());
  }

  std::unique_ptr<TestMojomSearchController> TakeLastTestController() {
    return std::move(last_test_controller_);
  }

  // mojom::SearchControllerFactory overrides:
  void CreateSearchControllerPicker(
      mojo::PendingReceiver<mojom::SearchController> controller,
      bool bookmark,
      bool history,
      bool open_tab) override {
    CHECK(!last_test_controller_);
    last_test_controller_ = std::make_unique<TestMojomSearchController>(
        bookmark, history, open_tab);
    last_test_controller_->Bind(std::move(controller));

    create_search_controller_callback_.Run();
  }

 private:
  base::RepeatingClosure create_search_controller_callback_ = base::DoNothing();

  std::unique_ptr<TestMojomSearchController> last_test_controller_;
  mojo::Receiver<mojom::SearchControllerFactory> receiver_{this};
};

class TestFactoryObserver : public SearchControllerFactoryAsh::Observer {
 public:
  explicit TestFactoryObserver(SearchControllerFactoryAsh* factory) {
    obs_.Observe(factory);
  }
  ~TestFactoryObserver() override = default;

  void OnSearchControllerFactoryBound(
      SearchControllerFactoryAsh* factory) override {
    on_bound_.SetValue(factory);
  }

  base::test::TestFuture<SearchControllerFactoryAsh*>& on_bound() {
    return on_bound_;
  }

 private:
  base::test::TestFuture<SearchControllerFactoryAsh*> on_bound_;

  base::ScopedObservation<SearchControllerFactoryAsh,
                          SearchControllerFactoryAsh::Observer>
      obs_{this};
};

using SearchControllerFactoryAshTest = ::testing::Test;

TEST_F(SearchControllerFactoryAshTest,
       CreateSearchControllerPickerReturnsNullptrIfNotBound) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  ASSERT_FALSE(factory.IsBound());

  std::unique_ptr<SearchControllerAsh> controller =
      factory.CreateSearchControllerPicker(false, false, false);

  EXPECT_FALSE(controller);
}

TEST_F(SearchControllerFactoryAshTest,
       CreateSearchControllerPickerReturnsNonNullIfBoundAndConnected) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());
  ASSERT_TRUE(factory.IsBound());

  std::unique_ptr<SearchControllerAsh> controller =
      factory.CreateSearchControllerPicker(false, false, false);

  EXPECT_TRUE(controller);
}

TEST_F(SearchControllerFactoryAshTest,
       CreateSearchControllerPickerReturnsNonNullIfBoundAndDisconnected) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  {
    TestMojomSearchControllerFactory mojom_factory;
    factory.BindRemote(mojom_factory.BindToRemote());
    ASSERT_TRUE(factory.IsBound());
  }
  // Ensure that the factory receives the disconnection.
  // TODO: b/326147929 - Use a `QuitClosure` for this.
  base::RunLoop().RunUntilIdle();

  std::unique_ptr<SearchControllerAsh> controller =
      factory.CreateSearchControllerPicker(false, false, false);

  EXPECT_TRUE(controller);
}

TEST_F(SearchControllerFactoryAshTest,
       CreateSearchControllerPickerCalledRemote) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());

  std::unique_ptr<SearchControllerAsh> controller =
      factory.CreateSearchControllerPicker(false, false, false);
  mojom_factory.RunUntilCreateSearchController();

  std::unique_ptr<TestMojomSearchController> mojom_controller =
      mojom_factory.TakeLastTestController();
  EXPECT_TRUE(mojom_controller);
}

TEST_F(SearchControllerFactoryAshTest, CreatedPickerControllerReceivesQueries) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());
  std::unique_ptr<SearchControllerAsh> controller =
      factory.CreateSearchControllerPicker(false, false, false);
  mojom_factory.RunUntilCreateSearchController();
  std::unique_ptr<TestMojomSearchController> mojom_controller =
      mojom_factory.TakeLastTestController();

  controller->Search(u"cat", base::DoNothing());
  mojom_controller->RunUntilSearch();

  EXPECT_EQ(mojom_controller->last_query(), u"cat");
}

TEST_F(SearchControllerFactoryAshTest, CreatedPickerControllerReceivesResults) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());
  std::unique_ptr<SearchControllerAsh> controller =
      factory.CreateSearchControllerPicker(false, false, false);
  mojom_factory.RunUntilCreateSearchController();
  std::unique_ptr<TestMojomSearchController> mojom_controller =
      mojom_factory.TakeLastTestController();
  base::test::TestFuture<std::vector<mojom::SearchResultPtr>> search_future;

  controller->Search(u"cat", search_future.GetRepeatingCallback());
  mojom_controller->RunUntilSearch();
  std::vector<mojom::SearchResultPtr> results;
  {
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url = GURL("https://www.google.com/search?q=cat");
    results.push_back(std::move(result));
  }
  {
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url =
        GURL("https://www.google.com/search?q=catalan+numbers");
    results.push_back(std::move(result));
  }
  mojom_controller->ProduceResults(mojom::SearchStatus::kDone,
                                   std::move(results));

  std::vector<mojom::SearchResultPtr> returned_results = search_future.Take();
  EXPECT_THAT(
      returned_results,
      ElementsAre(
          Pointee(Field("destination_url",
                        &mojom::SearchResult::destination_url,
                        Optional(GURL("https://www.google.com/search?q=cat")))),
          Pointee(Field(
              "destination_url", &mojom::SearchResult::destination_url,
              Optional(
                  GURL("https://www.google.com/search?q=catalan+numbers"))))));
}

TEST_F(SearchControllerFactoryAshTest,
       CreatedPickerControllerHasCorrectProviderTypes) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());

  std::unique_ptr<SearchControllerAsh> unused_controller =
      factory.CreateSearchControllerPicker(/*bookmarks=*/true,
                                           /*history=*/false,
                                           /*open_tabs=*/true);
  mojom_factory.RunUntilCreateSearchController();

  std::unique_ptr<TestMojomSearchController> mojom_controller =
      mojom_factory.TakeLastTestController();
  ASSERT_TRUE(mojom_controller);
  EXPECT_TRUE(mojom_controller->bookmarks());
  EXPECT_FALSE(mojom_controller->history());
  EXPECT_TRUE(mojom_controller->open_tabs());
}

TEST_F(SearchControllerFactoryAshTest, OnBoundNotCalledIfNotBound) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;

  TestFactoryObserver observer(&factory);

  EXPECT_FALSE(observer.on_bound().IsReady());
}

TEST_F(SearchControllerFactoryAshTest, OnBoundCalledIfAlreadyBound) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());

  TestFactoryObserver observer(&factory);

  EXPECT_TRUE(observer.on_bound().IsReady());
}

TEST_F(SearchControllerFactoryAshTest, OnBoundCalledWhenBoundLater) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestFactoryObserver observer(&factory);
  ASSERT_FALSE(observer.on_bound().IsReady());

  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());

  EXPECT_TRUE(observer.on_bound().IsReady());
}

TEST_F(SearchControllerFactoryAshTest, OnBoundCalledWhenRebound) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestFactoryObserver observer(&factory);
  {
    // Connect a remote factory...
    TestMojomSearchControllerFactory mojom_factory;
    factory.BindRemote(mojom_factory.BindToRemote());
    ASSERT_TRUE(observer.on_bound().IsReady());
    observer.on_bound().Clear();
    // ...then disconnect it.
  }
  // Ensure that the factory receives the disconnection so it can be rebound.
  // TODO: b/326147929 - Use a `QuitClosure` for this.
  base::RunLoop().RunUntilIdle();

  // Rebind another remote factory.
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());

  EXPECT_TRUE(observer.on_bound().IsReady());
}

TEST_F(SearchControllerFactoryAshTest, OnBoundCalledWithFactoryPtr) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchControllerFactoryAsh factory;
  TestMojomSearchControllerFactory mojom_factory;
  factory.BindRemote(mojom_factory.BindToRemote());

  TestFactoryObserver observer(&factory);

  SearchControllerFactoryAsh* ptr = observer.on_bound().Take();
  EXPECT_EQ(ptr, &factory);
}

}  // namespace
}  // namespace crosapi