blob: 1d78433c41c8a5539999a02a9ee0a381ab984dcc [file] [log] [blame]
// Copyright 2025 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/ui/lens/lens_composebox_controller.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "chrome/browser/lens/core/mojom/lens.mojom.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/lens/lens_composebox_handler.h"
#include "chrome/browser/ui/lens/lens_overlay_controller.h"
#include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h"
#include "chrome/browser/ui/lens/lens_search_controller.h"
#include "chrome/browser/ui/lens/test_lens_overlay_controller.h"
#include "chrome/browser/ui/lens/test_lens_overlay_query_controller.h"
#include "chrome/browser/ui/lens/test_lens_overlay_side_panel_coordinator.h"
#include "chrome/browser/ui/lens/test_lens_search_contextualization_controller.h"
#include "chrome/browser/ui/lens/test_lens_search_controller.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/lens/lens_composebox_user_action.h"
#include "components/lens/lens_features.h"
#include "components/lens/lens_overlay_dismissal_source.h"
#include "components/lens/lens_overlay_invocation_source.h"
#include "components/lens/lens_overlay_permission_utils.h"
#include "components/lens/proto/server/lens_overlay_response.pb.h"
#include "components/omnibox/browser/aim_eligibility_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/lens_server_proto/aim_communication.pb.h"
#include "ui/base/unowned_user_data/user_data_factory.h"
namespace {
using State = LensOverlayController::State;
constexpr char kTestSearchSessionId[] = "test_search_session_id";
constexpr char kTestServerSessionId[] = "test_server_session_id";
const lens::mojom::CenterRotatedBoxPtr kTestRegion =
lens::mojom::CenterRotatedBox::New(
gfx::RectF(0.5, 0.5, 0.8, 0.8),
0.0,
lens::mojom::CenterRotatedBox_CoordinateType::kNormalized);
const SkBitmap CreateNonEmptyBitmap(int width, int height) {
SkBitmap bitmap;
bitmap.allocN32Pixels(width, height);
bitmap.eraseColor(SK_ColorGREEN);
return bitmap;
}
// A fake LensSearchController to inject fake controllers.
class LensSearchControllerFake : public lens::TestLensSearchController {
public:
explicit LensSearchControllerFake(tabs::TabInterface* tab)
: lens::TestLensSearchController(tab) {}
~LensSearchControllerFake() override = default;
protected:
std::unique_ptr<LensOverlayController> CreateLensOverlayController(
tabs::TabInterface* tab,
LensSearchController* lens_search_controller,
variations::VariationsClient* variations_client,
signin::IdentityManager* identity_manager,
PrefService* pref_service,
syncer::SyncService* sync_service,
ThemeService* theme_service) override {
return std::make_unique<lens::TestLensOverlayController>(
tab, lens_search_controller, variations_client, identity_manager,
pref_service, sync_service, theme_service);
}
std::unique_ptr<lens::LensOverlayQueryController> CreateLensQueryController(
lens::LensOverlayFullImageResponseCallback full_image_callback,
lens::LensOverlayUrlResponseCallback url_callback,
lens::LensOverlayInteractionResponseCallback interaction_callback,
lens::LensOverlaySuggestInputsCallback suggest_inputs_callback,
lens::LensOverlayThumbnailCreatedCallback thumbnail_created_callback,
lens::UploadProgressCallback upload_progress_callback,
variations::VariationsClient* variations_client,
signin::IdentityManager* identity_manager,
Profile* profile,
lens::LensOverlayInvocationSource invocation_source,
bool use_dark_mode,
lens::LensOverlayGen204Controller* gen204_controller) override {
auto fake_query_controller =
std::make_unique<lens::TestLensOverlayQueryController>(
full_image_callback, url_callback, interaction_callback,
suggest_inputs_callback, thumbnail_created_callback,
upload_progress_callback, variations_client, identity_manager,
profile, invocation_source, use_dark_mode, gen204_controller);
// Set up the cluster info to test the search session ID is propagated.
lens::LensOverlayServerClusterInfoResponse cluster_info_response;
cluster_info_response.set_server_session_id(kTestServerSessionId);
cluster_info_response.set_search_session_id(kTestSearchSessionId);
fake_query_controller->set_fake_cluster_info_response(
cluster_info_response);
return fake_query_controller;
}
std::unique_ptr<lens::LensOverlaySidePanelCoordinator>
CreateLensOverlaySidePanelCoordinator() override {
return std::make_unique<lens::TestLensOverlaySidePanelCoordinator>(this);
}
};
ui::UserDataFactory::ScopedOverride UseFakeLensSearchController() {
return tabs::TabFeatures::GetUserDataFactoryForTesting()
.AddOverrideForTesting(base::BindRepeating([](tabs::TabInterface& tab) {
return std::make_unique<LensSearchControllerFake>(&tab);
}));
}
} // namespace
class LensComposeboxControllerBrowserTest : public InProcessBrowserTest {
protected:
LensComposeboxControllerBrowserTest() {
lens_search_controller_override_ = UseFakeLensSearchController();
}
void SetUp() override {
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{
{lens::features::kLensOverlay,
/*params=*/{}},
{lens::features::kLensSearchAimM3, /*params=*/{}},
{lens::features::kLensOverlayContextualSearchbox,
{
// Updating the viewport each query can cause flakiness
// when checking the sequence ids.
{"update-viewport-each-query", "false"},
}},
},
/*disabled_features=*/{omnibox::kAimServerEligibilityEnabledEn});
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
embedded_test_server()->StartAcceptingConnections();
// Permits sharing the page screenshot by default.
PrefService* prefs = browser()->profile()->GetPrefs();
prefs->SetBoolean(lens::prefs::kLensSharingPageScreenshotEnabled, true);
prefs->SetBoolean(lens::prefs::kLensSharingPageContentEnabled, true);
}
void TearDownOnMainThread() override {
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
InProcessBrowserTest::TearDownOnMainThread();
// Disallow sharing the page screenshot by default.
PrefService* prefs = browser()->profile()->GetPrefs();
prefs->SetBoolean(lens::prefs::kLensSharingPageScreenshotEnabled, false);
prefs->SetBoolean(lens::prefs::kLensSharingPageContentEnabled, false);
}
void WaitForPaint(std::string_view relative_url = "/select.html") {
const GURL url = embedded_test_server()->GetURL(relative_url);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
}
LensSearchController* GetLensSearchController() {
return LensSearchController::From(browser()->GetActiveTabInterface());
}
LensOverlayController* GetLensOverlayController() {
return GetLensSearchController()->lens_overlay_controller();
}
lens::LensComposeboxController* GetLensComposeboxController() {
return GetLensSearchController()->lens_composebox_controller();
}
lens::TestLensOverlaySidePanelCoordinator* GetLensSidePanelCoordinator() {
return static_cast<lens::TestLensOverlaySidePanelCoordinator*>(
GetLensSearchController()->lens_overlay_side_panel_coordinator());
}
void MockAimToClientMessage(lens::AimToClientMessage aim_to_client_message) {
auto* lens_side_panel_coordinator = GetLensSidePanelCoordinator();
;
ASSERT_TRUE(lens_side_panel_coordinator);
// Serialize the message and pass it to the side panel coordinator.
std::vector<uint8_t> serialized_message(
aim_to_client_message.ByteSizeLong());
aim_to_client_message.SerializeToArray(&serialized_message[0],
serialized_message.size());
lens_side_panel_coordinator->OnAimMessage(serialized_message);
}
base::test::ScopedFeatureList feature_list_;
private:
ui::UserDataFactory::ScopedOverride lens_search_controller_override_;
};
IN_PROC_BROWSER_TEST_F(LensComposeboxControllerBrowserTest,
HandshakeResponseHandling) {
WaitForPaint();
auto* lens_controller = GetLensSearchController();
ASSERT_TRUE(lens_controller);
// Open the overlay directly to the side panel so composebox is visible.
SkBitmap initial_bitmap = CreateNonEmptyBitmap(100, 100);
GetLensSearchController()->OpenLensOverlayWithPendingRegion(
lens::LensOverlayInvocationSource::kContentAreaContextMenuImage,
kTestRegion->Clone(), initial_bitmap);
auto* overlay_controller = GetLensOverlayController();
ASSERT_TRUE(base::test::RunUntil([&]() {
return overlay_controller->state() == State::kOverlayAndResults;
}));
// Wait for the composebox handler to be set and then send a fake AIM query
// via mojo.
ASSERT_TRUE(base::test::RunUntil([&]() {
return GetLensComposeboxController()->composebox_handler_for_testing() !=
nullptr;
}));
// Mock a handshake response.
lens::AimToClientMessage aim_to_client_message;
aim_to_client_message.mutable_handshake_response()->add_capabilities(
lens::FeatureCapability::DEFAULT);
MockAimToClientMessage(aim_to_client_message);
// Verify the handshake is sent to the side panel.
auto* test_side_panel_coordinator = GetLensSidePanelCoordinator();
ASSERT_TRUE(test_side_panel_coordinator);
ASSERT_EQ(test_side_panel_coordinator->aim_handshake_received_call_count_, 1);
}
IN_PROC_BROWSER_TEST_F(LensComposeboxControllerBrowserTest,
IssueComposeboxQuerySendsPostMessage) {
WaitForPaint();
auto* lens_controller = GetLensSearchController();
ASSERT_TRUE(lens_controller);
// Open the overlay directly to the side panel so composebox is visible.
SkBitmap initial_bitmap = CreateNonEmptyBitmap(100, 100);
lens_controller->OpenLensOverlayWithPendingRegion(
lens::LensOverlayInvocationSource::kContentAreaContextMenuImage,
kTestRegion->Clone(), initial_bitmap);
auto* overlay_controller = GetLensOverlayController();
ASSERT_TRUE(base::test::RunUntil([&]() {
return overlay_controller->state() == State::kOverlayAndResults;
}));
// Wait for the composebox handler to be set and then send a fake AIM query
// via mojo.
ASSERT_TRUE(base::test::RunUntil([&]() {
return GetLensComposeboxController()->composebox_handler_for_testing() !=
nullptr;
}));
// Also need to run until the query controller has send all requests to avoid
// flakiness.
auto* fake_query_controller =
static_cast<lens::TestLensOverlayQueryController*>(
lens_controller->lens_overlay_query_controller());
ASSERT_TRUE(base::test::RunUntil([&]() {
return fake_query_controller->num_full_image_requests_sent() == 1 &&
fake_query_controller->num_page_content_update_requests_sent() ==
1 &&
fake_query_controller->num_interaction_requests_sent() == 1;
}));
// Mock a handshake call so the composebox controller can send query messages.
lens::AimToClientMessage aim_to_client_message;
aim_to_client_message.mutable_handshake_response()->add_capabilities(
lens::FeatureCapability::DEFAULT);
MockAimToClientMessage(aim_to_client_message);
// Send a query.
GetLensComposeboxController()->composebox_handler_for_testing()->SubmitQuery(
"test query", /*mouse_button=*/0, /*alt_key=*/false, /*ctrl_key=*/false,
/*meta_key=*/false,
/*shift_key=*/false);
// Verify the client message sent.
auto* test_side_panel_coordinator = GetLensSidePanelCoordinator();
ASSERT_TRUE(test_side_panel_coordinator);
ASSERT_TRUE(test_side_panel_coordinator->last_sent_client_message_to_aim_
.has_submit_query());
// Verify the submit query message.
auto submit_query = test_side_panel_coordinator
->last_sent_client_message_to_aim_.submit_query();
ASSERT_EQ(submit_query.payload().query_text(), "test query");
ASSERT_EQ(submit_query.payload().query_text_source(),
lens::QueryPayload::QUERY_TEXT_SOURCE_KEYBOARD_INPUT);
ASSERT_EQ(submit_query.payload().lens_image_query_data_size(), 1);
auto lens_image_query_data = submit_query.payload().lens_image_query_data(0);
ASSERT_EQ(lens_image_query_data.search_session_id(), kTestSearchSessionId);
ASSERT_EQ(lens_image_query_data.request_id().sequence_id(), 4);
ASSERT_EQ(lens_image_query_data.request_id().long_context_id(), 1);
ASSERT_EQ(lens_image_query_data.request_id().image_sequence_id(), 1);
}
IN_PROC_BROWSER_TEST_F(LensComposeboxControllerBrowserTest,
LogsComposeboxMetrics) {
base::HistogramTester histogram_tester;
WaitForPaint();
auto* lens_controller = GetLensSearchController();
ASSERT_TRUE(lens_controller);
// Open the overlay directly to the side panel so composebox is visible.
SkBitmap initial_bitmap = CreateNonEmptyBitmap(100, 100);
lens_controller->OpenLensOverlayWithPendingRegion(
lens::LensOverlayInvocationSource::kContentAreaContextMenuImage,
kTestRegion->Clone(), initial_bitmap);
auto* overlay_controller = GetLensOverlayController();
ASSERT_TRUE(base::test::RunUntil([&]() {
return overlay_controller->state() == State::kOverlayAndResults;
}));
// Wait for the composebox handler to be set and then send a fake AIM query
// via mojo.
ASSERT_TRUE(base::test::RunUntil([&]() {
return GetLensComposeboxController()->composebox_handler_for_testing() !=
nullptr;
}));
auto* composebox_handler =
GetLensComposeboxController()->composebox_handler_for_testing();
ASSERT_TRUE(composebox_handler);
// Mock a focus of the composebox. Should be logged.
composebox_handler->FocusChanged(true);
histogram_tester.ExpectBucketCount("Lens.Composebox.UserAction",
lens::LensComposeboxUserAction::kFocused,
1);
// Mock a focus out of the composebox. Should not be logged.
composebox_handler->FocusChanged(false);
histogram_tester.ExpectBucketCount("Lens.Composebox.UserAction",
lens::LensComposeboxUserAction::kFocused,
1);
// A new focus should be logged.
composebox_handler->FocusChanged(true);
histogram_tester.ExpectBucketCount("Lens.Composebox.UserAction",
lens::LensComposeboxUserAction::kFocused,
2);
// Mock a handshake response for the session end metrics.
lens::AimToClientMessage aim_to_client_message;
aim_to_client_message.mutable_handshake_response()->add_capabilities(
lens::FeatureCapability::DEFAULT);
MockAimToClientMessage(aim_to_client_message);
// Send a query.
GetLensComposeboxController()->composebox_handler_for_testing()->SubmitQuery(
"test query", /*mouse_button=*/0, /*alt_key=*/false, /*ctrl_key=*/false,
/*meta_key=*/false,
/*shift_key=*/false);
histogram_tester.ExpectBucketCount(
"Lens.Composebox.UserAction",
lens::LensComposeboxUserAction::kQuerySubmitted, 1);
// Send another query.
GetLensComposeboxController()->composebox_handler_for_testing()->SubmitQuery(
"test query 2", /*mouse_button=*/0, /*alt_key=*/false, /*ctrl_key=*/false,
/*meta_key=*/false,
/*shift_key=*/false);
histogram_tester.ExpectBucketCount(
"Lens.Composebox.UserAction",
lens::LensComposeboxUserAction::kQuerySubmitted, 2);
// Close the overlay to trigger session end metrics.
lens_controller->CloseLensSync(
lens::LensOverlayDismissalSource::kOverlayCloseButton);
ASSERT_TRUE(base::test::RunUntil(
[&]() { return overlay_controller->state() == State::kOff; }));
// Verify session end metrics are logged once.
histogram_tester.ExpectUniqueSample("Lens.Composebox.ShownInSession", true,
1);
histogram_tester.ExpectUniqueSample(
"Lens.Composebox.HandshakeCompletedInSession", true, 1);
histogram_tester.ExpectBucketCount("Lens.Composebox.UserActionInSession",
lens::LensComposeboxUserAction::kFocused,
1);
histogram_tester.ExpectBucketCount(
"Lens.Composebox.UserActionInSession",
lens::LensComposeboxUserAction::kQuerySubmitted, 1);
// Start a new session.
lens_controller->OpenLensOverlayWithPendingRegion(
lens::LensOverlayInvocationSource::kContentAreaContextMenuImage,
kTestRegion->Clone(), initial_bitmap);
ASSERT_TRUE(base::test::RunUntil([&]() {
return overlay_controller->state() == State::kOverlayAndResults;
}));
// Wait for the composebox handler to be set.
ASSERT_TRUE(base::test::RunUntil([&]() {
return GetLensComposeboxController()->composebox_handler_for_testing() !=
nullptr;
}));
// Close the overlay to trigger session end metrics again.
lens_controller->CloseLensSync(
lens::LensOverlayDismissalSource::kSidePanelCloseButton);
ASSERT_TRUE(base::test::RunUntil(
[&]() { return overlay_controller->state() == State::kOff; }));
// Verify session end metrics totals.
histogram_tester.ExpectUniqueSample("Lens.Composebox.ShownInSession", true,
2);
histogram_tester.ExpectBucketCount(
"Lens.Composebox.HandshakeCompletedInSession", true, 1);
histogram_tester.ExpectBucketCount(
"Lens.Composebox.HandshakeCompletedInSession", false, 1);
histogram_tester.ExpectBucketCount("Lens.Composebox.UserActionInSession",
lens::LensComposeboxUserAction::kFocused,
1);
histogram_tester.ExpectBucketCount(
"Lens.Composebox.UserActionInSession",
lens::LensComposeboxUserAction::kQuerySubmitted, 1);
}
IN_PROC_BROWSER_TEST_F(LensComposeboxControllerBrowserTest,
LensButtonClickReshowsOverlay) {
WaitForPaint();
auto* lens_controller = GetLensSearchController();
ASSERT_TRUE(lens_controller);
// Open the overlay directly to the side panel so composebox is visible.
SkBitmap initial_bitmap = CreateNonEmptyBitmap(100, 100);
lens_controller->OpenLensOverlayWithPendingRegion(
lens::LensOverlayInvocationSource::kContentAreaContextMenuImage,
kTestRegion->Clone(), initial_bitmap);
auto* overlay_controller = GetLensOverlayController();
ASSERT_TRUE(base::test::RunUntil([&]() {
return overlay_controller->state() == State::kOverlayAndResults;
}));
// Wait for the composebox handler to be set.
ASSERT_TRUE(base::test::RunUntil([&]() {
return GetLensComposeboxController()->composebox_handler_for_testing() !=
nullptr;
}));
// Hide the overlay. The state should transition to hidden since the side
// panel is open.
lens_controller->HideOverlay(
lens::LensOverlayDismissalSource::kOverlayBackgroundClick);
ASSERT_TRUE(base::test::RunUntil(
[&]() { return overlay_controller->state() == State::kHidden; }));
// Simulate Lens button click. This should reshow the overlay.
GetLensComposeboxController()
->composebox_handler_for_testing()
->HandleLensButtonClick();
ASSERT_TRUE(base::test::RunUntil([&]() {
return overlay_controller->state() == State::kOverlayAndResults;
}));
}