blob: 6a2dc977726f506988fd9c4ac51796d880efcfdc [file]
// 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/contextual_tasks/contextual_tasks_composebox_handler.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "chrome/browser/contextual_search/contextual_search_service_factory.h"
#include "chrome/browser/contextual_search/contextual_search_web_contents_helper.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_service_factory.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui_service.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui_service_factory.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/contextual_search/desktop_query_contextualizer_delegate.h"
#include "chrome/browser/ui/contextual_search/tab_contextualization_controller.h"
#include "chrome/browser/ui/lens/lens_query_flow_router.h"
#include "chrome/browser/ui/lens/lens_search_controller.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "components/omnibox/common/omnibox_features.h"
#include "chrome/browser/ui/webui/searchbox/searchbox_test_utils.h"
#include "chrome/browser/ui/webui/webui_embedding_context.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/contextual_search/contextual_search_metrics_recorder.h"
#include "components/contextual_search/contextual_search_service.h"
#include "components/contextual_search/contextual_search_session_handle.h"
#include "components/contextual_search/contextual_search_types.h"
#include "components/contextual_search/fake_variations_client.h"
#include "components/contextual_search/mock_contextual_search_context_controller.h"
#include "components/contextual_search/mock_contextual_search_session_handle.h"
#include "components/contextual_tasks/public/contextual_task.h"
#include "components/contextual_tasks/public/contextual_tasks_service.h"
#include "components/contextual_tasks/public/features.h"
#include "components/contextual_tasks/public/mock_contextual_tasks_service.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/omnibox/common/composebox_features.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/tabs/public/mock_tab_interface.h"
#include "components/variations/scoped_variations_ids_provider.h"
#include "components/variations/variations_switches.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_web_ui.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 "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/lens_server_proto/aim_communication.pb.h"
#include "third_party/omnibox_proto/chrome_aim_entry_point.pb.h"
#include "url/gurl.h"
#include "url/url_constants.h"
class BrowserWindowInterface;
class TemplateURLService;
class LocalContextualSearchboxHandlerTestHarness : public InProcessBrowserTest {
public:
LocalContextualSearchboxHandlerTestHarness() = default;
~LocalContextualSearchboxHandlerTestHarness() override = default;
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
tabs::TabInterface* active_tab = AddTab(GURL(url::kAboutBlankURL));
web_contents_ = active_tab->GetContents();
template_url_service_ = TemplateURLServiceFactory::GetForProfile(profile());
}
void TearDownOnMainThread() override {
// Safely reset pointers inside controllers to avoid dangling references
for (int i = 0; i < browser()->tab_strip_model()->count(); ++i) {
tabs::TabInterface* tab =
tabs::TabLookupFromWebContents::FromWebContents(
browser()->tab_strip_model()->GetWebContentsAt(i))
->model();
if (tab && tab->GetTabFeatures()) {
tab->GetTabFeatures()->SetTabContextualizationControllerForTesting(
nullptr);
}
}
mock_tab_controller_ = nullptr;
web_contents_ = nullptr;
template_url_service_ = nullptr;
tab_features_override_ = nullptr;
InProcessBrowserTest::TearDownOnMainThread();
}
protected:
raw_ptr<content::WebContents> web_contents_ = nullptr;
raw_ptr<TemplateURLService> template_url_service_ = nullptr;
contextual_search::FakeVariationsClient fake_variations_client_;
raw_ptr<tabs::TabFeatures> tab_features_override_ = nullptr;
// Mock controller kept by Setup/AddTab to set expectations
raw_ptr<MockTabContextualizationController> mock_tab_controller_ = nullptr;
// Helper methods to access protected members
content::WebContents* web_contents() { return web_contents_; }
Profile* profile() { return browser()->GetProfile(); }
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory() {
return profile()
->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess();
}
TemplateURLService* template_url_service() { return template_url_service_; }
contextual_search::FakeVariationsClient* fake_variations_client() {
return &fake_variations_client_;
}
tabs::TabInterface* AddTab(const GURL& url) {
chrome::AddSelectedTabWithURL(browser(), url, ui::PAGE_TRANSITION_LINK);
content::WebContents* contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::TestNavigationObserver navigation_observer(contents);
navigation_observer.Wait();
tabs::TabInterface* tab =
tabs::TabLookupFromWebContents::FromWebContents(contents)->model();
tab->GetTabFeatures()->SetTabContextualizationControllerForTesting(nullptr);
auto mock_tab_controller =
std::make_unique<MockTabContextualizationController>(tab);
mock_tab_controller_ = mock_tab_controller.get();
tab->GetTabFeatures()->SetTabContextualizationControllerForTesting(
std::move(mock_tab_controller));
return tab;
}
};
class MockContextualTasksUI : public ContextualTasksUI {
public:
explicit MockContextualTasksUI(content::WebUI* web_ui)
: ContextualTasksUI(web_ui) {}
~MockContextualTasksUI() override = default;
contextual_search::ContextualSearchSessionHandle*
GetOrCreateContextualSessionHandle() override {
return session_handle_ptr_;
}
void SetSessionHandle(
contextual_search::ContextualSearchSessionHandle* handle) {
session_handle_ptr_ = handle;
}
MOCK_METHOD(void,
PostAimMessage,
(const lens::ClientToAimMessage& message),
(override));
MOCK_METHOD(content::WebContents*, GetWebUIWebContents, (), (override));
MOCK_METHOD(const std::optional<base::Uuid>&, GetTaskId, (), (override));
MOCK_METHOD(BrowserWindowInterface*, GetBrowser, (), (override));
MOCK_METHOD(bool, IsLensOverlayShowing, (), (const, override));
MOCK_METHOD(const GURL&, GetInnerFrameUrl, (), (const, override));
MOCK_METHOD(std::unique_ptr<contextual_search::InputStateModel>,
TakeInputStateModel,
(),
(override));
MOCK_METHOD(std::vector<int32_t>, GetRestoredTabIds, (), (override));
MOCK_METHOD(bool, IsActiveTabContextSuggestionShowing, (), (const, override));
MOCK_METHOD(bool, IsContextualTasksEligibleOnInit, (), (const, override));
MOCK_METHOD(contextual_tasks::ContextualTasksAutoSuggestionManager*,
GetAutoSuggestionManager,
(),
(override));
private:
raw_ptr<contextual_search::ContextualSearchSessionHandle>
session_handle_ptr_ = nullptr;
};
class TestContextualTasksComposeboxHandler
: public ContextualTasksComposeboxHandler {
public:
using ContextualTasksComposeboxHandler::ContextualTasksComposeboxHandler;
MOCK_METHOD(std::optional<base::UnguessableToken>,
GetLensOverlayToken,
(),
(override));
MOCK_METHOD(LensSearchController*,
GetLensSearchController,
(),
(const, override));
MOCK_METHOD(void,
OnContextUploadStatusChanged,
(const base::UnguessableToken& context_token,
lens::MimeType mime_type,
contextual_search::ContextUploadStatus context_upload_status,
const std::optional<contextual_search::ContextUploadErrorType>&
error_type),
(override));
protected:
contextual_tasks::ContextualTasksService* GetContextualTasksService()
override {
return mock_contextual_tasks_service_;
}
public:
void SetMockContextualTasksService(
contextual_tasks::ContextualTasksService* contextual_tasks_service) {
mock_contextual_tasks_service_ = contextual_tasks_service;
}
contextual_search::InputStateModel* TakeInputStateModelForTesting() {
return input_state_model_.get();
}
private:
raw_ptr<contextual_tasks::ContextualTasksService>
mock_contextual_tasks_service_ = nullptr;
};
class MockLensQueryFlowRouter : public lens::LensQueryFlowRouter {
public:
explicit MockLensQueryFlowRouter(LensSearchController* controller)
: lens::LensQueryFlowRouter(controller) {}
MOCK_METHOD(std::optional<base::UnguessableToken>,
overlay_tab_context_file_token,
(),
(const, override));
};
class MockLensSearchController : public LensSearchController {
public:
explicit MockLensSearchController(tabs::TabInterface* tab)
: LensSearchController(tab) {
mock_router_ =
std::make_unique<testing::NiceMock<MockLensQueryFlowRouter>>(this);
}
~MockLensSearchController() override = default;
MOCK_METHOD(void,
OpenLensOverlay,
(lens::LensOverlayInvocationSource invocation_source,
bool should_show_csb),
(override));
MOCK_METHOD(void,
CloseLensSync,
(lens::LensOverlayDismissalSource dismissal_source),
(override));
MOCK_METHOD(void,
CloseLensAsync,
(lens::LensOverlayDismissalSource dismissal_source),
(override));
MOCK_METHOD(void,
CloseLensAsync,
(lens::LensOverlayDismissalSource dismissal_source,
bool side_panel_already_closing),
(override));
lens::LensQueryFlowRouter* query_router() override {
return mock_router_.get();
}
MockLensQueryFlowRouter* mock_router() { return mock_router_.get(); }
private:
std::unique_ptr<MockLensQueryFlowRouter> mock_router_;
};
class ContextualTasksComposeboxHandlerTest
: public LocalContextualSearchboxHandlerTestHarness {
public:
ContextualTasksComposeboxHandlerTest()
: ContextualTasksComposeboxHandlerTest(
std::map<std::string, std::string>()) {}
explicit ContextualTasksComposeboxHandlerTest(
const std::map<std::string, std::string>& parameters) {
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{contextual_tasks::kContextualTasks, parameters},
{contextual_tasks::kContextualTasksForceEntryPointEligibility, {}}},
/*disabled_features=*/
// TODO(crbug.com/452061489): Fix tests that fail when the WebUI Omnibox
// is enabled and then remove these two Features.
{omnibox::internal::kWebUIOmniboxPopup,
omnibox::internal::kWebUIOmniboxAimPopup});
}
~ContextualTasksComposeboxHandlerTest() override = default;
void SimulateUploadStatusChanged(
const base::UnguessableToken& context_token,
lens::MimeType mime_type,
contextual_search::ContextUploadStatus context_upload_status,
const std::optional<contextual_search::ContextUploadErrorType>&
error_type = std::nullopt) {
for (auto& obs : upload_observers_) {
obs.OnContextUploadStatusChanged(context_token, mime_type,
context_upload_status, error_type);
}
}
void PostUploadStatusChanged(
const base::UnguessableToken& context_token,
lens::MimeType mime_type,
contextual_search::ContextUploadStatus context_upload_status,
const std::optional<contextual_search::ContextUploadErrorType>&
error_type = std::nullopt) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&ContextualTasksComposeboxHandlerTest::SimulateUploadStatusChanged,
base::Unretained(this), context_token, mime_type,
context_upload_status, error_type));
}
std::unique_ptr<contextual_search::InputStateModel>
CreateMockInputStateModel() {
omnibox::SearchboxConfig config;
auto model = std::make_unique<contextual_search::InputStateModel>(
*session_handle_, config, GURL(), /*is_off_the_record=*/false,
/*is_signed_in=*/false,
/*browser_identity_matches_aim_identity=*/false);
model->setActiveModel(omnibox::ModelMode::MODEL_MODE_GEMINI_PRO);
return model;
}
void SetUpMockUI() {
web_ui_.set_web_contents(web_contents());
webui::SetTabInterface(web_contents(), nullptr);
webui::SetBrowserWindowInterface(web_contents(), browser());
auto mock_controller = std::make_unique<testing::NiceMock<
contextual_search::MockContextualSearchContextController>>();
mock_controller_ = mock_controller.get();
controller_weak_factory_ = std::make_unique<base::WeakPtrFactory<
contextual_search::ContextualSearchContextController>>(
mock_controller_);
ON_CALL(*mock_controller_, AsWeakPtr()).WillByDefault([this]() {
return controller_weak_factory_->GetWeakPtr();
});
ON_CALL(*mock_controller_, AddObserver(testing::_))
.WillByDefault(
[this](contextual_search::ContextualSearchContextController::
ContextUploadStatusObserver* obs) {
if (!upload_observers_.HasObserver(obs)) {
upload_observers_.AddObserver(obs);
}
});
ON_CALL(*mock_controller_, RemoveObserver(testing::_))
.WillByDefault(
[this](contextual_search::ContextualSearchContextController::
ContextUploadStatusObserver* obs) {
upload_observers_.RemoveObserver(obs);
});
service_ = std::make_unique<contextual_search::ContextualSearchService>(
/*identity_manager=*/nullptr, url_loader_factory(),
template_url_service(), fake_variations_client(),
version_info::Channel::UNKNOWN, "en-US",
/*tab_validator=*/nullptr,
base::BindRepeating(
[](std::optional<size_t>,
base::OnceCallback<void(std::vector<std::string>)>) {}));
auto contextual_session_handle = service_->CreateSessionForTesting(
std::move(mock_controller),
std::make_unique<contextual_search::ContextualSearchMetricsRecorder>(
contextual_search::ContextualSearchSource::kContextualTasks));
// Check the search content sharing settings to notify the session handle
// that the client is properly checking the pref value.
contextual_session_handle->CheckSearchContentSharingSettings(
profile()->GetPrefs());
session_handle_ =
service_->GetSession(contextual_session_handle->session_id(),
/*invocation_source=*/std::nullopt);
session_handle_->CheckSearchContentSharingSettings(profile()->GetPrefs());
ContextualSearchWebContentsHelper::GetOrCreateForWebContents(web_contents())
->SetTaskSession(std::nullopt, std::move(contextual_session_handle),
/*input_state_model=*/nullptr);
mock_ui_ =
std::make_unique<testing::NiceMock<MockContextualTasksUI>>(&web_ui_);
mock_ui_->SetSessionHandle(session_handle_.get());
ON_CALL(*mock_ui_, GetWebUIWebContents())
.WillByDefault(testing::Return(web_contents()));
ON_CALL(*mock_ui_, GetTaskId())
.WillByDefault(testing::ReturnRefOfCopy(std::optional<base::Uuid>()));
ON_CALL(*mock_ui_, GetBrowser()).WillByDefault(testing::Return(browser()));
ON_CALL(*mock_ui_, GetInnerFrameUrl())
.WillByDefault(testing::ReturnRefOfCopy(GURL()));
ON_CALL(*mock_ui_, GetAutoSuggestionManager())
.WillByDefault(testing::Return(&auto_suggestion_manager_));
ON_CALL(*mock_ui_, IsActiveTabContextSuggestionShowing())
.WillByDefault([this]() {
return auto_suggestion_manager_.GetCurrentSuggestion() != nullptr;
});
ON_CALL(*mock_ui_, IsContextualTasksEligibleOnInit())
.WillByDefault(testing::Return(true));
// Create mock controller directly.
mock_contextual_tasks_service_owner_ = std::make_unique<
testing::NiceMock<contextual_tasks::MockContextualTasksService>>();
mock_contextual_tasks_service_ptr_ =
mock_contextual_tasks_service_owner_.get();
}
void SetUpHandler() {
handler_ = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
searchbox_page_receiver_.BindNewPipeAndPassRemote(),
base::BindRepeating(
&ContextualTasksUI::GetOrCreateContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::ClearContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::TakeInputStateModel,
base::Unretained(mock_ui_.get())));
ON_CALL(*handler_, GetLensSearchController())
.WillByDefault(testing::Return(mock_lens_controller_.get()));
handler_->SetMockContextualTasksService(mock_contextual_tasks_service_ptr_);
handler_->recontextualizer_ =
std::make_unique<contextual_tasks::QueryContextualizer>(
mock_contextual_tasks_service_ptr_,
handler_->desktop_delegate_.get());
// Default to calling the real implementation for
// OnContextUploadStatusChanged.
ON_CALL(*handler_, OnContextUploadStatusChanged(testing::_, testing::_,
testing::_, testing::_))
.WillByDefault(
[handler = handler_.get()](
const base::UnguessableToken& context_token,
lens::MimeType mime_type,
contextual_search::ContextUploadStatus context_upload_status,
const std::optional<contextual_search::ContextUploadErrorType>&
error_type) {
handler->ContextualTasksComposeboxHandler::
OnContextUploadStatusChanged(context_token, mime_type,
context_upload_status,
error_type);
});
ASSERT_TRUE(mock_lens_controller_);
}
void SetUpOnMainThread() override {
// Install override before AddTab is called in base SetUpOnMainThread.
lens_controller_override_ =
tabs::TabFeatures::GetUserDataFactoryForTesting().AddOverrideForTesting(
base::BindLambdaForTesting(
[this](tabs::TabInterface& tab)
-> std::unique_ptr<LensSearchController> {
auto mock = std::make_unique<
testing::NiceMock<MockLensSearchController>>(&tab);
this->mock_lens_controller_ = mock.get();
return mock;
}));
LocalContextualSearchboxHandlerTestHarness::SetUpOnMainThread();
SetUpMockUI();
SetUpHandler();
}
std::unique_ptr<contextual_tasks::MockContextualTasksService>
mock_contextual_tasks_service_owner_;
void TearDownOnMainThread() override {
// Manually verify and clear expectations to avoid issues during teardown
// when the tab is closed and CloseLensSync is called again with kTabClosed.
testing::Mock::VerifyAndClearExpectations(mock_lens_controller_);
// Reset handler first to destroy the omnibox client which observes the
// lens controller.
handler_.reset();
mock_controller_ = nullptr;
mock_contextual_tasks_service_ptr_ = nullptr;
mock_tab_controller_ = nullptr;
mock_lens_controller_ = nullptr;
mock_ui_->SetSessionHandle(nullptr);
session_handle_.reset();
service_.reset();
mock_ui_.reset();
LocalContextualSearchboxHandlerTestHarness::TearDownOnMainThread();
}
protected:
content::TestWebUI web_ui_;
std::unique_ptr<testing::NiceMock<MockContextualTasksUI>> mock_ui_;
contextual_tasks::ContextualTasksAutoSuggestionManager
auto_suggestion_manager_;
std::unique_ptr<TestContextualTasksComposeboxHandler> handler_;
// For session management.
std::unique_ptr<contextual_search::ContextualSearchService> service_;
std::unique_ptr<contextual_search::ContextualSearchSessionHandle>
session_handle_;
raw_ptr<contextual_search::MockContextualSearchContextController>
mock_controller_;
raw_ptr<contextual_tasks::MockContextualTasksService>
mock_contextual_tasks_service_ptr_ = nullptr;
raw_ptr<MockLensSearchController> mock_lens_controller_ = nullptr;
testing::NiceMock<MockSearchboxPage> mock_searchbox_page_;
mojo::Receiver<searchbox::mojom::Page> searchbox_page_receiver_{
&mock_searchbox_page_};
base::ObserverList<contextual_search::ContextualSearchContextController::
ContextUploadStatusObserver>
upload_observers_;
std::unique_ptr<base::WeakPtrFactory<
contextual_search::ContextualSearchContextController>>
controller_weak_factory_;
base::test::ScopedFeatureList feature_list_;
ui::UserDataFactory::ScopedOverride lens_controller_override_;
};
class ContextualTasksComposeboxHandlerTestWithAutoSuggestionDisabled
: public ContextualTasksComposeboxHandlerTest {
public:
ContextualTasksComposeboxHandlerTestWithAutoSuggestionDisabled()
: ContextualTasksComposeboxHandlerTest(
{{"ContextualTasksTabAutoSuggestionChipEnabled", "false"}}) {}
~ContextualTasksComposeboxHandlerTestWithAutoSuggestionDisabled() override =
default;
};
class ContextualTasksComposeboxHandlerTestWithContextManagementEnabled
: public ContextualTasksComposeboxHandlerTest {
public:
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled() {
feature_list_context_management_.InitWithFeatures(
{omnibox::kContextManagementInComposebox,
omnibox::kContextManagementInOmnibox},
{});
}
~ContextualTasksComposeboxHandlerTestWithContextManagementEnabled() override =
default;
void SetUpOnMainThread() override {
lens_controller_override_ =
tabs::TabFeatures::GetUserDataFactoryForTesting().AddOverrideForTesting(
base::BindLambdaForTesting(
[this](tabs::TabInterface& tab)
-> std::unique_ptr<LensSearchController> {
auto mock = std::make_unique<
testing::NiceMock<MockLensSearchController>>(&tab);
this->mock_lens_controller_ = mock.get();
return mock;
}));
LocalContextualSearchboxHandlerTestHarness::SetUpOnMainThread();
SetUpMockUI();
}
private:
base::test::ScopedFeatureList feature_list_context_management_;
};
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest, SubmitQuery) {
base::UserActionTester user_action_tester;
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL in SubmitQuery!";
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
EXPECT_CALL(
*mock_lens_controller_,
CloseLensSync(
lens::LensOverlayDismissalSource::kContextualTasksQuerySubmitted));
handler_->SubmitQuery("test query", 0, false, false, false, false,
/*is_voice_search=*/false);
EXPECT_EQ(session_handle_->previous_turns().back().query, "test query");
EXPECT_EQ(1, user_action_tester.GetActionCount(
"ContextualTasks.Composebox.UserAction.QuerySubmitted"));
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CloseLensOverlayFromWebUI) {
EXPECT_CALL(*mock_lens_controller_,
CloseLensAsync(lens::LensOverlayDismissalSource::
kContextualTasksImageUploadsDisabled));
handler_->CloseLensOverlayFromWebUI(
composebox::mojom::LensOverlayDismissalSource::
kContextualTasksImageUploadsDisabled);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage) {
std::string kQuery = "direct query";
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(testing::ReturnRefOfCopy(std::optional<base::Uuid>()));
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&kQuery](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_PipesAdditionalCgiParams) {
std::string kQuery = "direct query";
std::map<std::string, std::string> kCgiParams = {
{"gs_lcrp", "EgZjaHJvbWWwAgE"}, {"source", "chrome.crn.rb"}};
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(testing::ReturnRefOfCopy(std::optional<base::Uuid>()));
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&kQuery, &kCgiParams](
std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
EXPECT_EQ(info->additional_cgi_params, kCgiParams);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false,
kCgiParams);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_UpdatesMetricsRecorderSource) {
// Set the initial source of the session metrics recorder to kLens.
session_handle_->GetMetricsRecorder()->UpdateContextualSearchSource(
contextual_search::ContextualSearchSource::kLens);
EXPECT_EQ(session_handle_->GetMetricsRecorder()->source(),
contextual_search::ContextualSearchSource::kLens);
std::string kQuery = "direct query";
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(testing::ReturnRefOfCopy(std::optional<base::Uuid>()));
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
// The source of the metrics recorder should now be updated to
// kContextualTasks.
EXPECT_EQ(session_handle_->GetMetricsRecorder()->source(),
contextual_search::ContextualSearchSource::kContextualTasks);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_WithOverlayToken) {
std::string kQuery = "direct query";
base::UnguessableToken overlay_token = base::UnguessableToken::Create();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(testing::ReturnRefOfCopy(std::optional<base::Uuid>()));
EXPECT_CALL(*handler_, GetLensOverlayToken())
.WillOnce(testing::Return(overlay_token));
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&kQuery, &overlay_token](
std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
EXPECT_THAT(info->file_tokens, testing::Contains(overlay_token));
EXPECT_TRUE(info->force_include_latest_interaction_request_data);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_NoOverlayToken) {
std::string kQuery = "direct query";
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(testing::ReturnRefOfCopy(std::optional<base::Uuid>()));
EXPECT_CALL(*handler_, GetLensOverlayToken())
.WillOnce(testing::Return(std::nullopt));
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&kQuery](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
EXPECT_FALSE(info->force_include_latest_interaction_request_data);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_RecontextualizeExpiredTab) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "recontextualize query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with expired tab.
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("about:blank");
// The default title for about:blank is "about:blank".
std::string kTitle = "about:blank";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kWebpage);
resource.title = kTitle;
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup FileInfo with expired status.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadExpired;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// Expect GetPageContext call.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([session_id](
MockTabContextualizationController::GetPageContextCallback
callback) {
// Return some dummy content.
auto data = std::make_unique<lens::ContextualInputData>();
data->tab_session_id = session_id;
data->page_url = GURL("about:blank");
data->page_title = "about:blank";
data->context_id = 12345;
data->is_page_context_eligible = true;
std::move(callback).Run(std::move(data));
});
// Expect StartFileUploadFlow call.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillOnce(
[this](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
EXPECT_TRUE(data->is_implicit_upload);
PostUploadStatusChanged(
file_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_RecontextualizeContentChanged) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "recontextualize query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with uploaded tab (not expired).
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("about:blank");
std::string kTitle = "about:blank";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kWebpage);
resource.title = kTitle;
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup context with uploaded status and some previous content.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadSuccessful;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// Expect GetPageContext call with NEW content.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([session_id](
MockTabContextualizationController::GetPageContextCallback
callback) {
auto data = std::make_unique<lens::ContextualInputData>();
std::string new_content = "new content";
auto new_content_span = base::as_bytes(base::span(new_content));
std::vector<uint8_t> new_bytes(new_content_span.begin(),
new_content_span.end());
lens::ContextualInput new_input(std::move(new_bytes),
lens::MimeType::kPlainText);
data->context_input.emplace().push_back(std::move(new_input));
data->tab_session_id = session_id;
data->page_url = GURL("about:blank");
data->page_title = "about:blank";
data->context_id = 12345;
data->is_page_context_eligible = true;
std::move(callback).Run(std::move(data));
});
// Expect StartFileUploadFlow call because content changed.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillOnce(
[this](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
EXPECT_TRUE(data->is_implicit_upload);
PostUploadStatusChanged(
file_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_NoRecontextualizationIfUnchanged) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "valid tab query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with uploaded tab
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
contextual_tasks::UrlResource resource(
GURL("about:blank"), contextual_tasks::ResourceType::kWebpage);
resource.title = "about:blank";
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup FileInfo with uploaded status and SAME content.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadSuccessful;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
// Populate input_data for comparison
auto input_data = std::make_unique<lens::ContextualInputData>();
std::string content = "same content";
auto content_span = base::as_bytes(base::span(content));
std::vector<uint8_t> bytes(content_span.begin(), content_span.end());
lens::ContextualInput input(std::move(bytes), lens::MimeType::kPlainText);
input_data->context_input.emplace().push_back(std::move(input));
input_data->primary_content_type = lens::MimeType::kPlainText;
file_info.input_data = std::move(input_data);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// Expect GetPageContext call with SAME content.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([session_id](
MockTabContextualizationController::GetPageContextCallback
callback) {
auto data = std::make_unique<lens::ContextualInputData>();
// Reconstruct same input.
std::string content = "same content";
auto content_span = base::as_bytes(base::span(content));
std::vector<uint8_t> bytes(content_span.begin(), content_span.end());
lens::ContextualInput new_input(std::move(bytes),
lens::MimeType::kPlainText);
data->context_input.emplace().push_back(std::move(new_input));
data->primary_content_type = lens::MimeType::kPlainText;
// Set session ID to match
data->tab_session_id = session_id;
data->page_url = GURL("about:blank");
data->page_title = "about:blank";
data->context_id = 12345;
std::move(callback).Run(std::move(data));
});
// Expect StartFileUploadFlow call to NOT be called.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_ActiveTabNotInContext) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "query with unrelated active tab";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with NO tabs (or just not the active one).
contextual_tasks::ContextualTask task(task_id);
// Add a resource that is NOT the active tab.
contextual_tasks::UrlResource resource(
GURL("http://example.com"), contextual_tasks::ResourceType::kWebpage);
resource.tab_id = SessionID::NewUnique(); // Random ID
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Expect StartFileUploadFlow to NOT be called.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
// Expect CreateClientToAimRequest IS called (query submission continues).
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_ActiveTabUrlMismatch) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "query with url mismatch";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with matching SessionID but mismatching URL.
// Active tab is at about:blank. Resource is at http://example.com.
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("http://example.com");
std::u16string kTitle = u"Example Title";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kWebpage);
resource.title = base::UTF16ToUTF8(kTitle);
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Expect StartFileUploadFlow to NOT be called.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
// Expect CreateClientToAimRequest IS called.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
// crbug.com/488112121: This test covers the temporary behavior of disabling
// tools when the aegc=1 URL parameter is present. Remove this test when the
// temporary workaround in ContextualTasksComposeboxHandler is removed.
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
AegcParameterDisablesTools) {
omnibox::SearchboxConfig config;
config.add_input_type_configs()->set_input_type(
omnibox::InputType::INPUT_TYPE_LENS_IMAGE);
config.add_input_type_configs()->set_input_type(
omnibox::InputType::INPUT_TYPE_LENS_FILE);
config.add_tool_configs()->set_tool(omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH);
auto session_handle =
std::make_unique<contextual_search::MockContextualSearchSessionHandle>();
auto input_state_model = std::make_unique<contextual_search::InputStateModel>(
*session_handle, config, GURL(), /*is_off_the_record=*/false,
/*is_signed_in=*/false,
/*browser_identity_matches_aim_identity=*/false);
EXPECT_CALL(*mock_ui_, TakeInputStateModel())
.WillOnce(testing::Return(testing::ByMove(std::move(input_state_model))));
GURL aegc_url("https://gemini.google.com/app?aegc=1");
EXPECT_CALL(*mock_ui_, GetInnerFrameUrl())
.WillRepeatedly(testing::ReturnRef(aegc_url));
// Re-initialize the model to pick up the URL change.
handler_->OnTaskChanged();
auto* model = handler_->TakeInputStateModelForTesting();
ASSERT_TRUE(model);
const auto& state = model->get_state_for_testing();
EXPECT_THAT(state.disabled_tools,
testing::Contains(omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH));
EXPECT_THAT(
state.disabled_input_types,
testing::UnorderedElementsAre(omnibox::InputType::INPUT_TYPE_LENS_FILE));
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_RecontextualizeScreenshotChanged_SkBitmap) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "recontextualize query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with uploaded tab.
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("about:blank");
std::string kTitle = "about:blank";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kWebpage);
resource.title = kTitle;
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup FileInfo with uploaded status and OLD bitmap.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadSuccessful;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
auto input_data = std::make_unique<lens::ContextualInputData>();
SkBitmap old_bitmap;
old_bitmap.allocN32Pixels(10, 10);
old_bitmap.eraseColor(SK_ColorRED);
input_data->viewport_screenshot = old_bitmap;
file_info.input_data = std::move(input_data);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// Expect GetPageContext call with NEW bitmap (different color).
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([session_id](
MockTabContextualizationController::GetPageContextCallback
callback) {
auto data = std::make_unique<lens::ContextualInputData>();
SkBitmap new_bitmap;
new_bitmap.allocN32Pixels(10, 10);
new_bitmap.eraseColor(SK_ColorBLUE);
data->viewport_screenshot = new_bitmap;
data->tab_session_id = session_id;
data->page_url = GURL("about:blank");
data->page_title = "about:blank";
data->context_id = 12345;
data->is_page_context_eligible = true;
std::move(callback).Run(std::move(data));
});
// Expect StartFileUploadFlow call because bitmap changed.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillOnce(
[this](const base::UnguessableToken& context_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
EXPECT_TRUE(data->is_implicit_upload);
PostUploadStatusChanged(
context_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_NoRecontextualizationIfScreenshotUnchanged_SkBitmap) {
// Test case for no recontextualization.
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "valid tab query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with uploaded tab
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
contextual_tasks::UrlResource resource(
GURL("about:blank"), contextual_tasks::ResourceType::kWebpage);
resource.title = "about:blank";
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup FileInfo with uploaded status and SAME bitmap.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadSuccessful;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
auto input_data = std::make_unique<lens::ContextualInputData>();
SkBitmap old_bitmap;
old_bitmap.allocN32Pixels(10, 10);
old_bitmap.eraseColor(SK_ColorRED);
input_data->viewport_screenshot = old_bitmap;
file_info.input_data = std::move(input_data);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// Expect GetPageContext call with SAME bitmap.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([session_id](
MockTabContextualizationController::GetPageContextCallback
callback) {
auto data = std::make_unique<lens::ContextualInputData>();
SkBitmap new_bitmap;
new_bitmap.allocN32Pixels(10, 10);
new_bitmap.eraseColor(SK_ColorRED);
data->viewport_screenshot = new_bitmap;
data->tab_session_id = session_id;
data->page_url = GURL("about:blank");
data->page_title = "about:blank";
data->context_id = 12345;
std::move(callback).Run(std::move(data));
});
// Expect StartFileUploadFlow call to NOT be called.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
OnAutocompleteAccept) {
base::UserActionTester user_action_tester;
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
AutocompleteMatch match;
handler_->GetOmniboxClientForTesting()->OnAutocompleteAccept(
GURL("https://www.google.com/search?q=test query"), nullptr,
WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_TYPED,
AutocompleteMatchType::SEARCH_SUGGEST, base::TimeTicks::Now(), false,
false, u"test query", match, match);
EXPECT_EQ(1, user_action_tester.GetActionCount(
"ContextualTasks.Composebox.UserAction.QuerySubmitted"));
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
HandleLensButtonClick) {
base::UserActionTester user_action_tester;
base::HistogramTester histogram_tester;
EXPECT_CALL(
*mock_lens_controller_,
OpenLensOverlay(
lens::LensOverlayInvocationSource::kContextualTasksComposebox, true));
handler_->HandleLensButtonClick();
EXPECT_EQ(1, user_action_tester.GetActionCount(
"ContextualTasks.Composebox.UserAction.LensButtonClicked"));
histogram_tester.ExpectUniqueSample(
"ContextualTasks.Composebox.UserAction.LensButtonClicked", true, 1);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
OnAutocompleteAccept_ExtractsQuery) {
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, "extracted query");
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
AutocompleteMatch match;
handler_->GetOmniboxClientForTesting()->OnAutocompleteAccept(
GURL("https://www.google.com/search?q=extracted%20query"), nullptr,
WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_TYPED,
AutocompleteMatchType::SEARCH_SUGGEST, base::TimeTicks::Now(), false,
false, u"extracted query", match, match);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
OnAutocompleteAccept_NoQueryParam) {
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, "");
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
AutocompleteMatch match;
handler_->GetOmniboxClientForTesting()->OnAutocompleteAccept(
GURL("https://www.google.com/search?other=param"), nullptr,
WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_TYPED,
AutocompleteMatchType::SEARCH_SUGGEST, base::TimeTicks::Now(), false,
false, u"other param", match, match);
}
struct ToolModeTestParam {
omnibox::ToolMode tool_mode;
};
class ContextualTasksComposeboxHandlerToolModeTest
: public ContextualTasksComposeboxHandlerTest,
public ::testing::WithParamInterface<ToolModeTestParam> {};
IN_PROC_BROWSER_TEST_P(ContextualTasksComposeboxHandlerToolModeTest,
SetsToolModeFlags) {
const auto& param = GetParam();
// Setting active tool should send `exit_tool_info` to AIM webpage (on client
// side).
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_TRUE(info->exit_tool_info.has_value());
EXPECT_EQ(info->exit_tool_info->tool_mode,
omnibox::ToolMode::TOOL_MODE_UNSPECIFIED);
EXPECT_EQ(info->exit_tool_info->new_tool_mode, param.tool_mode);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
// No server-added tool means `exitTool` is sent.
handler_->SetActiveToolMode(param.tool_mode, /*is_set_by_server=*/false);
handler_->RecordToolSelectionAction(param.tool_mode);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->active_tool, param.tool_mode);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
handler_->CreateAndSendQueryMessage("test query", /*is_voice_search=*/false);
}
INSTANTIATE_TEST_SUITE_P(
All,
ContextualTasksComposeboxHandlerToolModeTest,
::testing::Values(
ToolModeTestParam{omnibox::ToolMode::TOOL_MODE_UNSPECIFIED},
ToolModeTestParam{omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH},
ToolModeTestParam{omnibox::ToolMode::TOOL_MODE_IMAGE_GEN},
ToolModeTestParam{omnibox::ToolMode::TOOL_MODE_IMAGE_GEN_UPLOAD}));
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SetActiveToolMode_SendsExitToolMessage) {
// Setting active tool should send `exit_tool_info` to AIM webpage (on client
// side).
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_TRUE(info->exit_tool_info.has_value());
EXPECT_EQ(info->exit_tool_info->tool_mode,
omnibox::ToolMode::TOOL_MODE_UNSPECIFIED);
EXPECT_EQ(info->exit_tool_info->new_tool_mode,
omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
handler_->SetActiveToolMode(omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH,
/*is_set_by_server=*/false);
// Clearing active tool should also send `exit_tool_info`.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_TRUE(info->exit_tool_info.has_value());
EXPECT_EQ(info->exit_tool_info->tool_mode,
omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH);
EXPECT_EQ(info->exit_tool_info->new_tool_mode,
omnibox::ToolMode::TOOL_MODE_UNSPECIFIED);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
handler_->SetActiveToolMode(omnibox::ToolMode::TOOL_MODE_UNSPECIFIED,
/*is_set_by_server=*/false);
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
SetActiveToolMode_SetByServer_DoesNotSendExitToolMessage) {
// When `is_set_by_server` is true, `exit_tool_info` message should NOT be
// sent.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_)).Times(0);
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->SetActiveToolMode(omnibox::ToolMode::TOOL_MODE_DEEP_SEARCH,
/*is_set_by_server=*/true);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
AddTabContext_Delayed) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "delayed tab query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
// 1. Add delayed tab context.
int32_t tab_id = 100;
std::optional<base::UnguessableToken> token_opt;
base::MockCallback<ContextualSearchboxHandler::AddTabContextCallback>
callback;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
});
handler_->AddTabContext(tab_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
std::vector<int32_t> selected_tab_ids = handler_->GetSelectedTabIds();
EXPECT_THAT(selected_tab_ids, testing::Contains(tab_id));
ASSERT_TRUE(token_opt.has_value());
base::UnguessableToken token = token_opt.value();
ASSERT_FALSE(token.is_empty());
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
// 2. Verify tab is added to GetTabsToUpdate (via CreateAndSendQueryMessage).
// We need to mock the tab handle resolution. Since we can't easily mock
// TabHandle::Get() for arbitrary IDs in this test harness without more setup,
// we will use the active tab's ID which IS set up.
tabs::TabInterface* active_tab =
TabListInterface::From(browser())->GetActiveTab();
int32_t active_tab_id = active_tab->GetHandle().raw_value();
// Reset and try again with active tab ID.
std::optional<base::UnguessableToken> active_token_opt;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
active_token_opt = result.value();
});
handler_->AddTabContext(active_tab_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
ASSERT_TRUE(active_token_opt.has_value());
base::UnguessableToken active_token = active_token_opt.value();
ASSERT_FALSE(active_token.is_empty());
// Expect GetPageContext call for the active tab.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([](MockTabContextualizationController::GetPageContextCallback
callback) {
auto data = std::make_unique<lens::ContextualInputData>();
data->is_page_context_eligible = true;
std::move(callback).Run(std::move(data));
});
// Expect StartFileUploadFlow call.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillOnce(
[this](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
// The delay-upload tab is an implicit upload because it was
// auto-suggested.
EXPECT_TRUE(data->is_implicit_upload);
PostUploadStatusChanged(
file_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
DeleteContext_Delayed) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "delete context query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// 1. Add delayed tab context.
tabs::TabInterface* active_tab =
TabListInterface::From(browser())->GetActiveTab();
int32_t active_tab_id = active_tab->GetHandle().raw_value();
std::optional<base::UnguessableToken> token_opt;
base::MockCallback<ContextualSearchboxHandler::AddTabContextCallback>
callback;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
});
handler_->AddTabContext(active_tab_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
ASSERT_TRUE(token_opt.has_value());
base::UnguessableToken token = token_opt.value();
ASSERT_FALSE(token.is_empty());
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
// 2. Delete the context.
handler_->DeleteContext(token, /*from_automatic_chip=*/true);
// No stashed message since we have not submitted a query yet,
// nor uploaded the delayed tab yet.
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
// 3. Verify StartFileUploadFlow is NOT called when submitting.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled,
RestoreTabIds) {
std::vector<int32_t> restored_tab_ids = {1, 2};
EXPECT_CALL(*mock_ui_, GetRestoredTabIds())
.WillOnce(testing::Return(restored_tab_ids));
EXPECT_CALL(mock_searchbox_page_, SetRestoredTabIds(restored_tab_ids))
.Times(1);
SetUpHandler();
searchbox_page_receiver_.FlushForTesting();
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
GetSelectedTabIds) {
int32_t tab_id = 15;
base::MockCallback<ContextualSearchboxHandler::AddTabContextCallback>
callback;
handler_->AddTabContext(tab_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
std::vector<int32_t> selected_tab_ids = handler_->GetSelectedTabIds();
EXPECT_THAT(selected_tab_ids, testing::Contains(tab_id));
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_WaitsForUpload) {
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found.";
int32_t tab_handle_id = active_tab->GetHandle().raw_value();
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
base::Uuid task_id = base::Uuid::GenerateRandomV4();
contextual_tasks::ContextualTask task(task_id);
// Set mock taskID for when submit query/upload file.
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Mock getting tab's content by mocking the 2 functions
// that start tab uploads until barrier closure.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillRepeatedly([session_id](auto callback) {
auto data = std::make_unique<lens::ContextualInputData>();
data->context_id = 123;
data->tab_session_id = session_id;
data->is_page_context_eligible = true;
data->page_url = GURL("https://example.com");
std::move(callback).Run(std::move(data));
});
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid&,
const std::set<contextual_tasks::ContextualTaskContextSource>&,
std::unique_ptr<contextual_tasks::ContextDecorationParams>,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Expect client request is formulated.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Run add tab context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
callback;
std::optional<base::UnguessableToken> token_opt;
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
run_loop.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
run_loop.Run();
ASSERT_TRUE(token_opt.has_value()) << "AddTabContext failed.";
base::UnguessableToken token = *token_opt;
// Mock file upload status:
contextual_search::FileInfo uploading_info{};
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
uploading_info.mime_type = lens::MimeType::kPdf;
uploading_info.tab_session_id =
sessions::SessionTabHelper::IdForTab(active_tab->GetContents());
uploading_info.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(token))
.WillRepeatedly(testing::Return(&uploading_info));
// Do not submit request to server yet.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->SubmitQuery("Summarize the tab", 0, false, false, false, false,
/*is_voice_search=*/false);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
// Now, once file is successfully uploaded, should send request to server.
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kUploadSuccessful;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
SimulateUploadStatusChanged(
token, lens::MimeType::kPdf,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_ImageReplacedThenOtherTerminalStates) {
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found.";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
contextual_tasks::ContextualTask task(task_id);
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
auto file_info = searchbox::mojom::SelectedFileInfo::New();
file_info->file_name = "test.pdf";
file_info->mime_type = "application/pdf";
file_info->is_deletable = true;
std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
mojo_base::BigBuffer file_bytes(data);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Only execute part of context upload compared to past tests
// to verify early return's in upload callback workflow.
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillRepeatedly(
[&context](
const base::Uuid&,
const std::set<contextual_tasks::ContextualTaskContextSource>&,
std::unique_ptr<contextual_tasks::ContextDecorationParams>,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Run add file context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddFileContextCallback>
callback;
std::optional<base::UnguessableToken> current_token;
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
current_token = result.value();
run_loop.Quit();
});
handler_->AddFileContext(std::move(file_info), std::move(file_bytes),
callback.Get());
run_loop.Run();
ASSERT_TRUE(current_token.has_value()) << "AddFileContext failed.";
SimulateUploadStatusChanged(
*current_token, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kProcessing, std::nullopt);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
SimulateUploadStatusChanged(
*current_token, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kUploadReplaced, std::nullopt);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
auto file_info_2 = searchbox::mojom::SelectedFileInfo::New();
file_info_2->file_name = "test2.pdf";
file_info_2->mime_type = "application/pdf";
file_info_2->is_deletable = true;
std::vector<uint8_t> data_2 = {0xDE, 0xAD, 0xBE, 0xEF};
mojo_base::BigBuffer file_bytes_2(data_2);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Run add file context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddFileContextCallback>
callback_2;
std::optional<base::UnguessableToken> current_token_2;
base::RunLoop run_loop_2;
EXPECT_CALL(callback_2, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
current_token_2 = result.value();
run_loop_2.Quit();
});
handler_->AddFileContext(std::move(file_info_2), std::move(file_bytes_2),
callback_2.Get());
run_loop_2.Run();
ASSERT_TRUE(current_token_2.has_value()) << "AddFileContext failed.";
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
SimulateUploadStatusChanged(
*current_token_2, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kProcessing, std::nullopt);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
SimulateUploadStatusChanged(
*current_token_2, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kNotUploaded, std::nullopt);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
SimulateUploadStatusChanged(
*current_token_2, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kUploadStarted, std::nullopt);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
SimulateUploadStatusChanged(
*current_token_2, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kProcessingSuggestSignalsReady,
std::nullopt);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->SubmitQuery("What is this?", 0, false, false, false, false,
/*is_voice_search=*/false);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
testing::Mock::VerifyAndClearExpectations(mock_ui_.get());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
SimulateUploadStatusChanged(
*current_token_2, lens::MimeType::kImage,
contextual_search::ContextUploadStatus::kUploadExpired, std::nullopt);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_ThenDeleteToTriggerFullSubmit) {
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found.";
int32_t tab_handle_id = active_tab->GetHandle().raw_value();
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
base::Uuid task_id = base::Uuid::GenerateRandomV4();
contextual_tasks::ContextualTask task(task_id);
// Set mock taskID for when submit query/upload file.
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Mock getting tab's content by mocking the 2 functions
// that start tab uploads until barrier closure.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillRepeatedly([session_id](auto callback) {
auto data = std::make_unique<lens::ContextualInputData>();
data->context_id = 123;
data->tab_session_id = session_id;
data->is_page_context_eligible = true;
data->page_url = GURL("https://example.com");
std::move(callback).Run(std::move(data));
});
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid&,
const std::set<contextual_tasks::ContextualTaskContextSource>&,
std::unique_ptr<contextual_tasks::ContextDecorationParams>,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Expect client request is formulated.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Run add tab context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
callback;
std::optional<base::UnguessableToken> token_opt;
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
run_loop.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
run_loop.Run();
ASSERT_TRUE(token_opt.has_value()) << "AddTabContext failed.";
base::UnguessableToken token = *token_opt;
// Mock file upload status:
contextual_search::FileInfo uploading_info{};
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
uploading_info.mime_type = lens::MimeType::kPdf;
uploading_info.tab_session_id =
sessions::SessionTabHelper::IdForTab(active_tab->GetContents());
uploading_info.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(token))
.WillRepeatedly(testing::Return(&uploading_info));
// Do not submit request to server yet.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
// Should stash message instead of submit.
handler_->SubmitQuery("Summarize the tab", 0, false, false, false, false,
/*is_voice_search=*/false);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 1);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
// Deleting last file uploading should trigger full submit.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
handler_->DeleteContext(token, /*from_automatic_chip=*/true);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_AfterDeleteLastUploadingFile) {
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found.";
int32_t tab_handle_id = active_tab->GetHandle().raw_value();
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
base::Uuid task_id = base::Uuid::GenerateRandomV4();
contextual_tasks::ContextualTask task(task_id);
// Set mock taskID for when submit query/upload file.
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Mock getting tab's content by mocking the 2 functions
// that start tab uploads until barrier closure.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillRepeatedly([session_id](auto callback) {
auto data = std::make_unique<lens::ContextualInputData>();
data->context_id = 123;
data->tab_session_id = session_id;
data->is_page_context_eligible = true;
data->page_url = GURL("https://example.com");
std::move(callback).Run(std::move(data));
});
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid&,
const std::set<contextual_tasks::ContextualTaskContextSource>&,
std::unique_ptr<contextual_tasks::ContextDecorationParams>,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Expect client request is formulated.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Run add tab context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
callback;
std::optional<base::UnguessableToken> token_opt;
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
run_loop.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
run_loop.Run();
ASSERT_TRUE(token_opt.has_value()) << "AddTabContext failed.";
base::UnguessableToken token = *token_opt;
// Mock file upload status:
contextual_search::FileInfo uploading_info{};
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
uploading_info.mime_type = lens::MimeType::kPdf;
uploading_info.tab_session_id =
sessions::SessionTabHelper::IdForTab(active_tab->GetContents());
uploading_info.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(token))
.WillRepeatedly(testing::Return(&uploading_info));
// Do not submit request to server yet.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->SubmitQuery("Summarize the tab", 0, false, false, false, false,
/*is_voice_search=*/false);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
// Now, once file is deleted, should send request to server.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
handler_->DeleteContext(token, /*from_automatic_chip=*/true);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_WaitsForDelayedUpload) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is null.";
std::string kQuery = "recontextualize query";
// Setup context with uploaded tab (not expired).
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("about:blank");
std::string kTitle = "about:blank";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kUnknown);
resource.title = kTitle;
resource.tab_id = session_id;
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found!.";
int32_t tab_handle_id = active_tab->GetHandle().raw_value();
base::Uuid task_id = base::Uuid::GenerateRandomV4();
contextual_tasks::ContextualTask task(task_id);
task.AddUrlResource(resource);
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
// Execute full context upload compared to past tests
// to verify full upload callback workflow.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillRepeatedly([session_id](auto callback) {
auto data = std::make_unique<lens::ContextualInputData>();
data->context_id = 123;
data->tab_session_id = session_id;
data->is_page_context_eligible = true;
data->page_url = GURL("https://example.com");
std::move(callback).Run(std::move(data));
});
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
EXPECT_CALL(
*mock_controller_,
StartFileUploadFlow(
testing::_, testing::A<std::unique_ptr<lens::ContextualInputData>>(),
testing::_))
.WillOnce(
[this](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
PostUploadStatusChanged(
file_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Run add tab context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
callback;
std::optional<base::UnguessableToken> token_opt;
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
run_loop.Quit();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
run_loop.Run();
ASSERT_TRUE(token_opt.has_value())
<< "AddTabContext failed. URL setup might be wrong.";
contextual_search::FileInfo uploading_info{};
uploading_info.mime_type = lens::MimeType::kPdf;
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
uploading_info.tab_session_id =
sessions::SessionTabHelper::IdForTab(active_tab->GetContents());
uploading_info.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(testing::_))
.WillRepeatedly(testing::Return(&uploading_info));
// Should submit when SubmitQuery run + delayed tabs finish uploading.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
ASSERT_TRUE(handler_->IsAnyContextUploading());
// No pending query yet since have not submitted yet.
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
handler_->SubmitQuery("What is this?", 0, false, false, false, false,
/*is_voice_search=*/false);
base::RunLoop().RunUntilIdle();
// Now the delayed tabs should have uploaded.
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_Immediately) {
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found.";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
contextual_tasks::ContextualTask task(task_id);
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
auto file_info = searchbox::mojom::SelectedFileInfo::New();
file_info->file_name = "test.pdf";
file_info->mime_type = "application/pdf";
file_info->is_deletable = true;
std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
mojo_base::BigBuffer file_bytes(data);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Only execute part of context upload compared to past tests
// to verify early return's in upload callback workflow.
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid&,
const std::set<contextual_tasks::ContextualTaskContextSource>&,
std::unique_ptr<contextual_tasks::ContextDecorationParams>,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Run add file context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddFileContextCallback>
callback;
std::optional<base::UnguessableToken> current_token;
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
current_token = result.value();
run_loop.Quit();
});
handler_->AddFileContext(std::move(file_info), std::move(file_bytes),
callback.Get());
run_loop.Run();
ASSERT_TRUE(current_token.has_value()) << "AddFileContext failed.";
// File is not finished uploading.
contextual_search::FileInfo uploading_info{};
uploading_info.mime_type = lens::MimeType::kPdf;
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
uploading_info.tab_session_id =
sessions::SessionTabHelper::IdForTab(active_tab->GetContents());
uploading_info.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(*current_token))
.WillRepeatedly(testing::Return(&uploading_info));
handler_->SubmitQuery("What is this?", 0, false, false, false, false,
/*is_voice_search=*/false);
ASSERT_TRUE(handler_->IsAnyContextUploading());
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
// File is finished uploading.
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kUploadSuccessful;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
SimulateUploadStatusChanged(
*current_token, lens::MimeType::kPdf,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_WaitsForFilesAndDelayedTabs) {
// Set up tabs and functions that return them.
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
ASSERT_NE(active_tab, nullptr) << "No active tab found.";
int32_t tab_handle_id = active_tab->GetHandle().raw_value();
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
contextual_tasks::ContextualTask task(task_id);
GURL kUrl("about:blank");
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kUnknown);
resource.title = "about:blank";
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
// Execute full context upload compared to past tests
// to verify full upload callback workflow.
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillOnce([&context](auto, auto, auto, auto callback) {
std::move(callback).Run(std::move(context));
});
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillRepeatedly([session_id](auto callback) {
auto data = std::make_unique<lens::ContextualInputData>();
data->context_id = 123;
data->tab_session_id = session_id;
data->is_page_context_eligible = true;
std::move(callback).Run(std::move(data));
});
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillRepeatedly(
[](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
EXPECT_FALSE(data->is_implicit_upload);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
// Run add file context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddFileContextCallback>
file_cb;
std::optional<base::UnguessableToken> file_token_opt;
auto file_info = searchbox::mojom::SelectedFileInfo::New();
file_info->file_name = "test_file.pdf";
file_info->mime_type = "application/pdf";
std::vector<uint8_t> data = {0x1, 0x2};
base::RunLoop run_loop;
EXPECT_CALL(file_cb, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
file_token_opt = result.value();
run_loop.Quit();
});
handler_->AddFileContext(std::move(file_info), mojo_base::BigBuffer(data),
file_cb.Get());
run_loop.Run();
// Run add tab context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
normal_tab_cb;
std::optional<base::UnguessableToken> normal_tab_token_opt;
base::RunLoop run_loop_2;
EXPECT_CALL(normal_tab_cb, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
normal_tab_token_opt = result.value();
run_loop_2.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
normal_tab_cb.Get());
run_loop_2.Run();
// Run add tab context's callback via mock so can store token in test.
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
delayed_tab_cb;
EXPECT_CALL(delayed_tab_cb, Run(testing::_))
.WillOnce([&](const auto& result) {
// We don't store the token or quit a loop for delayed_tab_cb as it's
// not critical for the logic tested here, but we acknowledge the call.
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
delayed_tab_cb.Get());
ASSERT_TRUE(normal_tab_token_opt.has_value());
ASSERT_TRUE(file_token_opt.has_value());
// Verify added context:
ASSERT_EQ(handler_->GetNumContextUploading(), 2);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
ASSERT_TRUE(handler_->IsAnyContextUploading());
// Configure Mock to say Files/Normal Tabs are still "Processing".
contextual_search::FileInfo info_processing;
info_processing.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
info_processing.tab_session_id = session_id;
info_processing.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(testing::_))
.WillRepeatedly(testing::Return(&info_processing));
// Do not submit to server yet.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
testing::Mock::VerifyAndClearExpectations(mock_controller_.get());
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
EXPECT_CALL(*mock_controller_, GetFileInfo(testing::_))
.WillRepeatedly(testing::Return(&info_processing));
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillOnce(
[this](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
PostUploadStatusChanged(
file_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
});
handler_->SubmitQuery("Combined Test", 0, false, false, false, false,
/*is_voice_search=*/false);
base::RunLoop().RunUntilIdle();
// Delayed tabs should be uploaded once submit is run.
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
// Expect that message is stashed instead while normal tab/files are
// uploading.
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 2);
// Explicit files and non-delayed tabs need to be manually completed,
// since they started uploading before the auto-completing mock was set up.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
SimulateUploadStatusChanged(
*normal_tab_token_opt, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
SimulateUploadStatusChanged(
*file_token_opt, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
AddDeleteAdd_DelayedAndRegular_Submit) {
// Set up task and tabs, and mock related functions.
tabs::TabInterface* active_tab = browser()->tab_strip_model()->GetActiveTab();
int32_t tab_handle_id = active_tab->GetHandle().raw_value();
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
contextual_tasks::ContextualTask task(task_id);
contextual_tasks::UrlResource resource(
GURL("about:blank"), contextual_tasks::ResourceType::kUnknown);
resource.title = "about:blank";
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
// Execute full context upload compared to past tests
// to verify full upload callback workflow.
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillRepeatedly([&context](auto, auto, auto, auto callback) {
std::move(callback).Run(
std::make_unique<contextual_tasks::ContextualTaskContext>(
*context));
});
// Capture upload tab callback to simulate delayed tab pause.
lens::TabContextualizationController::GetPageContextCallback
delayed_tab_callback;
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillRepeatedly(
[&](auto callback) { delayed_tab_callback = std::move(callback); });
std::vector<base::UnguessableToken> query_tokens;
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillRepeatedly(
[&query_tokens](
const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
query_tokens.push_back(file_token);
});
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillRepeatedly(testing::Return(lens::ClientToAimMessage()));
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
cb_d1;
std::optional<base::UnguessableToken> token_d1_opt;
base::RunLoop run_loop_d1;
// Run add tab context's callback via mock so can store token in test.
EXPECT_CALL(cb_d1, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_d1_opt = result.value();
run_loop_d1.Quit();
});
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
cb_d1.Get());
run_loop_d1.Run();
ASSERT_TRUE(token_d1_opt.has_value());
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
// Delete Delayed Tab #1.
handler_->DeleteContext(*token_d1_opt, /*from_automatic_chip=*/true);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
// Add Delayed Tab #2 (which we will not delete).
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
cb_d2;
std::optional<base::UnguessableToken> token_d2_opt;
base::RunLoop run_loop_d2;
// Run add tab context's callback via mock so can store token in test.
EXPECT_CALL(cb_d2, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_d2_opt = result.value();
run_loop_d2.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
cb_d2.Get());
run_loop_d2.Run();
ASSERT_TRUE(token_d2_opt.has_value());
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
// Add regular tab A (which we will delete).
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
cb_rA;
std::optional<base::UnguessableToken> token_rA_opt;
base::RunLoop run_loop_rA;
// Run add tab context's callback via mock so can store token in test.
EXPECT_CALL(cb_rA, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_rA_opt = result.value();
run_loop_rA.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
cb_rA.Get());
run_loop_rA.Run();
ASSERT_TRUE(token_rA_opt.has_value());
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_EQ(handler_->GetNumContextUploading(), 1);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
handler_->DeleteContext(*token_rA_opt, /*from_automatic_chip=*/false);
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
// Add regular tab B (which we will keep).
base::MockCallback<ContextualTasksComposeboxHandler::AddTabContextCallback>
cb_rB;
std::optional<base::UnguessableToken> token_rB_opt;
base::RunLoop run_loop_rB;
// Run add tab context's callback via mock so can store token in test.
EXPECT_CALL(cb_rB, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_rB_opt = result.value();
run_loop_rB.Quit();
});
handler_->AddTabContext(tab_handle_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
cb_rB.Get());
run_loop_rB.Run();
ASSERT_TRUE(token_rB_opt.has_value());
ASSERT_EQ(handler_->GetNumContextUploading(), 1);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 1);
contextual_search::FileInfo file_info_rB;
file_info_rB.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
file_info_rB.tab_session_id = session_id;
file_info_rB.request_id = lens::LensOverlayRequestId();
EXPECT_CALL(*mock_controller_, GetFileInfo(testing::_))
.WillRepeatedly(testing::Return(&file_info_rB));
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->SubmitQuery("Stress Test", 0, false, false, false, false,
/*is_voice_search=*/false);
base::RunLoop().RunUntilIdle();
// Delayed tab #2 finishes uploading.
ASSERT_TRUE(!delayed_tab_callback.is_null());
auto data = std::make_unique<lens::ContextualInputData>();
data->is_page_context_eligible = true;
std::move(delayed_tab_callback).Run(std::move(data));
base::RunLoop().RunUntilIdle();
// Now manually complete the UploadTracker's pending items.
for (const auto& token : query_tokens) {
SimulateUploadStatusChanged(
token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
}
base::RunLoop().RunUntilIdle();
// Verify that still uploading.
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
ASSERT_EQ(handler_->GetNumContextUploading(), 1);
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
// Finish uploading file B.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
SimulateUploadStatusChanged(
*token_rB_opt, lens::MimeType::kHtml,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_WithVisualSelection) {
std::string kQuery = "overlay query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
base::UnguessableToken overlay_token = base::UnguessableToken::Create();
// Set task ID so we enter the relevant if block.
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Mock GetLensOverlayToken to return a token.
EXPECT_CALL(*handler_, GetLensOverlayToken())
.WillOnce(testing::Return(overlay_token));
// Expect CloseLensSync to be called.
EXPECT_CALL(
*mock_lens_controller_,
CloseLensSync(
lens::LensOverlayDismissalSource::kContextualTasksQuerySubmitted));
// Expect GetContextForTask NOT to be called (recontextualization skipped).
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.Times(0);
// Expect CreateClientToAimRequest IS called (immediate submission).
// Verify overlay token is included.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
EXPECT_THAT(info->file_tokens, testing::Contains(overlay_token));
EXPECT_TRUE(info->force_include_latest_interaction_request_data);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_WithVisualSelection_AndUpload) {
std::string kQuery = "overlay query with upload";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
base::UnguessableToken overlay_token = base::UnguessableToken::Create();
// Setup an upload to make IsAnyContextUploading() true.
auto file_info = searchbox::mojom::SelectedFileInfo::New();
file_info->file_name = "test.pdf";
file_info->mime_type = "application/pdf";
std::vector<uint8_t> data = {0x1};
// Create a pending upload
base::MockCallback<ContextualTasksComposeboxHandler::AddFileContextCallback>
callback;
base::RunLoop run_loop;
base::UnguessableToken file_token;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
file_token = result.value();
run_loop.Quit();
});
handler_->AddFileContext(std::move(file_info), mojo_base::BigBuffer(data),
callback.Get());
run_loop.Run();
ASSERT_TRUE(handler_->IsAnyContextUploading());
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
EXPECT_CALL(*handler_, GetLensOverlayToken())
.WillOnce(testing::Return(overlay_token));
EXPECT_CALL(
*mock_lens_controller_,
CloseLensSync(
lens::LensOverlayDismissalSource::kContextualTasksQuerySubmitted));
// Expect GetContextForTask TO be called because an upload is in progress.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(task_id, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// CreateClientToAimRequest should NOT be called during
// CreateAndSendQueryMessage because context is uploading.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_)).Times(0);
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
EXPECT_TRUE(handler_->HasPendingQueryForTesting());
// Now complete the upload and verify that CreateClientToAimRequest is called.
testing::Mock::VerifyAndClearExpectations(mock_controller_.get());
testing::Mock::VerifyAndClearExpectations(mock_ui_.get());
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
EXPECT_THAT(info->file_tokens, testing::Contains(overlay_token));
EXPECT_TRUE(info->force_include_latest_interaction_request_data);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
SimulateUploadStatusChanged(
file_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
EXPECT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_WithVisualSelection_AndUploadedTokens) {
std::string kQuery = "overlay query with tokens";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
base::UnguessableToken overlay_token = base::UnguessableToken::Create();
// Set task ID.
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Set up uploaded tokens in the session handle used by the handler.
session_handle_->CreateContextToken();
// Mock GetLensOverlayToken.
EXPECT_CALL(*handler_, GetLensOverlayToken())
.WillOnce(testing::Return(overlay_token));
// Expect CloseLensSync.
EXPECT_CALL(
*mock_lens_controller_,
CloseLensSync(
lens::LensOverlayDismissalSource::kContextualTasksQuerySubmitted));
// Expect GetContextForTask TO BE CALLED.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(task_id, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Expect CreateClientToAimRequest to be called eventually.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
EXPECT_EQ(info->file_tokens.size(), 2ul);
EXPECT_THAT(info->file_tokens, testing::Contains(overlay_token));
EXPECT_TRUE(info->force_include_latest_interaction_request_data);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
CreateAndSendQueryMessage_NoVisualSelection) {
std::string kQuery = "normal query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
// Token that exists but should not be used.
base::UnguessableToken overlay_token = base::UnguessableToken::Create();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Mock GetLensOverlayToken to return nullopt.
EXPECT_CALL(*handler_, GetLensOverlayToken())
.WillOnce(testing::Return(std::nullopt));
// Expect CloseLensSync to be called (it's always called).
EXPECT_CALL(
*mock_lens_controller_,
CloseLensSync(
lens::LensOverlayDismissalSource::kContextualTasksQuerySubmitted));
// Expect GetContextForTask TO be called.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(task_id, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// The test returns a context with no matching attachments to the active tab,
// so it will proceed to submission immediately.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce([&](std::unique_ptr<
contextual_search::ContextualSearchContextController::
CreateClientToAimRequestInfo> info) {
EXPECT_EQ(info->query_text, kQuery);
// Verify overlay token is NOT included.
EXPECT_THAT(info->file_tokens,
testing::Not(testing::Contains(overlay_token)));
EXPECT_FALSE(info->force_include_latest_interaction_request_data);
return lens::ClientToAimMessage();
});
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
ClearFiles_Delayed) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "clear files query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// 1. Add delayed tab context.
tabs::TabInterface* active_tab =
TabListInterface::From(browser())->GetActiveTab();
int32_t active_tab_id = active_tab->GetHandle().raw_value();
std::optional<base::UnguessableToken> token_opt;
base::MockCallback<ContextualSearchboxHandler::AddTabContextCallback>
callback;
EXPECT_CALL(callback, Run(testing::_)).WillOnce([&](const auto& result) {
ASSERT_TRUE(result.has_value());
token_opt = result.value();
});
handler_->AddTabContext(active_tab_id, /*delay_upload=*/true,
searchbox::mojom::TabAttachmentSource::kContextMenu,
callback.Get());
ASSERT_TRUE(token_opt.has_value());
base::UnguessableToken token = token_opt.value();
ASSERT_FALSE(token.is_empty());
// 2. Clear files.
handler_->ClearFiles(/*should_block_auto_suggested_tabs=*/false);
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
ASSERT_EQ(handler_->GetNumTabsDelayed(), 0);
// 3. Verify StartFileUploadFlow is NOT called.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
ClearFiles_BlockAutoSuggestedTabs) {
GURL url("https://example.com");
auto create_tab_info = [&]() {
auto info = std::make_unique<contextual_tasks::SuggestedTabInfo>();
info->url = url;
info->title = u"Example";
return info;
};
// 1. Initially, the suggestion should be allowed.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(!received_info.is_null())
<< "Expected a non-null pointer for received_info.";
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
EXPECT_TRUE(mock_ui_->IsActiveTabContextSuggestionShowing());
// 2. Blocklist the URL by clearing the files.
handler_->ClearFiles(/*should_block_auto_suggested_tabs=*/true);
searchbox_page_receiver_.FlushForTesting();
EXPECT_FALSE(mock_ui_->IsActiveTabContextSuggestionShowing());
// 3. Simulate a title change - tab context should still be filtered out.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(received_info.is_null())
<< "Expected a null pointer for received_info.";
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
EXPECT_FALSE(mock_ui_->IsActiveTabContextSuggestionShowing());
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithAutoSuggestionDisabled,
UpdateSuggestedTabContext_ForceAllowWhenUploadedViaLens) {
GURL url("https://example.com");
AddTab(url);
auto create_tab_info = [&]() {
auto info = std::make_unique<contextual_tasks::SuggestedTabInfo>();
info->url = url;
info->title = u"Example";
info->tab_id = TabListInterface::From(browser())
->GetActiveTab()
->GetHandle()
.raw_value();
return info;
};
// 2. Set the bool flag on the session handle!
session_handle_->set_is_contextual_lens_session(true);
// 3. Expect the suggestion IS ALLOWED despite the feature flag being
// disabled, because dynamic enabling sees the session bool!
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(!received_info.is_null())
<< "Expected a non-null pointer because session was contextual.";
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
EXPECT_TRUE(mock_ui_->IsActiveTabContextSuggestionShowing());
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithAutoSuggestionDisabled,
UpdateSuggestedTabContext_NoForceAllowWhenVisualQuery) {
GURL url("https://example.com");
AddTab(url);
auto create_tab_info = [&]() {
auto info = std::make_unique<contextual_tasks::SuggestedTabInfo>();
info->url = url;
info->title = u"Example";
info->tab_id = TabListInterface::From(browser())
->GetActiveTab()
->GetHandle()
.raw_value();
return info;
};
// 2. Keep the bool flag on the session handle as false!
session_handle_->set_is_contextual_lens_session(false);
// 3. Expect the suggestion IS NOT ALLOWED because it wasn't a text query.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(received_info.is_null())
<< "Expected a null pointer because it was a visual query.";
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
UpdateSuggestedTabContext) {
GURL url("https://example.com");
auto create_tab_info = [&]() {
auto info = std::make_unique<contextual_tasks::SuggestedTabInfo>();
info->url = url;
info->title = u"Example";
return info;
};
// 1. Initially, the suggestion should be allowed.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(!received_info.is_null())
<< "Expected a non-null pointer for received_info.";
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
EXPECT_TRUE(mock_ui_->IsActiveTabContextSuggestionShowing());
// 2. Blocklist the URL by dismissing an automatic chip.
// We need to navigate the active tab to the URL being blocklisted.
AddTab(url);
handler_->DeleteContext(base::UnguessableToken::Create(),
/*from_automatic_chip=*/true);
// 3. Now the suggestion should be filtered out.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(received_info.is_null())
<< "Expected a null pointer for received_info.";
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
EXPECT_FALSE(mock_ui_->IsActiveTabContextSuggestionShowing());
// 4. Explicitly adding the tab should remove it from the blocklist.
{
base::HistogramTester histogram_tester;
base::UserActionTester user_action_tester;
tabs::TabInterface* active_tab =
TabListInterface::From(browser())->GetActiveTab();
int32_t active_tab_id = active_tab->GetHandle().raw_value();
handler_->AddTabContext(active_tab_id, /*delay_upload=*/false,
searchbox::mojom::TabAttachmentSource::kContextMenu,
base::DoNothing());
histogram_tester.ExpectTotalCount(
"ContextualTasks.Composebox.UserAction."
"AddedActiveTabAfterDeletingAutoSuggestion",
1);
EXPECT_EQ(user_action_tester.GetActionCount(
"ContextualTasks.Composebox.UserAction."
"AddedActiveTabAfterDeletingAutoSuggestion"),
1);
}
// 5. The suggestion should be allowed again.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(!received_info.is_null())
<< "Expected a non-null pointer for received_info.";
EXPECT_EQ(received_info->url, url);
});
auto_suggestion_manager_.SetCurrentSuggestion(create_tab_info());
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
EXPECT_TRUE(mock_ui_->IsActiveTabContextSuggestionShowing());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
AddFileContext_NullSessionHandle) {
// Create a handler with a callback that returns nullptr for session handle.
mojo::PendingRemote<searchbox::mojom::Page> searchbox_page_remote;
mojo::PendingReceiver<searchbox::mojom::Page> searchbox_page_receiver =
searchbox_page_remote.InitWithNewPipeAndPassReceiver();
auto handler = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
std::move(searchbox_page_remote),
base::BindRepeating(
[]() -> contextual_search::ContextualSearchSessionHandle* {
return nullptr;
}),
base::DoNothing(),
base::BindRepeating(&ContextualTasksUI::TakeInputStateModel,
base::Unretained(mock_ui_.get())));
auto file_info = searchbox::mojom::SelectedFileInfo::New();
std::vector<uint8_t> data = {0x1};
mojo_base::BigBuffer file_bytes(data);
base::MockCallback<ContextualTasksComposeboxHandler::AddFileContextCallback>
callback;
base::expected<base::UnguessableToken,
contextual_search::ContextUploadErrorType>
result;
EXPECT_CALL(callback, Run(testing::_)).WillOnce(testing::SaveArg<0>(&result));
handler->AddFileContext(std::move(file_info), std::move(file_bytes),
callback.Get());
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
contextual_search::ContextUploadErrorType::kBrowserProcessingError);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
OnContextUploadStatusChanged_LensOverlayToken_Ignored) {
base::UnguessableToken lens_token = base::UnguessableToken::Create();
EXPECT_CALL(*mock_lens_controller_->mock_router(),
overlay_tab_context_file_token())
.WillRepeatedly(testing::Return(lens_token));
EXPECT_CALL(mock_searchbox_page_, OnContextualInputStatusChanged(
testing::_, testing::_, testing::_))
.Times(0);
handler_->OnContextUploadStatusChanged(
lens_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
// Verify that for a different token, it IS called.
base::UnguessableToken other_token = base::UnguessableToken::Create();
EXPECT_CALL(mock_searchbox_page_, OnContextualInputStatusChanged(
testing::_, testing::_, testing::_))
.Times(1);
handler_->OnContextUploadStatusChanged(
other_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
ActiveModelIsPassed) {
// 1. Arrange: Setup a mock callback to simulate ContextualTasksUI returning a
// model. We explicitly set a distinct state (MODEL_MODE_GEMINI_PRO) to verify
// it gets passed correctly.
auto mock_callback = base::BindRepeating(
&ContextualTasksComposeboxHandlerTest::CreateMockInputStateModel,
base::Unretained(this));
mojo::PendingRemote<searchbox::mojom::Page> searchbox_page_remote;
mojo::PendingReceiver<searchbox::mojom::Page> searchbox_page_receiver =
searchbox_page_remote.InitWithNewPipeAndPassReceiver();
auto custom_handler = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
std::move(searchbox_page_remote),
base::BindRepeating(
&ContextualTasksUI::GetOrCreateContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::ClearContextualSessionHandle,
base::Unretained(mock_ui_.get())),
std::move(mock_callback));
// 2. Act: Trigger the handler to fetch the model via the callback.
custom_handler->InitializeInputStateModel();
// 3. Assert: Verify the handler successfully took ownership of the model
// and the internal state matches exactly what the callback provided.
contextual_search::InputStateModel* handler_model =
custom_handler->TakeInputStateModelForTesting();
ASSERT_NE(handler_model, nullptr);
EXPECT_EQ(handler_model->get_state_for_testing().active_model,
omnibox::ModelMode::MODEL_MODE_GEMINI_PRO);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SuggestInputsCallbackWorks) {
auto mock_session =
std::make_unique<contextual_search::MockContextualSearchSessionHandle>();
lens::proto::LensOverlaySuggestInputs suggest_inputs;
EXPECT_CALL(*mock_session, GetSuggestInputs())
.WillRepeatedly(testing::Return(suggest_inputs));
auto mock_session_ptr = mock_session.get();
auto mock_get_session_callback = base::BindRepeating(
[](contextual_search::MockContextualSearchSessionHandle* ptr)
-> contextual_search::ContextualSearchSessionHandle* { return ptr; },
mock_session_ptr);
mojo::PendingRemote<searchbox::mojom::Page> searchbox_page_remote;
mojo::PendingReceiver<searchbox::mojom::Page> searchbox_page_receiver =
searchbox_page_remote.InitWithNewPipeAndPassReceiver();
auto custom_handler = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
std::move(searchbox_page_remote), mock_get_session_callback,
base::BindRepeating(&ContextualTasksUI::ClearContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::TakeInputStateModel,
base::Unretained(mock_ui_.get())));
auto* client = static_cast<ContextualOmniboxClient*>(
custom_handler->GetOmniboxClientForTesting());
auto result = client->GetLensOverlaySuggestInputsForTesting();
ASSERT_TRUE(result.has_value());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
OnLensThumbnailCreated_TriggersUploadStatusChanges) {
// Setup: mock the overlay token.
base::UnguessableToken overlay_token = base::UnguessableToken::Create();
EXPECT_CALL(*mock_lens_controller_->mock_router(),
overlay_tab_context_file_token())
.WillRepeatedly(testing::Return(overlay_token));
// Mock OnContextUploadStatusChanged to verify calls and forward to real
// implementation.
// We capture the uploaded tokens to verify them.
std::vector<base::UnguessableToken> successful_uploads;
std::vector<base::UnguessableToken> replaced_uploads;
EXPECT_CALL(*handler_, OnContextUploadStatusChanged(testing::_, testing::_,
testing::_, testing::_))
.WillRepeatedly([&](const base::UnguessableToken& context_token,
lens::MimeType mime_type,
contextual_search::ContextUploadStatus
context_upload_status,
const std::optional<
contextual_search::ContextUploadErrorType>&
error_type) {
if (context_upload_status ==
contextual_search::ContextUploadStatus::kUploadSuccessful) {
successful_uploads.push_back(context_token);
} else if (context_upload_status ==
contextual_search::ContextUploadStatus::kUploadReplaced) {
replaced_uploads.push_back(context_token);
}
handler_
->ContextualTasksComposeboxHandler::OnContextUploadStatusChanged(
context_token, mime_type, context_upload_status, error_type);
});
// 1. First selection.
std::string thumbnail_data = "data:image/png;base64,DATA";
handler_->OnLensThumbnailCreated(thumbnail_data);
// Verify: Should have one successful upload and no replacements.
ASSERT_EQ(successful_uploads.size(), 1u);
ASSERT_EQ(replaced_uploads.size(), 0u);
base::UnguessableToken first_token = successful_uploads[0];
// 2. Second selection (replace).
std::string thumbnail_data_2 = "data:image/png;base64,DATA2";
handler_->OnLensThumbnailCreated(thumbnail_data_2);
// Verify: Should have one replacement (the first token) and one new
// successful upload.
ASSERT_EQ(successful_uploads.size(), 2u);
ASSERT_EQ(replaced_uploads.size(), 1u);
EXPECT_EQ(replaced_uploads[0], first_token);
EXPECT_NE(successful_uploads[1], first_token);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_WaitsForRecontextualization) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "stashed query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context with uploaded tab.
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("about:blank");
std::string kTitle = "about:blank";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kWebpage);
resource.title = kTitle;
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup FileInfo with expired status.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadExpired;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// 1. Capture the GetPageContext callback.
MockTabContextualizationController::GetPageContextCallback pending_callback;
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([&](MockTabContextualizationController::GetPageContextCallback
callback) { pending_callback = std::move(callback); });
// 2. Call CreateAndSendQueryMessage.
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
// Verify: recontextualization is pending, so the query is blocked.
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_FALSE(pending_callback.is_null());
// 3. Start file upload when recontextualizer completes context fetch.
base::UnguessableToken uploaded_token;
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.WillOnce([&](const base::UnguessableToken& file_token,
std::unique_ptr<lens::ContextualInputData> data,
std::optional<lens::ImageEncodingOptions> image_options) {
EXPECT_TRUE(data->is_implicit_upload);
uploaded_token = file_token;
});
auto data = std::make_unique<lens::ContextualInputData>();
data->tab_session_id = session_id;
data->page_url = GURL("about:blank");
data->page_title = "about:blank";
data->context_id = 12345;
data->is_page_context_eligible = true;
std::move(pending_callback).Run(std::move(data));
// Verify: Still uploading and query not stashed in handler (as
// recontextualization waits in UploadTracker before
// ContinueCreateAndSendQueryMessage is triggered).
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
// 4. Complete the upload status change.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
PostUploadStatusChanged(
uploaded_token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful);
run_loop.Run();
// Verify: Upload finished, stashed query is successfully sent.
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
Recontextualization_TabInvalidatedGracefullyCompletes) {
ASSERT_NE(mock_contextual_tasks_service_ptr_, nullptr)
<< "Mock controller is NULL!";
std::string kQuery = "invalid tab query";
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context.
contextual_tasks::ContextualTask task(task_id);
SessionID session_id = sessions::SessionTabHelper::IdForTab(web_contents());
GURL kUrl("about:blank");
std::string kTitle = "about:blank";
contextual_tasks::UrlResource resource(
kUrl, contextual_tasks::ResourceType::kWebpage);
resource.title = kTitle;
resource.tab_id = session_id;
task.AddUrlResource(resource);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(
*mock_contextual_tasks_service_ptr_,
GetContextForTask(
task_id,
testing::Contains(contextual_tasks::ContextualTaskContextSource::
kSubmittedContextDecorator),
testing::NotNull(), testing::_))
.WillOnce(
[&context](
const base::Uuid& task_id,
const std::set<contextual_tasks::ContextualTaskContextSource>&
sources,
std::unique_ptr<contextual_tasks::ContextDecorationParams> params,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
// Setup FileInfo with expired status.
std::vector<raw_ptr<const contextual_search::FileInfo>> file_info_list;
contextual_search::FileInfo file_info;
file_info.tab_session_id = session_id;
file_info.upload_status =
contextual_search::ContextUploadStatus::kUploadExpired;
file_info.request_id.emplace();
file_info.request_id->set_context_id(12345);
file_info_list.push_back(&file_info);
EXPECT_CALL(*mock_controller_, GetFileInfoList())
.WillRepeatedly(testing::Return(file_info_list));
// 1. GetPageContext returns nullptr, representing tab was invalidated.
EXPECT_CALL(*mock_tab_controller_, GetPageContext(testing::_))
.WillOnce([](MockTabContextualizationController::GetPageContextCallback
callback) { std::move(callback).Run(nullptr); });
// 2. Upload is NOT triggered.
EXPECT_CALL(*mock_controller_,
StartFileUploadFlow(testing::_, testing::_, testing::_))
.Times(0);
// 3. Complete direct AIM query submission without context upload.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
base::RunLoop run_loop;
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
handler_->CreateAndSendQueryMessage(kQuery, /*is_voice_search=*/false);
run_loop.Run();
// Verify: No context was uploaded, pending uploads are back to 0.
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
SubmitQuery_WaitsForModalityChipUpload) {
base::Uuid task_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_ui_, GetTaskId())
.WillRepeatedly(
testing::ReturnRefOfCopy(std::optional<base::Uuid>(task_id)));
// Setup context.
contextual_tasks::ContextualTask task(task_id);
auto context =
std::make_unique<contextual_tasks::ContextualTaskContext>(task);
EXPECT_CALL(*mock_contextual_tasks_service_ptr_,
GetContextForTask(testing::_, testing::_, testing::_, testing::_))
.WillOnce(
[&context](
const base::Uuid&,
const std::set<contextual_tasks::ContextualTaskContextSource>&,
std::unique_ptr<contextual_tasks::ContextDecorationParams>,
base::OnceCallback<void(
std::unique_ptr<contextual_tasks::ContextualTaskContext>)>
callback) { std::move(callback).Run(std::move(context)); });
base::UnguessableToken token = base::UnguessableToken::Create();
// Setup FileInfo representing a server-injected modality chip in kProcessing
// status.
contextual_search::FileInfo uploading_info{};
uploading_info.upload_status =
contextual_search::ContextUploadStatus::kProcessing;
uploading_info.mime_type = lens::MimeType::kUnknown;
auto input_data = std::make_unique<lens::ContextualInputData>();
input_data->modality_chip_props.emplace();
input_data->modality_chip_props->set_id("test_chip_id");
uploading_info.input_data = std::move(input_data);
EXPECT_CALL(*mock_controller_, GetFileInfo(token))
.WillRepeatedly(testing::Return(&uploading_info));
// Expect no queries are sent immediately because the chip is still uploading.
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(0);
// Simulate the status transition to kProcessing, which should register the
// modality chip in the handler's pending uploads set.
SimulateUploadStatusChanged(
token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kProcessing, std::nullopt);
// Verify the chip is tracked as uploading.
ASSERT_TRUE(handler_->IsAnyContextUploading());
ASSERT_EQ(handler_->GetNumContextUploading(), 1);
// Submit query manually. It should be stashed.
handler_->SubmitQuery("Test query", 0, false, false, false, false,
/*is_voice_search=*/false);
ASSERT_TRUE(handler_->HasPendingQueryForTesting());
// Now expect the stashed query to be sent when the chip completes
// successfully.
EXPECT_CALL(*mock_controller_, CreateClientToAimRequest(testing::_))
.WillOnce(testing::Return(lens::ClientToAimMessage()));
EXPECT_CALL(*mock_ui_, PostAimMessage(testing::_)).Times(1);
// Simulate transition to kUploadSuccessful.
SimulateUploadStatusChanged(
token, lens::MimeType::kUnknown,
contextual_search::ContextUploadStatus::kUploadSuccessful, std::nullopt);
// Verify pending uploads are cleared and the query has been sent.
ASSERT_FALSE(handler_->IsAnyContextUploading());
ASSERT_FALSE(handler_->HasPendingQueryForTesting());
ASSERT_EQ(handler_->GetNumContextUploading(), 0);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
MultiFilesSelected) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath file1_path = temp_dir.GetPath().AppendASCII("file1.pdf");
base::FilePath file2_path = temp_dir.GetPath().AppendASCII("file2.png");
std::string file1_content = "dummy pdf content";
std::string file2_content = "dummy image content";
ASSERT_TRUE(base::WriteFile(file1_path, file1_content));
ASSERT_TRUE(base::WriteFile(file2_path, file2_content));
std::vector<ui::SelectedFileInfo> files;
files.emplace_back(file1_path, file1_path);
files.emplace_back(file2_path, file2_path);
base::RunLoop run_loop;
int file_contexts_added = 0;
EXPECT_CALL(mock_searchbox_page_, AddFileContext(testing::_, testing::_))
.Times(2)
.WillRepeatedly([&](const base::UnguessableToken& token,
searchbox::mojom::SelectedFileInfoPtr file_info) {
file_contexts_added++;
if (file_info->file_name == "file1.pdf") {
EXPECT_EQ(file_info->mime_type, "application/pdf");
} else if (file_info->file_name == "file2.png") {
EXPECT_EQ(file_info->mime_type, "image/png");
}
if (file_contexts_added == 2) {
run_loop.Quit();
}
});
handler_->MultiFilesSelected(files);
// Wait for ThreadPool tasks to finish processing files.
run_loop.Run();
EXPECT_EQ(handler_->GetNumContextUploading(), 2);
}
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerTest,
UpdateStateFromUrl_SoftNavigation) {
// Arrange: Setup local config with Canvas tool and its url params.
omnibox::SearchboxConfig config;
auto* canvas_config = config.add_tool_configs();
canvas_config->set_tool(omnibox::ToolMode::TOOL_MODE_CANVAS);
auto* canvas_param = canvas_config->add_aim_url_params();
canvas_param->set_param_key("rc");
canvas_param->set_param_value("1");
// Create composebox handler using a custom callback that binds the local
// config.
auto mock_callback = base::BindRepeating(
[](contextual_search::ContextualSearchSessionHandle* session_handle,
const omnibox::SearchboxConfig config) {
return std::make_unique<contextual_search::InputStateModel>(
*session_handle, config, GURL(), /*is_off_the_record=*/false,
/*is_signed_in=*/false,
/*browser_identity_matches_aim_identity=*/false);
},
session_handle_.get(), config);
mojo::PendingRemote<searchbox::mojom::Page> searchbox_page_remote;
mojo::PendingReceiver<searchbox::mojom::Page> searchbox_page_receiver =
searchbox_page_remote.InitWithNewPipeAndPassReceiver();
auto custom_handler = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
std::move(searchbox_page_remote),
base::BindRepeating(
&ContextualTasksUI::GetOrCreateContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::ClearContextualSessionHandle,
base::Unretained(mock_ui_.get())),
std::move(mock_callback));
custom_handler->InitializeInputStateModel();
contextual_search::InputStateModel* model =
custom_handler->TakeInputStateModelForTesting();
ASSERT_NE(model, nullptr);
// Default tool is unspecified.
EXPECT_EQ(model->get_state_for_testing().active_tool,
omnibox::ToolMode::TOOL_MODE_UNSPECIFIED);
// Act: Simulate soft navigation by calling UpdateStateFromUrl with Canvas
// GURL.
GURL canvas_url("https://example.com/?rc=1");
custom_handler->UpdateStateFromUrl(canvas_url);
// Assert: Verify the tool successfully restored/persisted to Canvas.
EXPECT_EQ(model->get_state_for_testing().active_tool,
omnibox::ToolMode::TOOL_MODE_CANVAS);
EXPECT_TRUE(model->get_state_for_testing().is_canvas_query_submitted);
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled,
SetAimThreadRestoredTabs) {
SetUpHandler();
ASSERT_NE(handler_, nullptr);
std::vector<searchbox::mojom::TabInfoPtr> restored_tabs;
auto tab_info = searchbox::mojom::TabInfo::New();
tab_info->url = GURL("https://example.com");
tab_info->title = "Example Site";
restored_tabs.push_back(std::move(tab_info));
EXPECT_CALL(mock_searchbox_page_, SetAimThreadRestoredTabs(testing::_))
.WillOnce([&](const std::vector<searchbox::mojom::TabInfoPtr>& tabs) {
EXPECT_EQ(tabs.size(), 1u);
EXPECT_EQ(tabs[0]->url, GURL("https://example.com"));
EXPECT_EQ(tabs[0]->title, "Example Site");
});
handler_->SetAimThreadRestoredTabs(std::move(restored_tabs));
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled,
SetAimThreadRestoredTabs_ClearsAutoSuggestedTabWhenRestored) {
SetUpHandler();
ASSERT_NE(handler_, nullptr);
const GURL url("https://example.com");
auto suggestion = std::make_unique<contextual_tasks::SuggestedTabInfo>();
suggestion->url = url;
suggestion->title = u"Example Site";
suggestion->tab_id = 42;
auto_suggestion_manager_.SetCurrentSuggestion(std::move(suggestion));
std::vector<searchbox::mojom::TabInfoPtr> restored_tabs;
auto tab_info = searchbox::mojom::TabInfo::New();
tab_info->url = url;
tab_info->title = "Example Site";
tab_info->tab_id = 42;
restored_tabs.push_back(std::move(tab_info));
// Verify that UpdateAutoSuggestedTabContext(nullptr) is dispatched to WebUI
// to clear the uncommitted suggestion chip.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(received_info.is_null());
EXPECT_EQ(invocation_source, std::nullopt);
});
EXPECT_CALL(mock_searchbox_page_, SetAimThreadRestoredTabs(testing::_))
.WillOnce([url](const std::vector<searchbox::mojom::TabInfoPtr>& tabs) {
ASSERT_EQ(tabs.size(), 1u);
EXPECT_EQ(tabs[0]->tab_id, 42);
EXPECT_EQ(tabs[0]->url, url);
});
handler_->SetAimThreadRestoredTabs(std::move(restored_tabs));
searchbox_page_receiver_.FlushForTesting();
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled,
UpdateSuggestedTabContext_SuppressedIfTabAlreadyRestored) {
SetUpHandler();
ASSERT_NE(handler_, nullptr);
const GURL url("https://example.com");
auto suggestion = std::make_unique<contextual_tasks::SuggestedTabInfo>();
suggestion->url = url;
suggestion->title = u"Example Site";
suggestion->tab_id = 42;
auto_suggestion_manager_.SetCurrentSuggestion(std::move(suggestion));
// Mock UI indicates tab_id 42 is already restored in the task.
EXPECT_CALL(*mock_ui_, GetRestoredTabIds())
.WillRepeatedly(testing::Return(std::vector<int32_t>{42}));
// Expect that auto-suggestion is suppressed (sent as nullptr) because the tab
// is already restored.
EXPECT_CALL(mock_searchbox_page_,
UpdateAutoSuggestedTabContext(testing::_, testing::_))
.WillOnce([&](const searchbox::mojom::TabInfoPtr& received_info,
const std::optional<std::string>& invocation_source) {
EXPECT_TRUE(received_info.is_null());
});
handler_->UpdateSuggestedTabContext(
auto_suggestion_manager_.GetCurrentSuggestion());
searchbox_page_receiver_.FlushForTesting();
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled,
CacheSubmittedTabsOnInit) {
class TestSupportsTabHandles : public tabs::SupportsTabHandles {
public:
void SetSessionId(int32_t session_id) {
tabs::SupportsTabHandles::SetSessionId(session_id);
}
};
// Use the default tab created in SetUpOnMainThread.
tabs::TabInterface* tab1 =
tabs::TabLookupFromWebContents::FromWebContents(web_contents_)->model();
ASSERT_NE(tab1, nullptr);
SessionID session_id1 =
sessions::SessionTabHelper::FromWebContents(tab1->GetContents())
->session_id();
// Add a second tab.
tabs::TabInterface* tab2 = AddTab(GURL("about:blank#2"));
ASSERT_NE(tab2, nullptr);
SessionID session_id2 =
sessions::SessionTabHelper::FromWebContents(tab2->GetContents())
->session_id();
auto mock_session = std::make_unique<testing::NiceMock<
contextual_search::MockContextualSearchSessionHandle>>();
std::vector<contextual_search::FileInfo> submitted_file_infos;
base::Time now = base::Time::Now();
contextual_search::FileInfo tab_info1;
tab_info1.tab_url = GURL("about:blank#1");
tab_info1.tab_title = "About Blank 1";
tab_info1.tab_session_id = session_id1;
tab_info1.mime_type = lens::MimeType::kHtml;
tab_info1.selection_time = now;
submitted_file_infos.push_back(tab_info1);
contextual_search::FileInfo tab_info2;
tab_info2.tab_url = GURL("about:blank#2");
tab_info2.tab_title = "About Blank 2";
tab_info2.tab_session_id = session_id2;
tab_info2.mime_type = lens::MimeType::kHtml;
tab_info2.selection_time = now + base::Seconds(1);
submitted_file_infos.push_back(tab_info2);
// We should also include a raw file to ensure it's filtered out.
contextual_search::FileInfo file_info;
file_info.file_name = "test.pdf";
submitted_file_infos.push_back(file_info);
EXPECT_CALL(*mock_session, GetSubmittedContextFileInfos())
.WillRepeatedly(testing::Return(submitted_file_infos));
mock_ui_->SetSessionHandle(mock_session.get());
EXPECT_CALL(mock_searchbox_page_, SetAimThreadRestoredTabs(testing::_))
.WillOnce([&](const std::vector<searchbox::mojom::TabInfoPtr>& tabs) {
EXPECT_EQ(tabs.size(), 2u);
EXPECT_EQ(tabs[0]->url, GURL("about:blank#1"));
EXPECT_EQ(tabs[0]->title, "About Blank 1");
EXPECT_EQ(tabs[0]->tab_id, 2);
EXPECT_EQ(tabs[1]->url, GURL("about:blank#2"));
EXPECT_EQ(tabs[1]->title, "About Blank 2");
EXPECT_EQ(tabs[1]->tab_id, 3);
});
SetUpHandler();
ASSERT_NE(handler_, nullptr);
// Clean up session handle from mock UI.
mock_ui_->SetSessionHandle(nullptr);
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerTestWithContextManagementEnabled,
CacheSubmittedTabsOnInit_UnmappedClosedTab) {
auto mock_session = std::make_unique<testing::NiceMock<
contextual_search::MockContextualSearchSessionHandle>>();
std::vector<contextual_search::FileInfo> submitted_file_infos;
const SessionID closed_tab_session_id = SessionID::FromSerializedValue(9999);
contextual_search::FileInfo closed_tab_info;
closed_tab_info.tab_url = GURL("https://example.com/closed");
closed_tab_info.tab_title = "Closed Tab";
closed_tab_info.tab_session_id = closed_tab_session_id;
closed_tab_info.mime_type = lens::MimeType::kHtml;
closed_tab_info.selection_time = base::Time::Now();
submitted_file_infos.push_back(closed_tab_info);
EXPECT_CALL(*mock_session, GetSubmittedContextFileInfos())
.WillRepeatedly(testing::Return(submitted_file_infos));
mock_ui_->SetSessionHandle(mock_session.get());
EXPECT_CALL(mock_searchbox_page_, SetAimThreadRestoredTabs(testing::_))
.WillOnce([&](const std::vector<searchbox::mojom::TabInfoPtr>& tabs) {
ASSERT_EQ(tabs.size(), 1u);
EXPECT_EQ(tabs[0]->url, GURL("https://example.com/closed"));
EXPECT_EQ(tabs[0]->title, "Closed Tab");
// Verify fallback to raw SessionID integer when tab is unmapped.
EXPECT_EQ(tabs[0]->tab_id, 9999);
});
SetUpHandler();
ASSERT_NE(handler_, nullptr);
mock_ui_->SetSessionHandle(nullptr);
}
class ContextualTasksComposeboxHandlerAutoTriggerTest
: public ContextualTasksComposeboxHandlerTest {
public:
ContextualTasksComposeboxHandlerAutoTriggerTest() {
local_feature_list_.InitAndEnableFeatureWithParameters(
omnibox::kWebUIOmniboxAskGAboutThisPage,
{{"Omnibox_AskGCoBrowseWithVisualSelection", "true"}});
}
private:
base::test::ScopedFeatureList local_feature_list_;
};
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerAutoTriggerTest, AutoTriggerLens) {
// Destroy the default handler and reset the receiver to allow rebinding.
handler_.reset();
searchbox_page_receiver_.reset();
// Set the invocation source on the mock LensSearchController.
mock_lens_controller_->SetInvocationSource(
lens::LensOverlayInvocationSource::kOmniboxPageAction);
// We expect OpenLensOverlay to be called when OnTaskChanged is called.
EXPECT_CALL(
*mock_lens_controller_,
OpenLensOverlay(
lens::LensOverlayInvocationSource::kOmniboxPageAction,
testing::_))
.Times(1);
// Manually create the handler to use our mock page.
auto custom_handler = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
searchbox_page_receiver_.BindNewPipeAndPassRemote(),
base::BindRepeating(
&ContextualTasksUI::GetOrCreateContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::ClearContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::TakeInputStateModel,
base::Unretained(mock_ui_.get())));
ON_CALL(*custom_handler, GetLensSearchController())
.WillByDefault(testing::Return(mock_lens_controller_.get()));
custom_handler->OnTaskChanged();
}
class ContextualTasksComposeboxHandlerSmartTabSharingTest
: public ContextualTasksComposeboxHandlerTest {
public:
ContextualTasksComposeboxHandlerSmartTabSharingTest() {
feature_list_sts_.InitWithFeaturesAndParameters(
{{contextual_tasks::kContextualTasksContext,
{{"ContextualTasksContextSmartTabSharing", "true"}}},
{contextual_tasks::kContextualTasksForceEntryPointEligibility, {}}},
{});
}
~ContextualTasksComposeboxHandlerSmartTabSharingTest() override = default;
private:
base::test::ScopedFeatureList feature_list_sts_;
};
IN_PROC_BROWSER_TEST_F(ContextualTasksComposeboxHandlerSmartTabSharingTest,
OnTaskChangedResetsSmartTabSharing) {
// Enable STS.
handler_->SetSmartTabSharingActive(true);
EXPECT_TRUE(handler_->IsSmartTabSharingActive());
// Call OnTaskChanged.
handler_->OnTaskChanged();
// Verify STS is disabled.
EXPECT_FALSE(handler_->IsSmartTabSharingActive());
}
IN_PROC_BROWSER_TEST_F(
ContextualTasksComposeboxHandlerSmartTabSharingTest,
InitializeInputStateModelPreservesSmartTabSharingFromInputStateModel) {
auto mock_session = std::make_unique<
testing::NiceMock<contextual_search::MockContextualSearchSessionHandle>>();
auto input_state_model = std::make_unique<contextual_search::InputStateModel>(
*mock_session, omnibox::SearchboxConfig(), GURL(), false, false, false);
input_state_model->SetSmartTabSharingActive(true);
searchbox_page_receiver_.reset();
auto custom_handler = std::make_unique<TestContextualTasksComposeboxHandler>(
mock_ui_.get(), profile(), web_contents(),
mojo::PendingReceiver<composebox::mojom::PageHandler>(),
mojo::PendingReceiver<searchbox::mojom::PageHandler>(),
searchbox_page_receiver_.BindNewPipeAndPassRemote(),
base::BindRepeating(
&ContextualTasksUI::GetOrCreateContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(&ContextualTasksUI::ClearContextualSessionHandle,
base::Unretained(mock_ui_.get())),
base::BindRepeating(
[](std::unique_ptr<contextual_search::InputStateModel>* model) {
return std::move(*model);
},
base::Unretained(&input_state_model)));
EXPECT_TRUE(custom_handler->IsSmartTabSharingActive());
}