| // 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_ash.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "chromeos/crosapi/mojom/launcher_search.mojom-forward.h" |
| #include "chromeos/crosapi/mojom/launcher_search.mojom.h" |
| #include "mojo/public/cpp/bindings/associated_remote.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.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::InSequence; |
| using ::testing::IsEmpty; |
| using ::testing::Optional; |
| using ::testing::Pointee; |
| |
| // TODO: b/326147929 - Share this code with `crosapi::SearchControllerAsh` unit |
| // tests (and possibly `app_list::OmniboxLacrosProvider` unit tests too). |
| class TestMojomSearchController : public mojom::SearchController { |
| public: |
| mojo::PendingRemote<mojom::SearchController> BindToRemote() { |
| return receiver_.BindNewPipeAndPassRemote(); |
| } |
| |
| void RunUntilSearch() { |
| base::test::TestFuture<void> future; |
| base::AutoReset<base::RepeatingClosure> quit_loop( |
| &search_callback_, future.GetRepeatingCallback()); |
| EXPECT_TRUE(future.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_; } |
| |
| private: |
| 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_; |
| std::u16string last_query_; |
| }; |
| |
| using SearchResultsTestFuture = |
| ::base::test::TestFuture<std::vector<mojom::SearchResultPtr>>; |
| using DisconnectTestFuture = |
| ::base::test::TestFuture<::base::WeakPtr<::crosapi::SearchControllerAsh>>; |
| |
| using SearchControllerAshTest = ::testing::Test; |
| |
| TEST_F(SearchControllerAshTest, CallbackNotCalledIfNotConnected) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| |
| std::unique_ptr<SearchControllerAsh> controller; |
| { |
| TestMojomSearchController mojom_controller; |
| controller = |
| std::make_unique<SearchControllerAsh>(mojom_controller.BindToRemote()); |
| } |
| { |
| DisconnectTestFuture future1; |
| controller->AddDisconnectHandler(future1.GetCallback()); |
| EXPECT_TRUE(future1.Wait()); |
| } |
| controller->Search(u"cat", future.GetRepeatingCallback()); |
| |
| EXPECT_FALSE(future.IsReady()); |
| } |
| |
| TEST_F(SearchControllerAshTest, CallbackNotCalledIfBackendUnavailable) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| TestMojomSearchController mojom_controller; |
| |
| SearchControllerAsh controller(mojom_controller.BindToRemote()); |
| controller.Search(u"cat", future.GetRepeatingCallback()); |
| mojom_controller.RunUntilSearch(); |
| mojom_controller.ProduceResults(mojom::SearchStatus::kBackendUnavailable, |
| std::nullopt); |
| // Run until `controller.OnSearchResultsReceived()` is called. |
| // TODO: b/326147929 - Use a `QuitClosure` for this. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(future.IsReady()); |
| } |
| |
| TEST_F(SearchControllerAshTest, CallbackNotCalledIfCancelled) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| TestMojomSearchController mojom_controller; |
| |
| SearchControllerAsh controller(mojom_controller.BindToRemote()); |
| controller.Search(u"cat", future.GetRepeatingCallback()); |
| mojom_controller.RunUntilSearch(); |
| mojom_controller.ProduceResults(mojom::SearchStatus::kBackendUnavailable, |
| std::nullopt); |
| // Run until `controller.OnSearchResultsReceived()` is called. |
| // TODO: b/326147929 - Use a `QuitClosure` for this. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(future.IsReady()); |
| } |
| |
| TEST_F(SearchControllerAshTest, CallbackCalledWithEmptyResults) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| TestMojomSearchController mojom_controller; |
| |
| SearchControllerAsh controller(mojom_controller.BindToRemote()); |
| controller.Search(u"cat", future.GetRepeatingCallback()); |
| mojom_controller.RunUntilSearch(); |
| mojom_controller.ProduceResults(mojom::SearchStatus::kDone, |
| std::vector<mojom::SearchResultPtr>()); |
| |
| std::vector<mojom::SearchResultPtr> returned_results = future.Take(); |
| EXPECT_THAT(returned_results, IsEmpty()); |
| } |
| |
| TEST_F(SearchControllerAshTest, |
| CallbackCalledWithMultipleResultsSimultaneously) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| TestMojomSearchController mojom_controller; |
| |
| SearchControllerAsh controller(mojom_controller.BindToRemote()); |
| controller.Search(u"cat", 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 = 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(SearchControllerAshTest, CallbackCalledWithMultipleResultsSeparately) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| TestMojomSearchController mojom_controller; |
| |
| SearchControllerAsh controller(mojom_controller.BindToRemote()); |
| controller.Search(u"cat", 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_controller.ProduceResults(mojom::SearchStatus::kDone, |
| std::move(results)); |
| |
| std::vector<mojom::SearchResultPtr> returned_results = future.Take(); |
| EXPECT_THAT(returned_results, |
| ElementsAre(Pointee(Field( |
| "destination_url", &mojom::SearchResult::destination_url, |
| Optional(GURL("https://www.google.com/search?q=cat")))))); |
| } |
| { |
| std::vector<mojom::SearchResultPtr> results; |
| 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 = future.Take(); |
| EXPECT_THAT(returned_results, |
| ElementsAre(Pointee(Field( |
| "destination_url", &mojom::SearchResult::destination_url, |
| Optional(GURL( |
| "https://www.google.com/search?q=catalan+numbers")))))); |
| } |
| } |
| |
| TEST_F(SearchControllerAshTest, CallbackIsNotCalledWithInProgressResults) { |
| base::test::SingleThreadTaskEnvironment environment; |
| SearchResultsTestFuture future; |
| TestMojomSearchController mojom_controller; |
| |
| SearchControllerAsh controller(mojom_controller.BindToRemote()); |
| controller.Search(u"cat", 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_controller.ProduceResults(mojom::SearchStatus::kInProgress, |
| std::move(results)); |
| // Run until `controller.OnSearchResultsReceived()` is run. |
| // TODO: b/326147929 - Use a `QuitClosure` for this. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(future.IsReady()); |
| } |
| { |
| std::vector<mojom::SearchResultPtr> results; |
| 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 = future.Take(); |
| EXPECT_THAT(returned_results, |
| ElementsAre(Pointee(Field( |
| "destination_url", &mojom::SearchResult::destination_url, |
| Optional(GURL( |
| "https://www.google.com/search?q=catalan+numbers")))))); |
| } |
| } |
| |
| TEST_F(SearchControllerAshTest, |
| DisconnectHandlerIsCalledOnDisconnectWithValidPointer) { |
| base::test::SingleThreadTaskEnvironment environment; |
| DisconnectTestFuture future; |
| auto mojom_controller = std::make_unique<TestMojomSearchController>(); |
| SearchControllerAsh controller(mojom_controller->BindToRemote()); |
| |
| controller.AddDisconnectHandler(future.GetCallback()); |
| mojom_controller.reset(); |
| ASSERT_TRUE(future.Wait()) << "Disconnect handler was never called"; |
| ASSERT_FALSE(controller.IsConnected()); |
| |
| EXPECT_TRUE(future.IsReady()); |
| base::WeakPtr<SearchControllerAsh> weak_controller = future.Take(); |
| EXPECT_TRUE(weak_controller); |
| EXPECT_EQ(weak_controller.get(), &controller); |
| } |
| |
| TEST_F(SearchControllerAshTest, |
| DisconnectHandlersAreCalledOnDisconnectWithValidPointers) { |
| base::test::SingleThreadTaskEnvironment environment; |
| DisconnectTestFuture future_1; |
| DisconnectTestFuture future_2; |
| auto mojom_controller = std::make_unique<TestMojomSearchController>(); |
| SearchControllerAsh controller(mojom_controller->BindToRemote()); |
| |
| controller.AddDisconnectHandler(future_1.GetCallback()); |
| controller.AddDisconnectHandler(future_2.GetCallback()); |
| mojom_controller.reset(); |
| ASSERT_TRUE(future_2.Wait()) << "Disconnect handler was never called"; |
| ASSERT_FALSE(controller.IsConnected()); |
| |
| base::WeakPtr<SearchControllerAsh> weak_controller_1 = future_1.Take(); |
| EXPECT_TRUE(weak_controller_1); |
| EXPECT_EQ(weak_controller_1.get(), &controller); |
| base::WeakPtr<SearchControllerAsh> weak_controller_2 = future_2.Take(); |
| EXPECT_TRUE(weak_controller_2); |
| EXPECT_EQ(weak_controller_2.get(), &controller); |
| } |
| |
| TEST_F(SearchControllerAshTest, DisconnectHandlersAreCalledInAdditionOrder) { |
| base::test::SingleThreadTaskEnvironment environment; |
| DisconnectTestFuture future_1; |
| DisconnectTestFuture future_2; |
| auto mojom_controller = std::make_unique<TestMojomSearchController>(); |
| SearchControllerAsh controller(mojom_controller->BindToRemote()); |
| |
| controller.AddDisconnectHandler( |
| future_1.GetCallback().Then(base::BindLambdaForTesting([&future_2]() { |
| EXPECT_FALSE(future_2.IsReady()) |
| << "Second future called before first future"; |
| }))); |
| controller.AddDisconnectHandler(future_2.GetCallback()); |
| mojom_controller.reset(); |
| ASSERT_TRUE(future_2.Wait()) << "Disconnect handler was never called"; |
| ASSERT_FALSE(controller.IsConnected()); |
| |
| // This also guarantees that the "first future called before second future" |
| // `EXPECT` above is run. |
| EXPECT_TRUE(future_1.IsReady()) << "First future not called"; |
| EXPECT_TRUE(future_2.IsReady()) << "Second future not called"; |
| } |
| |
| TEST_F(SearchControllerAshTest, |
| DisconnectHandlerIsImmediatelyCalledIfAlreadyDisconnected) { |
| base::test::SingleThreadTaskEnvironment environment; |
| DisconnectTestFuture future; |
| auto mojom_controller = std::make_unique<TestMojomSearchController>(); |
| SearchControllerAsh controller(mojom_controller->BindToRemote()); |
| mojom_controller.reset(); |
| { |
| DisconnectTestFuture future1; |
| controller.AddDisconnectHandler(future1.GetCallback()); |
| EXPECT_TRUE(future1.Wait()); |
| } |
| ASSERT_FALSE(controller.IsConnected()); |
| |
| controller.AddDisconnectHandler(future.GetCallback()); |
| |
| EXPECT_TRUE(future.IsReady()); |
| } |
| |
| TEST_F(SearchControllerAshTest, |
| DisconnectHandlerIsNotCalledIfNeverDisconnected) { |
| base::test::SingleThreadTaskEnvironment environment; |
| DisconnectTestFuture future; |
| TestMojomSearchController mojom_controller; |
| auto controller = |
| std::make_unique<SearchControllerAsh>(mojom_controller.BindToRemote()); |
| |
| controller->AddDisconnectHandler(future.GetCallback()); |
| controller.reset(); |
| |
| EXPECT_FALSE(future.IsReady()); |
| } |
| |
| TEST_F(SearchControllerAshTest, |
| DisconnectHandlerIsCalledWithNullptrIfDestructed) { |
| base::test::SingleThreadTaskEnvironment environment; |
| DisconnectTestFuture future; |
| auto mojom_controller = std::make_unique<TestMojomSearchController>(); |
| auto controller = |
| std::make_unique<SearchControllerAsh>(mojom_controller->BindToRemote()); |
| |
| controller->AddDisconnectHandler(base::BindLambdaForTesting( |
| [&controller](base::WeakPtr<SearchControllerAsh>) { |
| controller.reset(); |
| })); |
| controller->AddDisconnectHandler(future.GetCallback()); |
| mojom_controller.reset(); |
| |
| base::WeakPtr<SearchControllerAsh> weak_controller = future.Take(); |
| EXPECT_FALSE(weak_controller); |
| } |
| |
| } // namespace |
| } // namespace crosapi |