diff --git a/browser/ui/ai_chat/BUILD.gn b/browser/ui/ai_chat/BUILD.gn index 08fe8f927b1c..429b16e73a3a 100644 --- a/browser/ui/ai_chat/BUILD.gn +++ b/browser/ui/ai_chat/BUILD.gn @@ -35,3 +35,26 @@ source_set("unit_tests") { "//testing/gtest:gtest", ] } + +source_set("browser_tests") { + testonly = true + + sources = [] + + if (!is_android) { + sources += [ "tab_informer_browsertest.cc" ] + } + + defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ] + + deps = [ + "//base/test:test_support", + "//brave/components/ai_chat/content/browser", + "//brave/components/ai_chat/core/browser", + "//brave/components/ai_chat/core/common", + "//brave/components/ai_chat/core/common/mojom", + "//chrome/browser/ui", + "//chrome/test:test_support", + "//testing/gtest", + ] +} diff --git a/browser/ui/ai_chat/tab_informer.h b/browser/ui/ai_chat/tab_informer.h index 065218844bcc..f54c43a709ef 100644 --- a/browser/ui/ai_chat/tab_informer.h +++ b/browser/ui/ai_chat/tab_informer.h @@ -50,6 +50,8 @@ class TabInformer : public mojom::TabInformer, bool ShouldTrackBrowser(Browser* browser) override; private: + friend class TabInformerBrowserTest; + State GetState(); void NotifyListeners(); void NotifyListener(mojom::TabListener* listener, State state); diff --git a/browser/ui/ai_chat/tab_informer_browsertest.cc b/browser/ui/ai_chat/tab_informer_browsertest.cc new file mode 100644 index 000000000000..ab51d99668e2 --- /dev/null +++ b/browser/ui/ai_chat/tab_informer_browsertest.cc @@ -0,0 +1,211 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/ai_chat/tab_informer.h" + +#include +#include + +#include "base/functional/callback_forward.h" +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "brave/components/ai_chat/core/common/mojom/tab_informer.mojom-forward.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/test/browser_test.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/self_owned_receiver.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ai_chat { + +namespace { + +using Predicate = + base::RepeatingCallback&)>; + +class UpdateTracker : public mojom::TabListener { + public: + UpdateTracker() = default; + ~UpdateTracker() override = default; + + const std::vector& GetLastWindows() const { + CHECK(last_windows_.has_value()); + return last_windows_.value(); + } + + void WaitFor(Predicate predicate) { + if (last_windows_ && predicate.Run(last_windows_.value())) { + return; + } + + base::RunLoop waiter; + on_change_ = base::BindLambdaForTesting([&]() { + if (!last_windows_) { + return; + } + if (predicate.Run(last_windows_.value())) { + waiter.Quit(); + on_change_.Reset(); + } + }); + + waiter.Run(); + } + + private: + void TabsChanged(std::vector windows) override { + last_windows_ = std::move(windows); + + if (!on_change_.is_null()) { + on_change_.Run(); + } + } + + base::RepeatingClosure on_change_; + std::optional> last_windows_; + std::unique_ptr run_loop_; +}; + +} // namespace + +class TabInformerBrowserTest : public InProcessBrowserTest { + public: + TabInformerBrowserTest() = default; + ~TabInformerBrowserTest() override = default; + + void SetUpOnMainThread() override { + InProcessBrowserTest::SetUpOnMainThread(); + + mojo::PendingReceiver receiver; + tab_informer_ = std::make_unique( + std::move(receiver), + browser()->tab_strip_model()->GetActiveWebContents()); + + auto listener = std::make_unique(); + tracker_ = listener.get(); + + mojo::PendingRemote pending_remote; + mojo::MakeSelfOwnedReceiver( + std::move(listener), pending_remote.InitWithNewPipeAndPassReceiver()); + tab_informer_->AddListener(std::move(pending_remote)); + } + + void TearDownOnMainThread() override { + tracker_ = nullptr; + tab_informer_.reset(); + + InProcessBrowserTest::SetUpOnMainThread(); + } + + const std::vector& GetLastWindows() const { + return tracker_->GetLastWindows(); + } + + void WaitFor(Predicate predicate) { tracker_->WaitFor(std::move(predicate)); } + + protected: + void AppendTab(std::string url) { + chrome::AddTabAt(browser(), GURL(url), -1, false); + } + + std::unique_ptr tab_informer_; + raw_ptr tracker_; +}; + +IN_PROC_BROWSER_TEST_F(TabInformerBrowserTest, GetsInitialTabs) { + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + return windows.size() == 1; + })); + + const auto& windows = GetLastWindows(); + + // Should be have no tabs because we exclude the owner tab + EXPECT_EQ(windows[0]->tabs.size(), 0u); +} + +IN_PROC_BROWSER_TEST_F(TabInformerBrowserTest, TabsChange) { + AppendTab("https://google.com"); + AppendTab("https://brave.com"); + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + return windows.size() == 1 && windows.at(0)->tabs.size() == 2; + })); + + { + const auto& windows = GetLastWindows(); + EXPECT_EQ(windows[0]->tabs[0]->url, GURL("https://google.com")); + EXPECT_EQ(windows[0]->tabs[1]->url, GURL("https://brave.com")); + } + + // Close all tabs but the one we're bound to + chrome::CloseOtherTabs(browser()); + + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + return windows.size() == 1 && windows.at(0)->tabs.size() == 0; + })); + + { + const auto& updated_windows = GetLastWindows(); + EXPECT_EQ(updated_windows[0]->tabs.size(), 0u); + } +} + +IN_PROC_BROWSER_TEST_F(TabInformerBrowserTest, MultipleWindows) { + AppendTab("https://topos.nz"); + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + return windows.size() == 1 && windows.at(0)->tabs.size() == 1; + })); + + { + const auto& windows = GetLastWindows(); + EXPECT_EQ(windows[0]->tabs[0]->url, GURL("https://topos.nz")); + } + + chrome::NewWindow(browser()); + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + return windows.size() == 2 && windows.at(0)->tabs.size() == 1 && + windows.at(1)->tabs.size() == 0; + })); + + AppendTab("https://brave.com"); + AppendTab("https://readr.nz"); + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + return windows.size() == 2 && windows.at(0)->tabs.size() == 3 && + windows.at(1)->tabs.size() == 0; + })); + + chrome::MoveTabsToNewWindow(browser(), {2, 3}); + WaitFor(base::BindLambdaForTesting( + [](const std::vector& windows) { + LOG(ERROR) << "Window 0: " << windows.at(0)->tabs.size() + << ", Window 1: " << windows.at(1)->tabs.size(); + return windows.size() == 3 && windows.at(0)->tabs.size() == 1 && + windows.at(1)->tabs.size() == 0 && + windows.at(2)->tabs.size() == 2; + })); + + { + const auto& windows = GetLastWindows(); + + // Just topos.nz - first tab is bound to the TabInformer + EXPECT_EQ(windows[0]->tabs[0]->url, GURL("https://topos.nz")); + + // readr.nz, brave.com + EXPECT_EQ(windows[2]->tabs[1]->url, GURL("https://readr.nz")); + EXPECT_EQ(windows[2]->tabs[0]->url, GURL("https://brave.com")); + } +} + +} // namespace ai_chat diff --git a/components/ai_chat/core/browser/ai_chat_service.cc b/components/ai_chat/core/browser/ai_chat_service.cc index 6354eedad3b3..fee41287acb5 100644 --- a/components/ai_chat/core/browser/ai_chat_service.cc +++ b/components/ai_chat/core/browser/ai_chat_service.cc @@ -159,8 +159,7 @@ ConversationHandler* AIChatService::CreateConversation() { conversation_uuid, "", base::Time::Now(), false, std::nullopt, mojom::SiteInfo::New(base::Uuid::GenerateRandomV4().AsLowercaseString(), mojom::ContentType::PageContent, std::nullopt, - std::nullopt, -1, std::nullopt, 0, false, - false)); + std::nullopt, -1, std::nullopt, 0, false, false)); conversations_.insert_or_assign(conversation_uuid, std::move(conversation)); } mojom::Conversation* conversation = diff --git a/ios/browser/api/ai_chat/BUILD.gn b/ios/browser/api/ai_chat/BUILD.gn index d7752a042108..2d4497e0e9c9 100644 --- a/ios/browser/api/ai_chat/BUILD.gn +++ b/ios/browser/api/ai_chat/BUILD.gn @@ -69,6 +69,7 @@ ios_objc_mojom_wrappers("ai_chat_mojom_wrappers") { mojom_target = "//brave/components/ai_chat/core/common/mojom" sources = [ "//brave/components/ai_chat/core/common/mojom/ai_chat.mojom", + "//brave/components/ai_chat/core/common/mojom/tab_informer.mojom", "//brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom", ] output_dir = "$root_gen_dir/brave/components/ai_chat/core/common/mojom/ios" @@ -86,5 +87,6 @@ ios_objc_mojom_wrappers("ai_chat_mojom_wrappers") { "UntrustedConversationHandler", "UntrustedConversationUI", "UntrustedUIHandler", + "TabInformer", ] } diff --git a/test/BUILD.gn b/test/BUILD.gn index 2605215d0c6c..972431215b12 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -866,6 +866,7 @@ test("brave_browser_tests") { "//brave/browser/test:browser_tests", "//brave/browser/themes", "//brave/browser/ui:browser_tests", + "//brave/browser/ui/ai_chat:browser_tests", "//brave/browser/ui/tabs/test:browser_tests", "//brave/browser/ui/views/tabs:browser_tests", "//brave/browser/ui/webui:browser_tests",