From f57bc2b96fcb0d1fbaccb041ff1764dc250006c1 Mon Sep 17 00:00:00 2001 From: Anthony Tseng Date: Thu, 14 Mar 2024 14:51:44 -0700 Subject: [PATCH] Add AIChatUIBrowserTest and fix existing tests --- browser/ai_chat/BUILD.gn | 2 + browser/ai_chat/DEPS | 4 + browser/ai_chat/ai_chat_ui_browsertest.cc | 221 ++++++++++++++++++ .../page_content_fetcher_browsertest.cc | 21 +- browser/ui/webui/ai_chat/ai_chat_ui.h | 4 + .../webui/ai_chat/ai_chat_ui_page_handler.h | 2 + .../content/browser/ai_chat_tab_helper.cc | 15 +- .../content/browser/ai_chat_tab_helper.h | 10 + test/data/leo/extra_long_canvas.html | 32 +++ test/data/leo/long_canvas.html | 40 ++++ 10 files changed, 327 insertions(+), 24 deletions(-) create mode 100644 browser/ai_chat/DEPS create mode 100644 browser/ai_chat/ai_chat_ui_browsertest.cc create mode 100644 test/data/leo/extra_long_canvas.html create mode 100644 test/data/leo/long_canvas.html diff --git a/browser/ai_chat/BUILD.gn b/browser/ai_chat/BUILD.gn index 277e342e9cef3..fe66c8f26cb04 100644 --- a/browser/ai_chat/BUILD.gn +++ b/browser/ai_chat/BUILD.gn @@ -33,6 +33,7 @@ source_set("browser_tests") { sources = [ "ai_chat_policy_browsertest.cc", "ai_chat_profile_browsertest.cc", + "ai_chat_ui_browsertest.cc", "page_content_fetcher_browsertest.cc", ] deps = [ @@ -47,6 +48,7 @@ source_set("browser_tests") { "//chrome/browser", "//chrome/test:test_support", "//chrome/test:test_support_ui", + "//printing/buildflags", ] } } diff --git a/browser/ai_chat/DEPS b/browser/ai_chat/DEPS new file mode 100644 index 0000000000000..f28bb8a9e5171 --- /dev/null +++ b/browser/ai_chat/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+printing/buildflags", +] + diff --git a/browser/ai_chat/ai_chat_ui_browsertest.cc b/browser/ai_chat/ai_chat_ui_browsertest.cc new file mode 100644 index 0000000000000..375aa88a57322 --- /dev/null +++ b/browser/ai_chat/ai_chat_ui_browsertest.cc @@ -0,0 +1,221 @@ +/* Copyright (c) 2024 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 "base/files/file_path.h" +#include "base/functional/bind.h" +#include "base/functional/callback_helpers.h" +#include "base/path_service.h" +#include "base/run_loop.h" +#include "base/strings/strcat.h" +#include "base/test/bind.h" +#include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" +#include "brave/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h" +#include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h" +#include "brave/components/constants/brave_paths.h" +#include "brave/components/l10n/common/test/scoped_default_locale.h" +#include "brave/components/text_recognition/common/buildflags/buildflags.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/side_panel/side_panel_ui.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/browser/ui/views/side_panel/side_panel_web_ui_view.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/content_mock_cert_verifier.h" +#include "net/dns/mock_host_resolver.h" +#include "printing/buildflags/buildflags.h" +#include "ui/compositor/compositor_switches.h" + +namespace { + +constexpr char kEmbeddedTestServerDirectory[] = "leo"; +} // namespace + +class AIChatUIBrowserTest : public InProcessBrowserTest { + public: + AIChatUIBrowserTest() : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {} + + void SetUpOnMainThread() override { + mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); + host_resolver()->AddRule("*", "127.0.0.1"); + content::SetupCrossSiteRedirector(&https_server_); + + brave::RegisterPathProvider(); + base::FilePath test_data_dir; + test_data_dir = base::PathService::CheckedGet(brave::DIR_TEST_DATA); + test_data_dir = test_data_dir.AppendASCII(kEmbeddedTestServerDirectory); + https_server_.ServeFilesFromDirectory(test_data_dir); + ASSERT_TRUE(https_server_.Start()); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + InProcessBrowserTest::SetUpCommandLine(command_line); +#if BUILDFLAG(ENABLE_TEXT_RECOGNITION) + command_line->AppendSwitch(::switches::kEnablePixelOutputInTests); +#endif + mock_cert_verifier_.SetUpCommandLine(command_line); + } + + void SetUpInProcessBrowserTestFixture() override { + InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); + mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); + } + + void TearDownInProcessBrowserTestFixture() override { + mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); + InProcessBrowserTest::TearDownInProcessBrowserTestFixture(); + } + + content::WebContents* GetActiveWebContents() { + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + void NavigateURL(const GURL& url) { + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); + ASSERT_TRUE(WaitForLoadStop(GetActiveWebContents())); + } + + void CreatePrintPreview(ai_chat::AIChatUIPageHandler* handler) { +#if BUILDFLAG(ENABLE_PRINT_PREVIEW) + handler->MaybeCreatePrintPreview(); +#endif + } + + ai_chat::AIChatUIPageHandler* OpenAIChatSidePanel() { + auto* side_panel_ui = SidePanelUI::GetSidePanelUIForBrowser(browser()); + side_panel_ui->Show(SidePanelEntryId::kChatUI); + auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser()); + auto* side_panel = browser_view->unified_side_panel(); + auto* ai_chat_side_panel = + side_panel->GetViewByID(SidePanelWebUIView::kSidePanelWebViewId); + if (!ai_chat_side_panel) { + return nullptr; + } + auto* side_panel_web_contents = + (static_cast(ai_chat_side_panel))->web_contents(); + if (!side_panel_web_contents) { + return nullptr; + } + content::WaitForLoadStop(side_panel_web_contents); + + auto* web_ui = side_panel_web_contents->GetWebUI(); + if (!web_ui) { + return nullptr; + } + auto* ai_chat_ui = web_ui->GetController()->GetAs(); + if (!ai_chat_ui) { + return nullptr; + } + return ai_chat_ui->GetPageHandlerForTesting(); + } + + void FetchPageContent(const base::Location& location, + ai_chat::AIChatTabHelper* helper, + std::string_view expected_text) { + SCOPED_TRACE(testing::Message() << location.ToString()); + base::RunLoop run_loop; + helper->GetPageContent( + base::BindLambdaForTesting( + [&run_loop, expected_text](std::string text, bool is_video, + std::string invalidation_token) { + EXPECT_FALSE(is_video); + EXPECT_EQ(text, expected_text); + run_loop.Quit(); + }), + ""); + run_loop.Run(); + } + + protected: + net::test_server::EmbeddedTestServer https_server_; + + private: + content::ContentMockCertVerifier mock_cert_verifier_; +}; + +IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreview) { + browser()->window()->SetContentsSize(gfx::Size(800, 600)); + + auto* chat_tab_helper = + ai_chat::AIChatTabHelper::FromWebContents(GetActiveWebContents()); + ASSERT_TRUE(chat_tab_helper); + chat_tab_helper->SetUserOptedIn(true); + auto* ai_chat_page_handler = OpenAIChatSidePanel(); + ASSERT_TRUE(ai_chat_page_handler); + + NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html")); + CreatePrintPreview(ai_chat_page_handler); +#if BUILDFLAG(ENABLE_TEXT_RECOGNITION) + FetchPageContent( + FROM_HERE, chat_tab_helper, + "This is the way.\n\nI have spoken.\nWherever I Go, He Goes."); + // Panel is still active so we don't need to set it up again + + // Page recognition host with a canvas element + NavigateURL(https_server_.GetURL("docs.google.com", "/canvas.html")); + CreatePrintPreview(ai_chat_page_handler); + FetchPageContent(FROM_HERE, chat_tab_helper, "this is the way"); +#if BUILDFLAG(IS_WIN) + // Unsupported locale should return no content for Windows only + // Other platforms do not use locale for extraction + const brave_l10n::test::ScopedDefaultLocale locale("xx_XX"); + NavigateURL(https_server_.GetURL("docs.google.com", "/canvas.html")); + CreatePrintPreview(ai_chat_page_handler); + FetchPageContent(FROM_HERE, chat_tab_helper, ""); +#endif // #if BUILDFLAG(IS_WIN) +#else + FetchPageContent(FROM_HERE, chat_tab_helper, ""); +#endif + + // Not supported on other hosts + NavigateURL(https_server_.GetURL("a.com", "/long_canvas.html")); + CreatePrintPreview(ai_chat_page_handler); + FetchPageContent(FROM_HERE, chat_tab_helper, ""); +} + +#if BUILDFLAG(ENABLE_TEXT_RECOGNITION) +IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreviewPagesLimit) { + browser()->window()->SetContentsSize(gfx::Size(800, 600)); + + auto* chat_tab_helper = + ai_chat::AIChatTabHelper::FromWebContents(GetActiveWebContents()); + ASSERT_TRUE(chat_tab_helper); + chat_tab_helper->SetUserOptedIn(true); + auto* ai_chat_page_handler = OpenAIChatSidePanel(); + ASSERT_TRUE(ai_chat_page_handler); + + NavigateURL( + https_server_.GetURL("docs.google.com", "/extra_long_canvas.html")); + CreatePrintPreview(ai_chat_page_handler); + std::string expected_string(19, '\n'); + base::StrAppend(&expected_string, {"This is the way."}); + FetchPageContent(FROM_HERE, chat_tab_helper, expected_string); +} + +IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreviewContextLimit) { + browser()->window()->SetContentsSize(gfx::Size(800, 600)); + + auto* chat_tab_helper = + ai_chat::AIChatTabHelper::FromWebContents(GetActiveWebContents()); + ASSERT_TRUE(chat_tab_helper); + chat_tab_helper->SetUserOptedIn(true); + auto* ai_chat_page_handler = OpenAIChatSidePanel(); + ASSERT_TRUE(ai_chat_page_handler); + + chat_tab_helper->SetMaxContentLengthForTesting(10); + NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html")); + CreatePrintPreview(ai_chat_page_handler); + FetchPageContent(FROM_HERE, chat_tab_helper, "This is the way."); + + chat_tab_helper->SetMaxContentLengthForTesting(20); + NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html")); + CreatePrintPreview(ai_chat_page_handler); + FetchPageContent(FROM_HERE, chat_tab_helper, + "This is the way.\n\nI have spoken."); +} +#endif diff --git a/browser/ai_chat/page_content_fetcher_browsertest.cc b/browser/ai_chat/page_content_fetcher_browsertest.cc index 5c4e41e79baa4..fa9c077b96c73 100644 --- a/browser/ai_chat/page_content_fetcher_browsertest.cc +++ b/browser/ai_chat/page_content_fetcher_browsertest.cc @@ -14,8 +14,6 @@ #include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h" #include "brave/components/ai_chat/content/browser/page_content_fetcher.h" #include "brave/components/constants/brave_paths.h" -#include "brave/components/l10n/common/test/scoped_default_locale.h" -#include "brave/components/text_recognition/common/buildflags/buildflags.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/test/base/in_process_browser_test.h" @@ -27,7 +25,6 @@ #include "net/dns/mock_host_resolver.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/test/test_url_loader_factory.h" -#include "ui/compositor/compositor_switches.h" namespace { @@ -77,9 +74,6 @@ class PageContentFetcherBrowserTest : public InProcessBrowserTest { void SetUpCommandLine(base::CommandLine* command_line) override { InProcessBrowserTest::SetUpCommandLine(command_line); -#if BUILDFLAG(ENABLE_TEXT_RECOGNITION) - command_line->AppendSwitch(::switches::kEnablePixelOutputInTests); -#endif mock_cert_verifier_.SetUpCommandLine(command_line); } @@ -109,7 +103,8 @@ class PageContentFetcherBrowserTest : public InProcessBrowserTest { SCOPED_TRACE(testing::Message() << location.ToString()); base::RunLoop run_loop; ai_chat::FetchPageContent( - browser()->tab_strip_model()->GetActiveWebContents(), "", + browser()->tab_strip_model()->GetActiveWebContents(), "", std::nullopt, + 4096, base::BindLambdaForTesting([&run_loop, &expected_text, &expected_is_video, &trim_whitespace]( std::string text, bool is_video, @@ -171,20 +166,8 @@ IN_PROC_BROWSER_TEST_F(PageContentFetcherBrowserTest, FetchPageContent) { // Not a page extraction host and page with no text NavigateURL(https_server_.GetURL("a.com", "/canvas.html")); FetchPageContent(FROM_HERE, "", false); -#if BUILDFLAG(ENABLE_TEXT_RECOGNITION) - // Page recognition host with a canvas element - NavigateURL(https_server_.GetURL("docs.google.com", "/canvas.html")); - FetchPageContent(FROM_HERE, "this is the way", false); -#if BUILDFLAG(IS_WIN) - // Unsupported locale should return no content for Windows only - // Other platforms do not use locale for extraction - const brave_l10n::test::ScopedDefaultLocale locale("xx_XX"); - NavigateURL(https_server_.GetURL("docs.google.com", "/canvas.html")); - FetchPageContent(FROM_HERE, "", false); -#endif // #if BUILDFLAG(IS_WIN) NavigateURL(https_server_.GetURL("github.com", kGithubUrlPath)); FetchPageContent(FROM_HERE, kGithubPatch, false); -#endif // #if BUILDFLAG(ENABLE_TEXT_RECOGNITION) } IN_PROC_BROWSER_TEST_F(PageContentFetcherBrowserTest, FetchPageContentPDF) { diff --git a/browser/ui/webui/ai_chat/ai_chat_ui.h b/browser/ui/webui/ai_chat/ai_chat_ui.h index f3676b337fffd..fbeca3d3297c0 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui.h +++ b/browser/ui/webui/ai_chat/ai_chat_ui.h @@ -75,6 +75,10 @@ class AIChatUI : public ui::UntrustedWebUIController static constexpr std::string GetWebUIName() { return "AIChatPanel"; } + ai_chat::AIChatUIPageHandler* GetPageHandlerForTesting() { + return page_handler_.get(); + } + private: #if BUILDFLAG(ENABLE_PRINT_PREVIEW) // printing::mojo::PrintPreviewUI: diff --git a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h index 7f05d0031d37d..edfa6685dbbcb 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h +++ b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h @@ -42,6 +42,7 @@ class FaviconService; } // namespace favicon class AIChatUI; +class AIChatUIBrowserTest; namespace ai_chat { class AIChatUIPageHandler : public ai_chat::mojom::PageHandler, public AIChatTabHelper::Observer, @@ -105,6 +106,7 @@ class AIChatUIPageHandler : public ai_chat::mojom::PageHandler, #endif private: + friend class ::AIChatUIBrowserTest; // AIChatTabHelper::Observer void OnHistoryUpdate() override; void OnAPIRequestInProgress(bool in_progress) override; diff --git a/components/ai_chat/content/browser/ai_chat_tab_helper.cc b/components/ai_chat/content/browser/ai_chat_tab_helper.cc index 21c4dcad9659d..b2f416c77afa9 100644 --- a/components/ai_chat/content/browser/ai_chat_tab_helper.cc +++ b/components/ai_chat/content/browser/ai_chat_tab_helper.cc @@ -97,7 +97,7 @@ void AIChatTabHelper::OnPDFA11yInfoLoaded() { is_pdf_a11y_info_loaded_ = true; if (pending_get_page_content_callback_) { FetchPageContent(web_contents(), "", std::nullopt, - GetCurrentModel().max_page_content_length, + GetMaxPageContentLength(), std::move(pending_get_page_content_callback_)); } pdf_load_observer_.reset(); @@ -109,8 +109,7 @@ void AIChatTabHelper::OnPDFA11yInfoLoaded() { void AIChatTabHelper::OnPreviewReady( const std::optional>& bitmaps) { if (pending_get_page_content_callback_) { - FetchPageContent(web_contents(), "", bitmaps, - GetCurrentModel().max_page_content_length, + FetchPageContent(web_contents(), "", bitmaps, GetMaxPageContentLength(), std::move(pending_get_page_content_callback_)); } } @@ -222,8 +221,7 @@ void AIChatTabHelper::GetPageContent(GetPageContentCallback callback, pending_get_page_content_callback_ = std::move(callback); } else { FetchPageContent(web_contents(), invalidation_token, std::nullopt, - GetCurrentModel().max_page_content_length, - std::move(callback)); + GetMaxPageContentLength(), std::move(callback)); } } } @@ -232,6 +230,13 @@ std::u16string AIChatTabHelper::GetPageTitle() const { return web_contents()->GetTitle(); } +uint32_t AIChatTabHelper::GetMaxPageContentLength() { + if (max_page_content_length_for_testing_) { + return *max_page_content_length_for_testing_; + } + return GetCurrentModel().max_page_content_length; +} + WEB_CONTENTS_USER_DATA_KEY_IMPL(AIChatTabHelper); } // namespace ai_chat diff --git a/components/ai_chat/content/browser/ai_chat_tab_helper.h b/components/ai_chat/content/browser/ai_chat_tab_helper.h index ddfa907bb78bf..6620743c6a954 100644 --- a/components/ai_chat/content/browser/ai_chat_tab_helper.h +++ b/components/ai_chat/content/browser/ai_chat_tab_helper.h @@ -28,6 +28,7 @@ namespace content { class ScopedAccessibilityMode; } +class AIChatUIBrowserTest; namespace ai_chat { class AIChatMetrics; @@ -43,12 +44,17 @@ class AIChatTabHelper : public content::WebContentsObserver, void SetOnPDFA11yInfoLoadedCallbackForTesting(base::OnceClosure cb); + void SetMaxContentLengthForTesting(std::optional max_length) { + max_page_content_length_for_testing_ = max_length; + } + // This will be called when print preview has been composited into image per // page. void OnPreviewReady(const std::optional>&); private: friend class content::WebContentsUserData; + friend class ::AIChatUIBrowserTest; // To observe PDF InnerWebContents for "Finished loading PDF" event which // means PDF content has been loaded to an accessibility tree. @@ -96,8 +102,12 @@ class AIChatTabHelper : public content::WebContentsObserver, std::string_view invalidation_token) override; std::u16string GetPageTitle() const override; + uint32_t GetMaxPageContentLength(); + raw_ptr ai_chat_metrics_; + std::optional max_page_content_length_for_testing_; + bool is_same_document_navigation_ = false; int64_t pending_navigation_id_; bool is_pdf_a11y_info_loaded_ = false; diff --git a/test/data/leo/extra_long_canvas.html b/test/data/leo/extra_long_canvas.html new file mode 100644 index 0000000000000..fc11f004c17b9 --- /dev/null +++ b/test/data/leo/extra_long_canvas.html @@ -0,0 +1,32 @@ + + + + Leo test + + + + + + diff --git a/test/data/leo/long_canvas.html b/test/data/leo/long_canvas.html new file mode 100644 index 0000000000000..6860335627d08 --- /dev/null +++ b/test/data/leo/long_canvas.html @@ -0,0 +1,40 @@ + + + + Leo test + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +